Files
node/vms/platformvm/txs/builder/builder.go
T
zeekayandHanzo Dev c44cc2289f platformvm builds GREEN off the codec: executor fold + full consumer flip
vms/platformvm/... build + vet PASS with pcodecs/txs.Codec gone from the VM.

Executor semantics (standard_tx_executor):
- registerOwnSet(): shared primitive — per-validator state.L1Validator with
  native owner blobs (txs.MarshalOwner/UnmarshalOwner, no codec), active-set
  capacity check, EndAccumulatedFee=balance+accruedFees, SetNetToL1Conversion
  manager-authority recording (byte-for-byte legacy tail).
- ConvertNetworkTx: promote endomorphism (owner-authorized, folds old
  ConvertNetworkToL1Tx), gated by security.Mode.Manager.
- CreateNetworkTx: base (AddNet/SetNetOwner) + sovereign path registers own set.
- Deleted CreateSovereignL1Tx + ConvertNetworkToL1Tx.
- txs.UnmarshalOwner added: canonical owner marshal/unmarshal pair, no codec.

All ~13 txs.Visitor impls carry ConvertNetworkTx; gossip/block Parse(b) codec-free;
wallet/network/primary off txs.Codec.

KNOWN GAP (pending design decision, NOT silently green): CreateNetworkTx reads
tx.Chains() only to derive managerChainID — it does NOT create the genesis
chains (no AddChain). Atomic-spawn-with-chains vs decomplect-to-CreateChainTx is
the open call. 17 codec-era _test.go parked as .bak for follow-up rewrite.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 14:25:08 -07:00

755 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"
"time"
"github.com/luxfi/runtime"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/platformvm/config"
"github.com/luxfi/node/vms/platformvm/security"
"github.com/luxfi/node/vms/platformvm/state"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/vms/platformvm/utxo"
"github.com/luxfi/timer/mockable"
"github.com/luxfi/utils"
"github.com/luxfi/math"
"github.com/luxfi/node/vms/platformvm/fx"
"github.com/luxfi/utxo/secp256k1fx"
)
// 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)
}
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, err := txs.NewImportTx(
&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Outs: outs,
Ins: ins,
},
from,
importedInputs,
)
if err != nil {
return nil, err
}
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, err := txs.NewExportTx(
&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs, // Non-exported outputs
},
chainID,
[]*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},
},
},
}},
)
if err != nil {
return nil, err
}
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) {
createBlockchainTxFee := b.cfg.CreateChainTxFee
ins, outs, _, signers, err := b.Spend(b.state, keys, 0, createBlockchainTxFee, 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
utils.Sort(fxIDs)
// Create the tx
utx, err := txs.NewCreateChainTx(
&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
},
netID,
chainName,
vmID,
fxIDs,
genesisData,
chainAuth,
)
if err != nil {
return nil, err
}
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
utils.Sort(ownerAddrs)
// Create the tx — a classic permissioned network: it restakes its parent
// (the primary network) and runs no own validator set. Members are added
// later via AddChainValidatorTx.
utx, err := txs.NewCreateNetworkTx(
&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
},
constants.PrimaryNetworkID,
&secp256k1fx.OutputOwners{
Threshold: threshold,
Addrs: ownerAddrs,
},
security.Mode{RestakeParent: true, Admission: security.NoOwnSet, Manager: security.PChain},
nil, // validators (none: no own set)
nil, // chains (none at genesis)
0, // managerChainIdx (unused without a contract-governed own set)
nil, // managerAddress
)
if err != nil {
return nil, err
}
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, err := txs.NewAddValidatorTx(
&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: unstakedOuts,
},
txs.Validator{
NodeID: nodeID,
Start: startTime,
End: endTime,
Wght: stakeAmount,
},
stakedOuts,
&secp256k1fx.OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{rewardAddress},
},
shares,
)
if err != nil {
return nil, err
}
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, err := txs.NewAddDelegatorTx(
&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: unlockedOuts,
},
txs.Validator{
NodeID: nodeID,
Start: startTime,
End: endTime,
Wght: stakeAmount,
},
lockedOuts,
&secp256k1fx.OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{rewardAddress},
},
)
if err != nil {
return nil, err
}
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, err := txs.NewAddChainValidatorTx(
&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
},
txs.Validator{
NodeID: nodeID,
Start: startTime,
End: endTime,
Wght: weight,
},
netID,
chainAuth,
)
if err != nil {
return nil, err
}
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, err := txs.NewRemoveChainValidatorTx(
&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
},
nodeID,
netID,
chainAuth,
)
if err != nil {
return nil, err
}
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.NewAdvanceTimeTx(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.NewRewardValidatorTx(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, err := txs.NewTransferChainOwnershipTx(
&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
},
netID,
chainAuth,
&secp256k1fx.OutputOwners{
Threshold: threshold,
Addrs: ownerAddrs,
},
)
if err != nil {
return nil, err
}
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, err := txs.NewBaseTx(&lux.BaseTx{
NetworkID: b.NetworkID,
BlockchainID: b.ChainID,
Ins: ins,
Outs: outs,
})
if err != nil {
return nil, err
}
tx, err := txs.NewSigned(utx, signers)
if err != nil {
return nil, err
}
return tx, tx.SyntacticVerify(b.rt)
}