Files
zeekay f480e48e39 run on the native fleet
Adds .hanzo/workflows/cicd.yml calling the canonical pipeline and drops the
GitHub-hosted workflows it replaces. Lands here because this is where the repo
is written — git.hanzo.ai is upstream and github.com is its push mirror, so the
same change made on github.com never reaches the runner fleet.
2026-08-20 20:20:25 -07:00

726 lines
19 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package builder
import (
"errors"
"fmt"
"slices"
"time"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/math"
"github.com/luxfi/proto/p/config"
"github.com/luxfi/proto/p/fx"
"github.com/luxfi/proto/p/state"
"github.com/luxfi/proto/p/txs"
"github.com/luxfi/proto/p/utxo"
"github.com/luxfi/runtime"
"github.com/luxfi/timer/mockable"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
func sortByCompare[T interface{ Compare(T) int }](s []T) {
slices.SortFunc(s, func(a, b T) int {
return a.Compare(b)
})
}
// Max number of items allowed in a page
const MaxPageSize = 1024
var (
_ Builder = (*builder)(nil)
ErrNoFunds = errors.New("no spendable funds were found")
)
type Builder interface {
AtomicTxBuilder
DecisionTxBuilder
ProposalTxBuilder
}
type AtomicTxBuilder interface {
// chainID: chain to import UTXOs from
// to: address of recipient
// keys: keys to import the funds
// changeAddr: address to send change to, if there is any
NewImportTx(
chainID ids.ID,
to ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
// amount: amount of tokens to export
// chainID: chain to send the UTXOs to
// to: address of recipient
// keys: keys to pay the fee and provide the tokens
// changeAddr: address to send change to, if there is any
NewExportTx(
amount uint64,
chainID ids.ID,
to ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
}
type DecisionTxBuilder interface {
// netID: ID of the net that validates the new chain
// genesisData: byte repr. of genesis state of the new chain
// vmID: ID of VM this chain runs
// fxIDs: ids of features extensions this chain supports
// chainName: name of the chain
// keys: keys to sign the tx
// changeAddr: address to send change to, if there is any
NewCreateChainTx(
netID ids.ID,
genesisData []byte,
vmID ids.ID,
fxIDs []ids.ID,
chainName string,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
// threshold: [threshold] of [ownerAddrs] needed to manage this network
// ownerAddrs: control addresses for the new network
// keys: keys to pay the fee
// changeAddr: address to send change to, if there is any
NewCreateNetworkTx(
threshold uint32,
ownerAddrs []ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
// amount: amount the sender is sending
// owner: recipient of the funds
// keys: keys to sign the tx and pay the amount
// changeAddr: address to send change to, if there is any
NewBaseTx(
amount uint64,
owner secp256k1fx.OutputOwners,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
}
type ProposalTxBuilder interface {
// stakeAmount: amount the validator stakes
// startTime: unix time they start validating
// endTime: unix time they stop validating
// nodeID: ID of the node we want to validate with
// rewardAddress: address to send reward to, if applicable
// shares: 10,000 times percentage of reward taken from delegators
// keys: Keys providing the staked tokens
// changeAddr: Address to send change to, if there is any
NewAddValidatorTx(
stakeAmount,
startTime,
endTime uint64,
nodeID ids.NodeID,
rewardAddress ids.ShortID,
shares uint32,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
// stakeAmount: amount the delegator stakes
// startTime: unix time they start delegating
// endTime: unix time they stop delegating
// nodeID: ID of the node we are delegating to
// rewardAddress: address to send reward to, if applicable
// keys: keys providing the staked tokens
// changeAddr: address to send change to, if there is any
NewAddDelegatorTx(
stakeAmount,
startTime,
endTime uint64,
nodeID ids.NodeID,
rewardAddress ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
// weight: sampling weight of the new validator
// startTime: unix time they start delegating
// endTime: unix time they top delegating
// nodeID: ID of the node validating
// netID: ID of the net the validator will validate
// keys: keys to use for adding the validator
// changeAddr: address to send change to, if there is any
NewAddChainValidatorTx(
weight,
startTime,
endTime uint64,
nodeID ids.NodeID,
netID ids.ID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
// Creates a transaction that removes [nodeID]
// as a validator from [netID]
// keys: keys to use for removing the validator
// changeAddr: address to send change to, if there is any
NewRemoveChainValidatorTx(
nodeID ids.NodeID,
netID ids.ID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
// Creates a transaction that transfers ownership of [netID]
// threshold: [threshold] of [ownerAddrs] needed to manage this chain
// ownerAddrs: control addresses for the new chain
// keys: keys to use for modifying the chain
// changeAddr: address to send change to, if there is any
NewTransferChainOwnershipTx(
netID ids.ID,
threshold uint32,
ownerAddrs []ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error)
// newAdvanceTimeTx creates a new tx that, if it is accepted and followed by a
// Commit block, will set the chain's timestamp to [timestamp].
NewAdvanceTimeTx(timestamp time.Time) (*txs.Tx, error)
// RewardStakerTx creates a new transaction that proposes to remove the staker
// [validatorID] from the default validator set.
NewRewardValidatorTx(txID ids.ID) (*txs.Tx, error)
}
// New constructs a Builder.
func New(
rt *runtime.Runtime,
cfg *config.Config,
clk *mockable.Clock,
fx fx.Fx,
state state.State,
atomicUTXOManager lux.AtomicUTXOManager,
utxoSpender utxo.Spender,
) Builder {
return &builder{
AtomicUTXOManager: atomicUTXOManager,
Spender: utxoSpender,
state: state,
cfg: cfg,
rt: rt,
NetworkID: rt.NetworkID,
ChainID: rt.ChainID,
UTXOAssetID: rt.UTXOAssetID,
clk: clk,
fx: fx,
}
}
type builder struct {
lux.AtomicUTXOManager
utxo.Spender
state state.State
cfg *config.Config
rt *runtime.Runtime
NetworkID uint32
ChainID ids.ID
UTXOAssetID ids.ID
clk *mockable.Clock
fx fx.Fx
}
func (b *builder) NewImportTx(
from ids.ID,
to ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
kc := secp256k1fx.NewKeychain(keys...)
addrs := kc.Addresses()
atomicUTXOs, _, _, err := b.GetAtomicUTXOs(from, addrs, ids.ShortEmpty, ids.Empty, MaxPageSize)
if err != nil {
return nil, fmt.Errorf("problem retrieving atomic UTXOs: %w", err)
}
importedInputs := []*lux.TransferableInput{}
signers := [][]*secp256k1.PrivateKey{}
importedAmounts := make(map[ids.ID]uint64)
now := b.clk.Unix()
for _, utxo := range atomicUTXOs {
inputIntf, utxoSigners, err := kc.Spend(utxo.Out, now)
if err != nil {
continue
}
input, ok := inputIntf.(lux.TransferableIn)
if !ok {
continue
}
assetID := utxo.AssetID()
importedAmounts[assetID], err = math.Add(importedAmounts[assetID], input.Amount())
if err != nil {
return nil, err
}
importedInputs = append(importedInputs, &lux.TransferableInput{
UTXOID: utxo.UTXOID,
Asset: utxo.Asset,
In: input,
})
signers = append(signers, utxoSigners)
}
lux.SortTransferableInputsWithSigners(importedInputs, signers)
if len(importedAmounts) == 0 {
return nil, ErrNoFunds // No imported UTXOs were spendable
}
importedLUX := importedAmounts[b.UTXOAssetID]
ins := []*lux.TransferableInput{}
outs := []*lux.TransferableOutput{}
switch {
case importedLUX < b.cfg.TxFee: // imported amount goes toward paying tx fee
var baseSigners [][]*secp256k1.PrivateKey
ins, outs, _, baseSigners, err = b.Spend(b.state, keys, 0, b.cfg.TxFee-importedLUX, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
signers = append(baseSigners, signers...)
delete(importedAmounts, b.UTXOAssetID)
case importedLUX == b.cfg.TxFee:
delete(importedAmounts, b.UTXOAssetID)
default:
importedAmounts[b.UTXOAssetID] -= b.cfg.TxFee
}
for assetID, amount := range importedAmounts {
outs = append(outs, &lux.TransferableOutput{
Asset: lux.Asset{ID: assetID},
Out: &secp256k1fx.TransferOutput{
Amt: amount,
OutputOwners: secp256k1fx.OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{to},
},
},
})
}
lux.SortTransferableOutputs(outs) // sort imported outputs
// Create the transaction
utx := &txs.ImportTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Outs: outs,
Ins: ins,
}},
SourceChain: from,
ImportedInputs: importedInputs,
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewExportTx(
amount uint64,
chainID ids.ID,
to ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
toBurn, err := math.Add(amount, b.cfg.TxFee)
if err != nil {
return nil, fmt.Errorf("amount (%d) + tx fee(%d) overflows", amount, b.cfg.TxFee)
}
ins, outs, _, signers, err := b.Spend(b.state, keys, 0, toBurn, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
// Create the transaction
utx := &txs.ExportTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs, // Non-exported outputs
}},
DestinationChain: chainID,
ExportedOutputs: []*lux.TransferableOutput{{ // Exported to X-Chain
Asset: lux.Asset{ID: b.UTXOAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: amount,
OutputOwners: secp256k1fx.OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{to},
},
},
}},
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewCreateChainTx(
netID ids.ID,
genesisData []byte,
vmID ids.ID,
fxIDs []ids.ID,
chainName string,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
createChainTxFee := b.cfg.CreateChainTxFee
ins, outs, _, signers, err := b.Spend(b.state, keys, 0, createChainTxFee, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
chainAuth, chainSigners, err := b.Authorize(b.state, netID, keys)
if err != nil {
return nil, fmt.Errorf("couldn't authorize tx's net restrictions: %w", err)
}
signers = append(signers, chainSigners)
// Sort the provided fxIDs
sortByCompare(fxIDs)
// Create the tx
utx := &txs.CreateChainTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
}},
ValidateNetworkID: netID,
BlockchainName: chainName,
VMID: vmID,
FxIDs: fxIDs,
GenesisData: genesisData,
ChainAuth: chainAuth,
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewCreateNetworkTx(
threshold uint32,
ownerAddrs []ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
createNetworkTxFee := b.cfg.CreateNetworkTxFee
ins, outs, _, signers, err := b.Spend(b.state, keys, 0, createNetworkTxFee, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
// Sort control addresses
sortByCompare(ownerAddrs)
// Create the tx
utx := &txs.CreateNetworkTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
}},
Owner: &secp256k1fx.OutputOwners{
Threshold: threshold,
Addrs: ownerAddrs,
},
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewAddValidatorTx(
stakeAmount,
startTime,
endTime uint64,
nodeID ids.NodeID,
rewardAddress ids.ShortID,
shares uint32,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
ins, unstakedOuts, stakedOuts, signers, err := b.Spend(b.state, keys, stakeAmount, b.cfg.AddNetworkValidatorFee, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
// Create the tx
utx := &txs.AddValidatorTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: unstakedOuts,
}},
Validator: txs.Validator{
NodeID: nodeID,
Start: startTime,
End: endTime,
Wght: stakeAmount,
},
StakeOuts: stakedOuts,
RewardsOwner: &secp256k1fx.OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{rewardAddress},
},
DelegationShares: shares,
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewAddDelegatorTx(
stakeAmount,
startTime,
endTime uint64,
nodeID ids.NodeID,
rewardAddress ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
ins, unlockedOuts, lockedOuts, signers, err := b.Spend(b.state, keys, stakeAmount, b.cfg.AddNetworkDelegatorFee, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
// Create the tx
utx := &txs.AddDelegatorTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: unlockedOuts,
}},
Validator: txs.Validator{
NodeID: nodeID,
Start: startTime,
End: endTime,
Wght: stakeAmount,
},
StakeOuts: lockedOuts,
DelegationRewardsOwner: &secp256k1fx.OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{rewardAddress},
},
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewAddChainValidatorTx(
weight,
startTime,
endTime uint64,
nodeID ids.NodeID,
netID ids.ID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
chainAuth, chainSigners, err := b.Authorize(b.state, netID, keys)
if err != nil {
return nil, fmt.Errorf("couldn't authorize tx's net restrictions: %w", err)
}
signers = append(signers, chainSigners)
// Create the tx
utx := &txs.AddChainValidatorTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
}},
ChainValidator: txs.ChainValidator{
Validator: txs.Validator{
NodeID: nodeID,
Start: startTime,
End: endTime,
Wght: weight,
},
Chain: netID,
},
ChainAuth: chainAuth,
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewRemoveChainValidatorTx(
nodeID ids.NodeID,
netID ids.ID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
chainAuth, chainSigners, err := b.Authorize(b.state, netID, keys)
if err != nil {
return nil, fmt.Errorf("couldn't authorize tx's net restrictions: %w", err)
}
signers = append(signers, chainSigners)
// Create the tx
utx := &txs.RemoveChainValidatorTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
}},
Chain: netID,
NodeID: nodeID,
ChainAuth: chainAuth,
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewAdvanceTimeTx(timestamp time.Time) (*txs.Tx, error) {
utx := &txs.AdvanceTimeTx{Time: uint64(timestamp.Unix())}
tx, err := txs.NewSigned(utx, nil)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewRewardValidatorTx(txID ids.ID) (*txs.Tx, error) {
utx := &txs.RewardValidatorTx{TxID: txID}
tx, err := txs.NewSigned(utx, nil)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewTransferChainOwnershipTx(
netID ids.ID,
threshold uint32,
ownerAddrs []ids.ShortID,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
chainAuth, chainSigners, err := b.Authorize(b.state, netID, keys)
if err != nil {
return nil, fmt.Errorf("couldn't authorize tx's net restrictions: %w", err)
}
signers = append(signers, chainSigners)
utx := &txs.TransferChainOwnershipTx{
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
}},
Chain: netID,
ChainAuth: chainAuth,
Owner: &secp256k1fx.OutputOwners{
Threshold: threshold,
Addrs: ownerAddrs,
},
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}
func (b *builder) NewBaseTx(
amount uint64,
owner secp256k1fx.OutputOwners,
keys []*secp256k1.PrivateKey,
changeAddr ids.ShortID,
) (*txs.Tx, error) {
toBurn, err := math.Add(amount, b.cfg.TxFee)
if err != nil {
return nil, fmt.Errorf("amount (%d) + tx fee(%d) overflows", amount, b.cfg.TxFee)
}
ins, outs, _, signers, err := b.Spend(b.state, keys, 0, toBurn, changeAddr)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
outs = append(outs, &lux.TransferableOutput{
Asset: lux.Asset{ID: b.UTXOAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: amount,
OutputOwners: owner,
},
})
lux.SortTransferableOutputs(outs)
utx := &txs.BaseTx{
BaseTx: lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
},
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}