diff --git a/vms/platformvm/txs/add_chain_validator_tx.go b/vms/platformvm/txs/add_chain_validator_tx.go index f8413e167..2fc75c00f 100644 --- a/vms/platformvm/txs/add_chain_validator_tx.go +++ b/vms/platformvm/txs/add_chain_validator_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. // // File deprecation notice: this file defines AddChainValidatorTx, the @@ -6,14 +6,10 @@ // (sovereign-L1), validators join a network — never a chain — via // AddValidatorTx. Chains live on networks (created via CreateChainTx); // the canonical model has validators validate networks, not chains. A -// sovereign L1 IS a primary network at its own networkID; the Lux -// primary networks live at 1/2/3/1337, and any downstream consumer -// running its own primary picks any other uint32. AddValidatorTx is -// the universal add-validator-to-a-network entry point for all of -// them. New code must use AddValidatorTx. The wire codec entries, -// Visitor method, and wallet IssueAddChainValidatorTx helper are kept -// for one release cycle so existing pre-LP-018 P-chain history -// continues to decode and replay. +// sovereign L1 IS a primary network at its own networkID. AddValidatorTx +// is the universal add-validator-to-a-network entry point. New code must +// use AddValidatorTx. This type is retained for one release cycle so +// existing pre-LP-018 P-chain history continues to decode and replay. package txs @@ -21,75 +17,93 @@ import ( "context" "errors" - "github.com/luxfi/runtime" "github.com/luxfi/constants" - "github.com/luxfi/crypto/bls" "github.com/luxfi/ids" "github.com/luxfi/node/vms/components/verify" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( - _ StakerTx = (*AddChainValidatorTx)(nil) - _ ScheduledStaker = (*AddChainValidatorTx)(nil) + _ UnsignedTx = (*AddChainValidatorTx)(nil) errAddPrimaryNetworkValidator = errors.New("can't add primary network validator with AddChainValidatorTx") ) -// AddChainValidatorTx is the legacy per-chain (pre-LP-018: per-L1) -// validator registration tx. +// AddChainValidatorTx is the legacy per-chain validator registration tx. +// The struct IS the wire: it holds the zap buffer and reads its fields by +// offset. No codec, no marshal. // -// Deprecated: Use AddValidatorTx. Under LP-018 sovereign-L1, validators -// join a network (Lux primary 1/2/3/1337 or any sovereign L1's own -// primary at its EVM chainID). Chains live on networks; validators no -// longer register per-chain. This type is retained for one release -// cycle for wire/codec compat with pre-LP-018 binaries. The Visitor -// method, codec slots, and wallet IssueAddChainValidatorTx helper are -// preserved so older P-chain history still decodes and replays. +// Wire: zap header + object{ envelope@0..76, Validator@77, Chain@121, +// ChainAuth@153 }. +// +// Deprecated: Use AddValidatorTx. Retained for one release cycle for +// wire/codec compat with pre-LP-018 binaries. type AddChainValidatorTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // The validator - ChainValidator `serialize:"true" json:"validator"` - // Auth that will be allowing this validator into the network - ChainAuth verify.Verifiable `serialize:"true" json:"chainAuthorization"` + spendingTx } -func (tx *AddChainValidatorTx) NodeID() ids.NodeID { - return tx.ChainValidator.NodeID +const ( + offACVValidator = spendSize // 77: inline Validator (44B) + offACVChain = offACVValidator + validatorSize // 121: chain id (32B) + offACVChainAuth = offACVChain + 32 // 153: chain-auth list ptr (8B) + addChainValidatorSize = offACVChainAuth + 8 // 161 +) + +// NewAddChainValidatorTx builds the tx into a fresh zap buffer — the one +// place a field becomes bytes (construction, not serialization). +func NewAddChainValidatorTx(base *lux.BaseTx, validator Validator, chain ids.ID, chainAuth verify.Verifiable) (*AddChainValidatorTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 1024 + addChainValidatorSize) + p, err := writeSpending(b, base) + if err != nil { + return nil, err + } + authOff, authCount, err := writeAuth(b, chainAuth) + if err != nil { + return nil, err + } + + ob := b.StartObject(addChainValidatorSize) + setEnvelope(ob, kindAddChainValidator, base, p) + setValidator(ob, offACVValidator, validator) + setID(ob, offACVChain, chain) + ob.SetList(offACVChainAuth, authOff, authCount) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &AddChainValidatorTx{spendingTx{msg}}, nil } -func (*AddChainValidatorTx) PublicKey() (*bls.PublicKey, bool, error) { - return nil, false, nil +// Validator is the inline validator descriptor (offset read). +func (tx *AddChainValidatorTx) Validator() Validator { + return readValidator(tx.root(), offACVValidator) } -func (*AddChainValidatorTx) PendingPriority() Priority { - return ChainPermissionedValidatorPendingPriority +// Chain is the network id this validator registers under (offset read). +func (tx *AddChainValidatorTx) Chain() ids.ID { return readID(tx.root(), offACVChain) } + +// ChainAuth proves the issuer may add this validator to the network. +func (tx *AddChainValidatorTx) ChainAuth() verify.Verifiable { + return readAuth(tx.root(), offACVChainAuth) } -func (*AddChainValidatorTx) CurrentPriority() Priority { - return ChainPermissionedValidatorCurrentPriority -} - -// SyntacticVerify returns nil iff [tx] is valid +// SyntacticVerify returns nil iff [tx] is valid. func (tx *AddChainValidatorTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil - case tx.Chain == constants.PrimaryNetworkID: + } + if tx.Chain() == constants.PrimaryNetworkID { return errAddPrimaryNetworkValidator } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - if err := verify.All(&tx.Validator, tx.ChainAuth); err != nil { + v := tx.Validator() + if err := verify.All(&v, tx.ChainAuth()); err != nil { return err } - - // cache that this is valid - tx.SyntacticallyVerified = true return nil } @@ -97,8 +111,7 @@ func (tx *AddChainValidatorTx) Visit(visitor Visitor) error { return visitor.AddChainValidatorTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime +// Initialize is a no-op; Runtime is passed explicitly to InitRuntime. func (tx *AddChainValidatorTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here return nil } diff --git a/vms/platformvm/txs/add_delegator_tx.go b/vms/platformvm/txs/add_delegator_tx.go index 6678f20a8..af52aaa7f 100644 --- a/vms/platformvm/txs/add_delegator_tx.go +++ b/vms/platformvm/txs/add_delegator_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -8,95 +8,98 @@ import ( "errors" "fmt" - "github.com/luxfi/runtime" - "github.com/luxfi/constants" - "github.com/luxfi/crypto/bls" - "github.com/luxfi/ids" safemath "github.com/luxfi/math" - lux "github.com/luxfi/utxo" "github.com/luxfi/node/vms/components/verify" "github.com/luxfi/node/vms/platformvm/fx" - "github.com/luxfi/utxo/secp256k1fx" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( - _ DelegatorTx = (*AddDelegatorTx)(nil) - _ ScheduledStaker = (*AddDelegatorTx)(nil) + _ UnsignedTx = (*AddDelegatorTx)(nil) errDelegatorWeightMismatch = errors.New("delegator weight is not equal to total stake weight") errStakeMustBeLUX = errors.New("stake must be LUX") ) -// AddDelegatorTx is an unsigned addDelegatorTx +// AddDelegatorTx is an unsigned addDelegatorTx. The struct IS the wire: it +// holds the zap buffer and reads its fields by offset. No codec, no marshal. +// +// Wire: zap header + object{ envelope@0..76, Validator@77, StakeOuts@121, +// DelegationRewardsOwner@137 }. type AddDelegatorTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // Describes the delegatee - Validator `serialize:"true" json:"validator"` - // Where to send staked tokens when done validating - StakeOuts []*lux.TransferableOutput `serialize:"true" json:"stake"` - // Where to send staking rewards when done validating - DelegationRewardsOwner fx.Owner `serialize:"true" json:"rewardsOwner"` + spendingTx } -// InitRuntime sets the FxID fields in the inputs and outputs of this -// [UnsignedAddDelegatorTx]. Also sets the [rt] to the given [vm.rt] so that -// the addresses can be json marshalled into human readable format -func (tx *AddDelegatorTx) InitRuntime(rt *runtime.Runtime) { - tx.BaseTx.InitRuntime(rt) - for _, out := range tx.StakeOuts { - out.FxID = secp256k1fx.ID - out.InitRuntime(rt) +const ( + offADValidator = spendSize // 77: inline Validator (44B) + offADStakeOuts = offADValidator + validatorSize // 121: stake-outs list ptr (8B) + offADStakeAddrs = offADStakeOuts + 8 // 129: stake-outs owner-addr array ptr (8B) + offADRewardsThreshold = offADStakeAddrs + 8 // 137: rewards owner threshold (u32) + offADRewardsLocktime = offADRewardsThreshold + 4 // 141: rewards owner locktime (u64) + offADRewardsAddrs = offADRewardsLocktime + 8 // 149: rewards owner addr array ptr (8B) + addDelegatorSize = offADRewardsAddrs + 8 // 157 +) + +// NewAddDelegatorTx builds the tx into a fresh zap buffer. +func NewAddDelegatorTx(base *lux.BaseTx, validator Validator, stakeOuts []*lux.TransferableOutput, delegationRewardsOwner fx.Owner) (*AddDelegatorTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 1024 + addDelegatorSize) + p, err := writeSpending(b, base) + if err != nil { + return nil, err } - // Owner doesn't have InitRuntime method + stakeListOff, stakeListCount, stakeAddrOff, stakeAddrCount, err := writeExtraOuts(b, stakeOuts) + if err != nil { + return nil, err + } + ownerThreshold, ownerLocktime, ownerAddrOff, ownerAddrCount, err := writeOwner(b, delegationRewardsOwner) + if err != nil { + return nil, err + } + + ob := b.StartObject(addDelegatorSize) + setEnvelope(ob, kindAddDelegator, base, p) + setValidator(ob, offADValidator, validator) + ob.SetList(offADStakeOuts, stakeListOff, stakeListCount) + ob.SetList(offADStakeAddrs, stakeAddrOff, stakeAddrCount) + setOwner(ob, offADRewardsThreshold, offADRewardsLocktime, offADRewardsAddrs, ownerThreshold, ownerLocktime, ownerAddrOff, ownerAddrCount) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &AddDelegatorTx{spendingTx{msg}}, nil } -func (*AddDelegatorTx) ChainID() ids.ID { - return constants.PrimaryNetworkID +// Validator describes the delegatee (offset read). +func (tx *AddDelegatorTx) Validator() Validator { return readValidator(tx.root(), offADValidator) } + +// StakeOuts is where staked tokens go when done validating (offset read). +func (tx *AddDelegatorTx) StakeOuts() []*lux.TransferableOutput { + return readExtraOuts(tx.root(), offADStakeOuts, offADStakeAddrs) } -func (tx *AddDelegatorTx) NodeID() ids.NodeID { - return tx.Validator.NodeID +// DelegationRewardsOwner is where staking rewards go when done validating. +func (tx *AddDelegatorTx) DelegationRewardsOwner() fx.Owner { + return readOwner(tx.root(), offADRewardsThreshold, offADRewardsLocktime, offADRewardsAddrs) } -func (*AddDelegatorTx) PublicKey() (*bls.PublicKey, bool, error) { - return nil, false, nil -} - -func (*AddDelegatorTx) PendingPriority() Priority { - return PrimaryNetworkDelegatorLegacyPendingPriority -} - -func (*AddDelegatorTx) CurrentPriority() Priority { - return PrimaryNetworkDelegatorCurrentPriority -} - -func (tx *AddDelegatorTx) Stake() []*lux.TransferableOutput { - return tx.StakeOuts -} - -func (tx *AddDelegatorTx) RewardsOwner() fx.Owner { - return tx.DelegationRewardsOwner -} - -// SyntacticVerify returns nil iff [tx] is valid +// SyntacticVerify returns nil iff [tx] is valid. func (tx *AddDelegatorTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - if err := verify.All(&tx.Validator, tx.DelegationRewardsOwner); err != nil { + v := tx.Validator() + if err := verify.All(&v, tx.DelegationRewardsOwner()); err != nil { return fmt.Errorf("failed to verify validator or rewards owner: %w", err) } + stakeOuts := tx.StakeOuts() totalStakeWeight := uint64(0) - for _, out := range tx.StakeOuts { + for _, out := range stakeOuts { if err := out.Verify(); err != nil { return fmt.Errorf("output verification failed: %w", err) } @@ -114,18 +117,15 @@ func (tx *AddDelegatorTx) SyntacticVerify(rt *runtime.Runtime) error { } switch { - case !lux.IsSortedTransferableOutputs(tx.StakeOuts): + case !lux.IsSortedTransferableOutputs(stakeOuts): return errOutputsNotSorted - case totalStakeWeight != tx.Wght: + case totalStakeWeight != v.Wght: return fmt.Errorf("%w, delegator weight %d total stake weight %d", errDelegatorWeightMismatch, - tx.Wght, + v.Wght, totalStakeWeight, ) } - - // cache that this is valid - tx.SyntacticallyVerified = true return nil } @@ -133,8 +133,7 @@ func (tx *AddDelegatorTx) Visit(visitor Visitor) error { return visitor.AddDelegatorTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime +// Initialize is a no-op; Runtime is passed explicitly to InitRuntime. func (tx *AddDelegatorTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here return nil } diff --git a/vms/platformvm/txs/add_permissionless_delegator_tx.go b/vms/platformvm/txs/add_permissionless_delegator_tx.go index 2b94d1613..c888f75dd 100644 --- a/vms/platformvm/txs/add_permissionless_delegator_tx.go +++ b/vms/platformvm/txs/add_permissionless_delegator_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -7,110 +7,113 @@ import ( "context" "fmt" - "github.com/luxfi/runtime" - "github.com/luxfi/constants" - "github.com/luxfi/crypto/bls" "github.com/luxfi/ids" safemath "github.com/luxfi/math" - lux "github.com/luxfi/utxo" "github.com/luxfi/node/vms/components/verify" "github.com/luxfi/node/vms/platformvm/fx" - "github.com/luxfi/utxo/secp256k1fx" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) -var ( - _ DelegatorTx = (*AddPermissionlessDelegatorTx)(nil) - _ ScheduledStaker = (*AddPermissionlessDelegatorTx)(nil) -) +var _ UnsignedTx = (*AddPermissionlessDelegatorTx)(nil) -// AddPermissionlessDelegatorTx is an unsigned addPermissionlessDelegatorTx +// AddPermissionlessDelegatorTx is an unsigned addPermissionlessDelegatorTx. +// The struct IS the wire: it holds the zap buffer and reads its fields by +// offset. No codec, no marshal. +// +// Wire: zap header + object{ envelope@0..76, Validator@77, Chain@121, +// StakeOuts@153, DelegationRewardsOwner@169 }. type AddPermissionlessDelegatorTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // Describes the validator - Validator `serialize:"true" json:"validator"` - // ID of the chain this validator is validating - Chain ids.ID `serialize:"true" json:"chainID"` - // Where to send staked tokens when done validating - StakeOuts []*lux.TransferableOutput `serialize:"true" json:"stake"` - // Where to send staking rewards when done validating - DelegationRewardsOwner fx.Owner `serialize:"true" json:"rewardsOwner"` + spendingTx } -// InitRuntime sets the FxID fields in the inputs and outputs of this -// [AddPermissionlessDelegatorTx]. Also sets the [rt] to the given [vm.rt] so -// that the addresses can be json marshalled into human readable format -func (tx *AddPermissionlessDelegatorTx) InitRuntime(rt *runtime.Runtime) { - tx.BaseTx.InitRuntime(rt) - for _, out := range tx.StakeOuts { - out.FxID = secp256k1fx.ID - out.InitRuntime(rt) +const ( + offAPDValidator = spendSize // 77: inline Validator (44B) + offAPDChain = offAPDValidator + validatorSize // 121: chain id (32B) + offAPDStakeOuts = offAPDChain + 32 // 153: stake-outs list ptr (8B) + offAPDStakeAddrs = offAPDStakeOuts + 8 // 161: stake-outs owner-addr array ptr (8B) + offAPDRewardsThreshold = offAPDStakeAddrs + 8 // 169: rewards owner threshold (u32) + offAPDRewardsLocktime = offAPDRewardsThreshold + 4 // 173: rewards owner locktime (u64) + offAPDRewardsAddrs = offAPDRewardsLocktime + 8 // 181: rewards owner addr array ptr (8B) + addPermissionlessDelegatorSize = offAPDRewardsAddrs + 8 // 189 +) + +// NewAddPermissionlessDelegatorTx builds the tx into a fresh zap buffer. +func NewAddPermissionlessDelegatorTx(base *lux.BaseTx, validator Validator, chain ids.ID, stakeOuts []*lux.TransferableOutput, delegationRewardsOwner fx.Owner) (*AddPermissionlessDelegatorTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 1024 + addPermissionlessDelegatorSize) + p, err := writeSpending(b, base) + if err != nil { + return nil, err } - // Owner doesn't have InitRuntime method -} - -func (tx *AddPermissionlessDelegatorTx) ChainID() ids.ID { - return tx.Chain -} - -func (tx *AddPermissionlessDelegatorTx) NodeID() ids.NodeID { - return tx.Validator.NodeID -} - -func (*AddPermissionlessDelegatorTx) PublicKey() (*bls.PublicKey, bool, error) { - return nil, false, nil -} - -func (tx *AddPermissionlessDelegatorTx) PendingPriority() Priority { - if tx.Chain == constants.PrimaryNetworkID { - return PrimaryNetworkDelegatorPermissionlessPendingPriority + stakeListOff, stakeListCount, stakeAddrOff, stakeAddrCount, err := writeExtraOuts(b, stakeOuts) + if err != nil { + return nil, err } - return ChainPermissionlessDelegatorPendingPriority -} - -func (tx *AddPermissionlessDelegatorTx) CurrentPriority() Priority { - if tx.Chain == constants.PrimaryNetworkID { - return PrimaryNetworkDelegatorCurrentPriority + ownerThreshold, ownerLocktime, ownerAddrOff, ownerAddrCount, err := writeOwner(b, delegationRewardsOwner) + if err != nil { + return nil, err } - return ChainPermissionlessDelegatorCurrentPriority + + ob := b.StartObject(addPermissionlessDelegatorSize) + setEnvelope(ob, kindAddPermissionlessDelegator, base, p) + setValidator(ob, offAPDValidator, validator) + setID(ob, offAPDChain, chain) + ob.SetList(offAPDStakeOuts, stakeListOff, stakeListCount) + ob.SetList(offAPDStakeAddrs, stakeAddrOff, stakeAddrCount) + setOwner(ob, offAPDRewardsThreshold, offAPDRewardsLocktime, offAPDRewardsAddrs, ownerThreshold, ownerLocktime, ownerAddrOff, ownerAddrCount) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &AddPermissionlessDelegatorTx{spendingTx{msg}}, nil } -func (tx *AddPermissionlessDelegatorTx) Stake() []*lux.TransferableOutput { - return tx.StakeOuts +// Validator describes the validator being delegated to (offset read). +func (tx *AddPermissionlessDelegatorTx) Validator() Validator { + return readValidator(tx.root(), offAPDValidator) } -func (tx *AddPermissionlessDelegatorTx) RewardsOwner() fx.Owner { - return tx.DelegationRewardsOwner +// Chain is the id of the chain this validator is validating (offset read). +func (tx *AddPermissionlessDelegatorTx) Chain() ids.ID { return readID(tx.root(), offAPDChain) } + +// StakeOuts is where staked tokens go when done validating (offset read). +func (tx *AddPermissionlessDelegatorTx) StakeOuts() []*lux.TransferableOutput { + return readExtraOuts(tx.root(), offAPDStakeOuts, offAPDStakeAddrs) } -// SyntacticVerify returns nil iff [tx] is valid +// DelegationRewardsOwner is where staking rewards go when done validating. +func (tx *AddPermissionlessDelegatorTx) DelegationRewardsOwner() fx.Owner { + return readOwner(tx.root(), offAPDRewardsThreshold, offAPDRewardsLocktime, offAPDRewardsAddrs) +} + +// SyntacticVerify returns nil iff [tx] is valid. func (tx *AddPermissionlessDelegatorTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil - case len(tx.StakeOuts) == 0: // Ensure there is provided stake + } + stakeOuts := tx.StakeOuts() + if len(stakeOuts) == 0 { // Ensure there is provided stake return errNoStake } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return fmt.Errorf("failed to verify BaseTx: %w", err) } - if err := verify.All(&tx.Validator, tx.DelegationRewardsOwner); err != nil { + v := tx.Validator() + if err := verify.All(&v, tx.DelegationRewardsOwner()); err != nil { return fmt.Errorf("failed to verify validator or rewards owner: %w", err) } - for _, out := range tx.StakeOuts { + for _, out := range stakeOuts { if err := out.Verify(); err != nil { return fmt.Errorf("failed to verify output: %w", err) } } - firstStakeOutput := tx.StakeOuts[0] + firstStakeOutput := stakeOuts[0] stakedAssetID := firstStakeOutput.AssetID() totalStakeWeight := firstStakeOutput.Output().Amount() - for _, out := range tx.StakeOuts[1:] { + for _, out := range stakeOuts[1:] { newWeight, err := safemath.Add(totalStakeWeight, out.Output().Amount()) if err != nil { return err @@ -124,18 +127,15 @@ func (tx *AddPermissionlessDelegatorTx) SyntacticVerify(rt *runtime.Runtime) err } switch { - case !lux.IsSortedTransferableOutputs(tx.StakeOuts): + case !lux.IsSortedTransferableOutputs(stakeOuts): return errOutputsNotSorted - case totalStakeWeight != tx.Wght: + case totalStakeWeight != v.Wght: return fmt.Errorf("%w, delegator weight %d total stake weight %d", errDelegatorWeightMismatch, - tx.Wght, + v.Wght, totalStakeWeight, ) } - - // cache that this is valid - tx.SyntacticallyVerified = true return nil } @@ -143,8 +143,7 @@ func (tx *AddPermissionlessDelegatorTx) Visit(visitor Visitor) error { return visitor.AddPermissionlessDelegatorTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime +// Initialize is a no-op; Runtime is passed explicitly to InitRuntime. func (tx *AddPermissionlessDelegatorTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here return nil } diff --git a/vms/platformvm/txs/add_permissionless_validator_tx.go b/vms/platformvm/txs/add_permissionless_validator_tx.go index 50ad4e460..30d707bdb 100644 --- a/vms/platformvm/txs/add_permissionless_validator_tx.go +++ b/vms/platformvm/txs/add_permissionless_validator_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -8,22 +8,20 @@ import ( "errors" "fmt" - "github.com/luxfi/runtime" "github.com/luxfi/constants" - "github.com/luxfi/crypto/bls" "github.com/luxfi/ids" safemath "github.com/luxfi/math" - lux "github.com/luxfi/utxo" "github.com/luxfi/node/vms/components/verify" "github.com/luxfi/node/vms/platformvm/fx" "github.com/luxfi/node/vms/platformvm/reward" "github.com/luxfi/node/vms/platformvm/signer" - "github.com/luxfi/utxo/secp256k1fx" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( - _ ValidatorTx = (*AddPermissionlessValidatorTx)(nil) - _ ScheduledStaker = (*AddPermissionlessDelegatorTx)(nil) + _ UnsignedTx = (*AddPermissionlessValidatorTx)(nil) errEmptyNodeID = errors.New("validator nodeID cannot be empty") errNoStake = errors.New("no stake") @@ -32,117 +30,131 @@ var ( errValidatorWeightMismatch = errors.New("validator weight mismatch") ) -// AddPermissionlessValidatorTx is an unsigned addPermissionlessValidatorTx +// AddPermissionlessValidatorTx is an unsigned addPermissionlessValidatorTx. +// The struct IS the wire: it holds the zap buffer and reads its fields by +// offset. No codec, no marshal. +// +// Wire: zap header + object{ envelope@0..76, Validator@77, Chain@121, +// Signer@153, StakeOuts@298, ValidatorRewardsOwner@314, +// DelegatorRewardsOwner@334, DelegationShares@354 }. type AddPermissionlessValidatorTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // Describes the validator - Validator `serialize:"true" json:"validator"` - // ID of the chain this validator is validating - Chain ids.ID `serialize:"true" json:"chainID"` - // If the [Chain] is the primary network, [Signer] is the BLS key for this - // validator. If the [Chain] is not the primary network, this value is the - // empty signer - // Note: We do not enforce that the BLS key is unique across all validators. - // This means that validators can share a key if they so choose. - // However, a NodeID does uniquely map to a BLS key - Signer signer.Signer `serialize:"true" json:"signer"` - // Where to send staked tokens when done validating - StakeOuts []*lux.TransferableOutput `serialize:"true" json:"stake"` - // Where to send validation rewards when done validating - ValidatorRewardsOwner fx.Owner `serialize:"true" json:"validationRewardsOwner"` - // Where to send delegation rewards when done validating - DelegatorRewardsOwner fx.Owner `serialize:"true" json:"delegationRewardsOwner"` - // Fee this validator charges delegators as a percentage, times 10,000 - // For example, if this validator has DelegationShares=300,000 then they - // take 30% of rewards from delegators - DelegationShares uint32 `serialize:"true" json:"shares"` + spendingTx } -// InitRuntime sets the FxID fields in the inputs and outputs of this -// [AddPermissionlessValidatorTx]. Also sets the [rt] to the given [vm.rt] so -// that the addresses can be json marshalled into human readable format -func (tx *AddPermissionlessValidatorTx) InitRuntime(rt *runtime.Runtime) { - tx.BaseTx.InitRuntime(rt) - for _, out := range tx.StakeOuts { - out.FxID = secp256k1fx.ID - out.InitRuntime(rt) +const ( + offAPVValidator = spendSize // 77: inline Validator (44B) + offAPVChain = offAPVValidator + validatorSize // 121: chain id (32B) + offAPVSigner = offAPVChain + 32 // 153: inline Signer (145B) + offAPVStakeOuts = offAPVSigner + signerSize // 298: stake-outs list ptr (8B) + offAPVStakeAddrs = offAPVStakeOuts + 8 // 306: stake-outs owner-addr array ptr (8B) + offAPVValRewardsThreshold = offAPVStakeAddrs + 8 // 314: validator rewards owner threshold (u32) + offAPVValRewardsLocktime = offAPVValRewardsThreshold + 4 // 318: validator rewards owner locktime (u64) + offAPVValRewardsAddrs = offAPVValRewardsLocktime + 8 // 326: validator rewards owner addr array ptr (8B) + offAPVDelRewardsThreshold = offAPVValRewardsAddrs + 8 // 334: delegator rewards owner threshold (u32) + offAPVDelRewardsLocktime = offAPVDelRewardsThreshold + 4 // 338: delegator rewards owner locktime (u64) + offAPVDelRewardsAddrs = offAPVDelRewardsLocktime + 8 // 346: delegator rewards owner addr array ptr (8B) + offAPVDelegationShares = offAPVDelRewardsAddrs + 8 // 354: delegation shares (u32) + addPermissionlessValidatorSize = offAPVDelegationShares + 4 // 358 +) + +// NewAddPermissionlessValidatorTx builds the tx into a fresh zap buffer. +func NewAddPermissionlessValidatorTx(base *lux.BaseTx, validator Validator, chain ids.ID, sig signer.Signer, stakeOuts []*lux.TransferableOutput, validatorRewardsOwner fx.Owner, delegatorRewardsOwner fx.Owner, delegationShares uint32) (*AddPermissionlessValidatorTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 1024 + addPermissionlessValidatorSize) + p, err := writeSpending(b, base) + if err != nil { + return nil, err } - // Owner doesn't have InitRuntime method - // tx.ValidatorRewardsOwner.InitRuntime(ctx) - // tx.DelegatorRewardsOwner.InitRuntime(ctx) -} - -func (tx *AddPermissionlessValidatorTx) ChainID() ids.ID { - return tx.Chain -} - -func (tx *AddPermissionlessValidatorTx) NodeID() ids.NodeID { - return tx.Validator.NodeID -} - -func (tx *AddPermissionlessValidatorTx) PublicKey() (*bls.PublicKey, bool, error) { - if err := tx.Signer.Verify(); err != nil { - return nil, false, err + stakeListOff, stakeListCount, stakeAddrOff, stakeAddrCount, err := writeExtraOuts(b, stakeOuts) + if err != nil { + return nil, err } - key := tx.Signer.Key() - return key, key != nil, nil -} - -func (tx *AddPermissionlessValidatorTx) PendingPriority() Priority { - if tx.Chain == constants.PrimaryNetworkID { - return PrimaryNetworkValidatorPendingPriority + valThreshold, valLocktime, valAddrOff, valAddrCount, err := writeOwner(b, validatorRewardsOwner) + if err != nil { + return nil, err } - return ChainPermissionlessValidatorPendingPriority -} - -func (tx *AddPermissionlessValidatorTx) CurrentPriority() Priority { - if tx.Chain == constants.PrimaryNetworkID { - return PrimaryNetworkValidatorCurrentPriority + delThreshold, delLocktime, delAddrOff, delAddrCount, err := writeOwner(b, delegatorRewardsOwner) + if err != nil { + return nil, err } - return ChainPermissionlessValidatorCurrentPriority + + ob := b.StartObject(addPermissionlessValidatorSize) + setEnvelope(ob, kindAddPermissionlessValidator, base, p) + setValidator(ob, offAPVValidator, validator) + setID(ob, offAPVChain, chain) + if err := setSigner(ob, offAPVSigner, sig); err != nil { + return nil, err + } + ob.SetList(offAPVStakeOuts, stakeListOff, stakeListCount) + ob.SetList(offAPVStakeAddrs, stakeAddrOff, stakeAddrCount) + setOwner(ob, offAPVValRewardsThreshold, offAPVValRewardsLocktime, offAPVValRewardsAddrs, valThreshold, valLocktime, valAddrOff, valAddrCount) + setOwner(ob, offAPVDelRewardsThreshold, offAPVDelRewardsLocktime, offAPVDelRewardsAddrs, delThreshold, delLocktime, delAddrOff, delAddrCount) + ob.SetUint32(offAPVDelegationShares, delegationShares) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &AddPermissionlessValidatorTx{spendingTx{msg}}, nil } -func (tx *AddPermissionlessValidatorTx) Stake() []*lux.TransferableOutput { - return tx.StakeOuts +// Validator describes the validator (offset read). +func (tx *AddPermissionlessValidatorTx) Validator() Validator { + return readValidator(tx.root(), offAPVValidator) } -func (tx *AddPermissionlessValidatorTx) ValidationRewardsOwner() fx.Owner { - return tx.ValidatorRewardsOwner +// Chain is the id of the chain this validator is validating (offset read). +func (tx *AddPermissionlessValidatorTx) Chain() ids.ID { return readID(tx.root(), offAPVChain) } + +// Signer is the BLS key for this validator (empty signer off the primary +// network) (offset read). +func (tx *AddPermissionlessValidatorTx) Signer() signer.Signer { + return readSigner(tx.root(), offAPVSigner) } -func (tx *AddPermissionlessValidatorTx) DelegationRewardsOwner() fx.Owner { - return tx.DelegatorRewardsOwner +// StakeOuts is where staked tokens go when done validating (offset read). +func (tx *AddPermissionlessValidatorTx) StakeOuts() []*lux.TransferableOutput { + return readExtraOuts(tx.root(), offAPVStakeOuts, offAPVStakeAddrs) } -func (tx *AddPermissionlessValidatorTx) Shares() uint32 { - return tx.DelegationShares +// ValidatorRewardsOwner is where validation rewards go when done validating. +func (tx *AddPermissionlessValidatorTx) ValidatorRewardsOwner() fx.Owner { + return readOwner(tx.root(), offAPVValRewardsThreshold, offAPVValRewardsLocktime, offAPVValRewardsAddrs) } -// SyntacticVerify returns nil iff [tx] is valid +// DelegatorRewardsOwner is where delegation rewards go when done validating. +func (tx *AddPermissionlessValidatorTx) DelegatorRewardsOwner() fx.Owner { + return readOwner(tx.root(), offAPVDelRewardsThreshold, offAPVDelRewardsLocktime, offAPVDelRewardsAddrs) +} + +// DelegationShares is the fee (times 10,000) charged to delegators. +func (tx *AddPermissionlessValidatorTx) DelegationShares() uint32 { + return tx.root().Uint32(offAPVDelegationShares) +} + +// SyntacticVerify returns nil iff [tx] is valid. func (tx *AddPermissionlessValidatorTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil - case tx.Validator.NodeID == ids.EmptyNodeID: + } + v := tx.Validator() + stakeOuts := tx.StakeOuts() + switch { + case v.NodeID == ids.EmptyNodeID: return errEmptyNodeID - case len(tx.StakeOuts) == 0: // Ensure there is provided stake + case len(stakeOuts) == 0: // Ensure there is provided stake return errNoStake - case tx.DelegationShares > reward.PercentDenominator: + case tx.DelegationShares() > reward.PercentDenominator: return errTooManyShares } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return fmt.Errorf("failed to verify BaseTx: %w", err) } - if err := verify.All(&tx.Validator, tx.Signer, tx.ValidatorRewardsOwner, tx.DelegatorRewardsOwner); err != nil { + sig := tx.Signer() + if err := verify.All(&v, sig, tx.ValidatorRewardsOwner(), tx.DelegatorRewardsOwner()); err != nil { return fmt.Errorf("failed to verify validator, signer, or rewards owners: %w", err) } - hasKey := tx.Signer.Key() != nil - isPrimaryNetwork := tx.Chain == constants.PrimaryNetworkID + hasKey := sig.Key() != nil + isPrimaryNetwork := tx.Chain() == constants.PrimaryNetworkID if hasKey != isPrimaryNetwork { return fmt.Errorf( "%w: hasKey=%v != isPrimaryNetwork=%v", @@ -152,16 +164,16 @@ func (tx *AddPermissionlessValidatorTx) SyntacticVerify(rt *runtime.Runtime) err ) } - for _, out := range tx.StakeOuts { + for _, out := range stakeOuts { if err := out.Verify(); err != nil { return fmt.Errorf("failed to verify output: %w", err) } } - firstStakeOutput := tx.StakeOuts[0] + firstStakeOutput := stakeOuts[0] stakedAssetID := firstStakeOutput.AssetID() totalStakeWeight := firstStakeOutput.Output().Amount() - for _, out := range tx.StakeOuts[1:] { + for _, out := range stakeOuts[1:] { newWeight, err := safemath.Add(totalStakeWeight, out.Output().Amount()) if err != nil { return err @@ -175,14 +187,11 @@ func (tx *AddPermissionlessValidatorTx) SyntacticVerify(rt *runtime.Runtime) err } switch { - case !lux.IsSortedTransferableOutputs(tx.StakeOuts): + case !lux.IsSortedTransferableOutputs(stakeOuts): return errOutputsNotSorted - case totalStakeWeight != tx.Wght: - return fmt.Errorf("%w: weight %d != stake %d", errValidatorWeightMismatch, tx.Wght, totalStakeWeight) + case totalStakeWeight != v.Wght: + return fmt.Errorf("%w: weight %d != stake %d", errValidatorWeightMismatch, v.Wght, totalStakeWeight) } - - // cache that this is valid - tx.SyntacticallyVerified = true return nil } @@ -190,8 +199,7 @@ func (tx *AddPermissionlessValidatorTx) Visit(visitor Visitor) error { return visitor.AddPermissionlessValidatorTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime +// Initialize is a no-op; Runtime is passed explicitly to InitRuntime. func (tx *AddPermissionlessValidatorTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here return nil } diff --git a/vms/platformvm/txs/add_validator_tx.go b/vms/platformvm/txs/add_validator_tx.go index 1bfd0e7e4..1440522ac 100644 --- a/vms/platformvm/txs/add_validator_tx.go +++ b/vms/platformvm/txs/add_validator_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -7,109 +7,106 @@ import ( "context" "fmt" - "github.com/luxfi/runtime" - "github.com/luxfi/constants" - "github.com/luxfi/crypto/bls" - "github.com/luxfi/ids" safemath "github.com/luxfi/math" - lux "github.com/luxfi/utxo" "github.com/luxfi/node/vms/components/verify" "github.com/luxfi/node/vms/platformvm/fx" "github.com/luxfi/node/vms/platformvm/reward" - "github.com/luxfi/utxo/secp256k1fx" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( - _ ValidatorTx = (*AddValidatorTx)(nil) - _ ScheduledStaker = (*AddValidatorTx)(nil) + _ UnsignedTx = (*AddValidatorTx)(nil) errTooManyShares = fmt.Errorf("a staker can only require at most %d shares from delegators", reward.PercentDenominator) ) -// AddValidatorTx is an unsigned addValidatorTx +// AddValidatorTx is an unsigned addValidatorTx. The struct IS the wire: it +// holds the zap buffer and reads its fields by offset. No codec, no marshal. +// +// Wire: zap header + object{ envelope@0..76, Validator@77, StakeOuts@121, +// RewardsOwner@137, DelegationShares@157 }. type AddValidatorTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // Describes the delegatee - Validator `serialize:"true" json:"validator"` - // Where to send staked tokens when done validating - StakeOuts []*lux.TransferableOutput `serialize:"true" json:"stake"` - // Where to send staking rewards when done validating - RewardsOwner fx.Owner `serialize:"true" json:"rewardsOwner"` - // Fee this validator charges delegators as a percentage, times 10,000 - // For example, if this validator has DelegationShares=300,000 then they - // take 30% of rewards from delegators - DelegationShares uint32 `serialize:"true" json:"shares"` + spendingTx } -// InitRuntime sets the FxID fields in the inputs and outputs of this -// [AddValidatorTx]. Also sets the [rt] to the given [vm.rt] so that -// the addresses can be json marshalled into human readable format -func (tx *AddValidatorTx) InitRuntime(rt *runtime.Runtime) { - tx.BaseTx.InitRuntime(rt) - for _, out := range tx.StakeOuts { - out.FxID = secp256k1fx.ID - out.InitRuntime(rt) +const ( + offAVValidator = spendSize // 77: inline Validator (44B) + offAVStakeOuts = offAVValidator + validatorSize // 121: stake-outs list ptr (8B) + offAVStakeAddrs = offAVStakeOuts + 8 // 129: stake-outs owner-addr array ptr (8B) + offAVRewardsThreshold = offAVStakeAddrs + 8 // 137: rewards owner threshold (u32) + offAVRewardsLocktime = offAVRewardsThreshold + 4 // 141: rewards owner locktime (u64) + offAVRewardsAddrs = offAVRewardsLocktime + 8 // 149: rewards owner addr array ptr (8B) + offAVDelegationShares = offAVRewardsAddrs + 8 // 157: delegation shares (u32) + addValidatorSize = offAVDelegationShares + 4 // 161 +) + +// NewAddValidatorTx builds the tx into a fresh zap buffer. +func NewAddValidatorTx(base *lux.BaseTx, validator Validator, stakeOuts []*lux.TransferableOutput, rewardsOwner fx.Owner, delegationShares uint32) (*AddValidatorTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 1024 + addValidatorSize) + p, err := writeSpending(b, base) + if err != nil { + return nil, err } - // Owner doesn't have InitRuntime method + stakeListOff, stakeListCount, stakeAddrOff, stakeAddrCount, err := writeExtraOuts(b, stakeOuts) + if err != nil { + return nil, err + } + ownerThreshold, ownerLocktime, ownerAddrOff, ownerAddrCount, err := writeOwner(b, rewardsOwner) + if err != nil { + return nil, err + } + + ob := b.StartObject(addValidatorSize) + setEnvelope(ob, kindAddValidator, base, p) + setValidator(ob, offAVValidator, validator) + ob.SetList(offAVStakeOuts, stakeListOff, stakeListCount) + ob.SetList(offAVStakeAddrs, stakeAddrOff, stakeAddrCount) + setOwner(ob, offAVRewardsThreshold, offAVRewardsLocktime, offAVRewardsAddrs, ownerThreshold, ownerLocktime, ownerAddrOff, ownerAddrCount) + ob.SetUint32(offAVDelegationShares, delegationShares) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &AddValidatorTx{spendingTx{msg}}, nil } -func (*AddValidatorTx) ChainID() ids.ID { - return constants.PrimaryNetworkID +// Validator describes the delegatee (offset read). +func (tx *AddValidatorTx) Validator() Validator { return readValidator(tx.root(), offAVValidator) } + +// StakeOuts is where staked tokens go when done validating (offset read). +func (tx *AddValidatorTx) StakeOuts() []*lux.TransferableOutput { + return readExtraOuts(tx.root(), offAVStakeOuts, offAVStakeAddrs) } -func (tx *AddValidatorTx) NodeID() ids.NodeID { - return tx.Validator.NodeID +// RewardsOwner is where staking rewards go when done validating. +func (tx *AddValidatorTx) RewardsOwner() fx.Owner { + return readOwner(tx.root(), offAVRewardsThreshold, offAVRewardsLocktime, offAVRewardsAddrs) } -func (*AddValidatorTx) PublicKey() (*bls.PublicKey, bool, error) { - return nil, false, nil -} +// DelegationShares is the fee (times 10,000) charged to delegators. +func (tx *AddValidatorTx) DelegationShares() uint32 { return tx.root().Uint32(offAVDelegationShares) } -func (*AddValidatorTx) PendingPriority() Priority { - return PrimaryNetworkValidatorPendingPriority -} - -func (*AddValidatorTx) CurrentPriority() Priority { - return PrimaryNetworkValidatorCurrentPriority -} - -func (tx *AddValidatorTx) Stake() []*lux.TransferableOutput { - return tx.StakeOuts -} - -func (tx *AddValidatorTx) ValidationRewardsOwner() fx.Owner { - return tx.RewardsOwner -} - -func (tx *AddValidatorTx) DelegationRewardsOwner() fx.Owner { - return tx.RewardsOwner -} - -func (tx *AddValidatorTx) Shares() uint32 { - return tx.DelegationShares -} - -// SyntacticVerify returns nil iff [tx] is valid +// SyntacticVerify returns nil iff [tx] is valid. func (tx *AddValidatorTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil - case tx.DelegationShares > reward.PercentDenominator: // Ensure delegators shares are in the allowed amount + } + if tx.DelegationShares() > reward.PercentDenominator { // shares in the allowed amount return errTooManyShares } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return fmt.Errorf("failed to verify BaseTx: %w", err) } - if err := verify.All(&tx.Validator, tx.RewardsOwner); err != nil { + v := tx.Validator() + if err := verify.All(&v, tx.RewardsOwner()); err != nil { return fmt.Errorf("failed to verify validator or rewards owner: %w", err) } + stakeOuts := tx.StakeOuts() totalStakeWeight := uint64(0) - for _, out := range tx.StakeOuts { + for _, out := range stakeOuts { if err := out.Verify(); err != nil { return fmt.Errorf("failed to verify output: %w", err) } @@ -127,14 +124,11 @@ func (tx *AddValidatorTx) SyntacticVerify(rt *runtime.Runtime) error { } switch { - case !lux.IsSortedTransferableOutputs(tx.StakeOuts): + case !lux.IsSortedTransferableOutputs(stakeOuts): return errOutputsNotSorted - case totalStakeWeight != tx.Wght: - return fmt.Errorf("%w: weight %d != stake %d", errValidatorWeightMismatch, tx.Wght, totalStakeWeight) + case totalStakeWeight != v.Wght: + return fmt.Errorf("%w: weight %d != stake %d", errValidatorWeightMismatch, v.Wght, totalStakeWeight) } - - // cache that this is valid - tx.SyntacticallyVerified = true return nil } @@ -142,8 +136,7 @@ func (tx *AddValidatorTx) Visit(visitor Visitor) error { return visitor.AddValidatorTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime +// Initialize is a no-op; Runtime is passed explicitly to InitRuntime. func (tx *AddValidatorTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here return nil } diff --git a/vms/platformvm/txs/base_tx.go b/vms/platformvm/txs/base_tx.go index 098b420e6..f28ff1cfa 100644 --- a/vms/platformvm/txs/base_tx.go +++ b/vms/platformvm/txs/base_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -9,11 +9,9 @@ import ( "fmt" "github.com/luxfi/runtime" - "github.com/luxfi/ids" - "github.com/luxfi/math/set" - lux "github.com/luxfi/utxo" "github.com/luxfi/utils" - "github.com/luxfi/utxo/secp256k1fx" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( @@ -25,86 +23,71 @@ var ( errInputsNotSortedUnique = errors.New("inputs not sorted and unique") ) -// BaseTx contains fields common to many transaction types. It should be -// embedded in transaction implementations. +// BaseTx is the bare spending envelope: kind + NetworkID/BlockchainID/Outs/ +// Ins/Memo, no delta fields. The struct IS the wire — it embeds spendingTx, +// which holds the zap buffer and serves the whole envelope surface (Bytes/ +// NetworkID/Outputs/InputIDs/Memo/...). Delta-carrying tx types embed +// spendingTx the same way and add their own field accessors. +// +// Wire: zap header + object{ envelope@0..76 } (kind=kindBase). type BaseTx struct { - lux.BaseTx `serialize:"true"` - - // true iff this transaction has already passed syntactic verification - SyntacticallyVerified bool `json:"-"` - - unsignedBytes []byte // Unsigned byte representation of this data + spendingTx } -func (tx *BaseTx) SetBytes(unsignedBytes []byte) { - tx.unsignedBytes = unsignedBytes -} - -func (tx *BaseTx) Bytes() []byte { - return tx.unsignedBytes -} - -func (tx *BaseTx) InputIDs() set.Set[ids.ID] { - inputIDs := set.NewSet[ids.ID](len(tx.Ins)) - for _, in := range tx.Ins { - inputIDs.Add(in.InputID()) +// NewBaseTx builds a bare spending tx into a fresh zap buffer — the one place +// the envelope fields become bytes (construction, not serialization). +func NewBaseTx(base *lux.BaseTx) (*BaseTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 256 + spendSize) + p, err := writeSpending(b, base) + if err != nil { + return nil, err } - return inputIDs -} - -func (tx *BaseTx) Outputs() []*lux.TransferableOutput { - return tx.Outs -} - -// InitRuntime sets the FxID fields in the inputs and outputs of this [BaseTx]. Also -// sets the [rt] to the given [vm.rt] so that the addresses can be json -// marshalled into human readable format -func (tx *BaseTx) InitRuntime(rt *runtime.Runtime) { - for _, in := range tx.BaseTx.Ins { - in.FxID = secp256k1fx.ID - } - for _, out := range tx.BaseTx.Outs { - out.FxID = secp256k1fx.ID - out.InitRuntime(rt) + ob := b.StartObject(spendSize) + setEnvelope(ob, kindBase, base, p) + ob.FinishAsRoot() + msg, err := zap.Parse(b.Finish()) + if err != nil { + return nil, err } + return &BaseTx{spendingTx{msg}}, nil } -// InitializeRuntime is a no-op. Runtime is passed explicitly. -func (tx *BaseTx) Initialize(ctx context.Context) error { - return nil -} - -// SyntacticVerify returns nil iff this tx is well formed +// SyntacticVerify returns nil iff this tx is well formed. func (tx *BaseTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil } - if err := tx.BaseTx.Verify(rt); err != nil { + return verifyBaseTx(tx.baseTx(), rt) +} + +func (tx *BaseTx) Visit(visitor Visitor) error { return visitor.BaseTx(tx) } + +func (tx *BaseTx) Initialize(context.Context) error { return nil } + +// verifyBaseTx runs the shared spending-envelope checks (metadata, per-output +// and per-input verification, canonical ordering). Every spending tx type +// composes this over its own delta-field checks — one envelope validator, +// reused. +func verifyBaseTx(base lux.BaseTx, rt *runtime.Runtime) error { + if err := base.Verify(rt); err != nil { return fmt.Errorf("metadata failed verification: %w", err) } - for _, out := range tx.Outs { + for _, out := range base.Outs { if err := out.Verify(); err != nil { return fmt.Errorf("output failed verification: %w", err) } } - for _, in := range tx.Ins { + for _, in := range base.Ins { if err := in.Verify(); err != nil { return fmt.Errorf("input failed verification: %w", err) } } switch { - case !lux.IsSortedTransferableOutputs(tx.Outs): + case !lux.IsSortedTransferableOutputs(base.Outs): return errOutputsNotSorted - case !utils.IsSortedAndUnique(tx.Ins): + case !utils.IsSortedAndUnique(base.Ins): return errInputsNotSortedUnique default: return nil } } - -func (tx *BaseTx) Visit(visitor Visitor) error { - return visitor.BaseTx(tx) -} diff --git a/vms/platformvm/txs/create_chain_tx.go b/vms/platformvm/txs/create_chain_tx.go index b4a584d3b..5f2dce97d 100644 --- a/vms/platformvm/txs/create_chain_tx.go +++ b/vms/platformvm/txs/create_chain_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -8,11 +8,13 @@ import ( "errors" "unicode" - "github.com/luxfi/runtime" "github.com/luxfi/constants" "github.com/luxfi/ids" "github.com/luxfi/node/vms/components/verify" + "github.com/luxfi/runtime" "github.com/luxfi/utils" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) const ( @@ -32,65 +34,114 @@ var ( errIllegalNameCharacter = errors.New("illegal name character") ) -// CreateChainTx is an unsigned createChainTx +// CreateChainTx creates a blockchain on a chain. The struct IS the wire: it +// embeds the spending envelope and reads its delta fields by offset. +// +// Delta layout (fixed section, after the 77-byte spending envelope): +// +// ChainID 32B @ 77 (id: chain that validates this blockchain) +// VMID 32B @ 109 (id: VM running on the new blockchain) +// BlockchainName 8B @ 141 (text ptr: human readable name) +// FxIDs 8B @ 149 (id list ptr: feature extensions) +// GenesisData 8B @ 157 (bytes ptr: genesis state) +// ChainAuth 8B @ 165 (auth ptr: sig-index list) type CreateChainTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // ID of the Chain that validates this blockchain - ChainID ids.ID `serialize:"true" json:"chainID"` - // A human readable name for the blockchain; need not be unique - BlockchainName string `serialize:"true" json:"blockchainName"` - // ID of the VM running on the new blockchain - VMID ids.ID `serialize:"true" json:"vmID"` - // IDs of the feature extensions running on the new blockchain - FxIDs []ids.ID `serialize:"true" json:"fxIDs"` - // Byte representation of genesis state of the new blockchain - GenesisData []byte `serialize:"true" json:"genesisData"` - // Authorizes this blockchain to be added to this chain - ChainAuth verify.Verifiable `serialize:"true" json:"chainAuthorization"` + spendingTx +} + +const ( + offCreateChainID = spendSize // 77 + offCreateChainVMID = 109 + offCreateChainName = 141 + offCreateChainFxIDs = 149 + offCreateChainGenesis = 157 + offCreateChainAuth = 165 + sizeCreateChain = 173 +) + +// NewCreateChainTx builds the tx into a fresh zap buffer. +func NewCreateChainTx(base *lux.BaseTx, chainID ids.ID, blockchainName string, vmID ids.ID, fxIDs []ids.ID, genesisData []byte, chainAuth verify.Verifiable) (*CreateChainTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 512 + sizeCreateChain) + p, err := writeSpending(b, base) + if err != nil { + return nil, err + } + fxOff, fxCount := writeIDList(b, fxIDs) + authOff, authCount, err := writeAuth(b, chainAuth) + if err != nil { + return nil, err + } + + ob := b.StartObject(sizeCreateChain) + setEnvelope(ob, kindCreateChain, base, p) + setID(ob, offCreateChainID, chainID) + setID(ob, offCreateChainVMID, vmID) + ob.SetText(offCreateChainName, blockchainName) + ob.SetList(offCreateChainFxIDs, fxOff, fxCount) + ob.SetBytes(offCreateChainGenesis, genesisData) + ob.SetList(offCreateChainAuth, authOff, authCount) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &CreateChainTx{spendingTx{msg}}, nil +} + +// ChainID is the ID of the chain that validates this blockchain (offset read). +func (tx *CreateChainTx) ChainID() ids.ID { return readID(tx.root(), offCreateChainID) } + +// VMID is the ID of the VM running on the new blockchain. +func (tx *CreateChainTx) VMID() ids.ID { return readID(tx.root(), offCreateChainVMID) } + +// BlockchainName is a human-readable (non-unique) name for the blockchain. +func (tx *CreateChainTx) BlockchainName() string { return tx.root().Text(offCreateChainName) } + +// FxIDs are the IDs of the feature extensions running on the new blockchain. +func (tx *CreateChainTx) FxIDs() []ids.ID { return readIDList(tx.root(), offCreateChainFxIDs) } + +// GenesisData is the byte representation of the new blockchain's genesis state. +func (tx *CreateChainTx) GenesisData() []byte { + if g := tx.root().Bytes(offCreateChainGenesis); len(g) > 0 { + return append([]byte(nil), g...) + } + return nil +} + +// ChainAuth authorizes this blockchain to be added to the chain. +func (tx *CreateChainTx) ChainAuth() verify.Verifiable { + return readAuth(tx.root(), offCreateChainAuth) } func (tx *CreateChainTx) SyntacticVerify(rt *runtime.Runtime) error { switch { case tx == nil: return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil - case tx.ChainID == constants.PrimaryNetworkID: + case tx.ChainID() == constants.PrimaryNetworkID: return ErrCantValidatePrimaryNetwork - case len(tx.BlockchainName) > MaxNameLen: + case len(tx.BlockchainName()) > MaxNameLen: return errNameTooLong - case tx.VMID == ids.Empty: + case tx.VMID() == ids.Empty: return errInvalidVMID - case !utils.IsSortedAndUnique(tx.FxIDs): + case !utils.IsSortedAndUnique(tx.FxIDs()): return errFxIDsNotSortedAndUnique - case len(tx.GenesisData) > MaxGenesisLen: + case len(tx.GenesisData()) > MaxGenesisLen: return errGenesisTooLong } - for _, r := range tx.BlockchainName { + for _, r := range tx.BlockchainName() { if r > unicode.MaxASCII || (!unicode.IsLetter(r) && !unicode.IsNumber(r) && r != ' ') { return errIllegalNameCharacter } } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - if err := tx.ChainAuth.Verify(); err != nil { - return err - } - - tx.SyntacticallyVerified = true - return nil + return tx.ChainAuth().Verify() } func (tx *CreateChainTx) Visit(visitor Visitor) error { return visitor.CreateChainTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime -func (tx *CreateChainTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here - return nil -} +// Initialize is a no-op; the struct is already the wire. +func (tx *CreateChainTx) Initialize(ctx context.Context) error { return nil } diff --git a/vms/platformvm/txs/create_network_tx.go b/vms/platformvm/txs/create_network_tx.go index 5a2b0a1f1..1e0b59ac9 100644 --- a/vms/platformvm/txs/create_network_tx.go +++ b/vms/platformvm/txs/create_network_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -6,55 +6,71 @@ package txs import ( "context" - "github.com/luxfi/runtime" - "github.com/luxfi/node/vms/platformvm/fx" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var _ UnsignedTx = (*CreateNetworkTx)(nil) -// CreateNetworkTx is an unsigned proposal to create a new chain +// CreateNetworkTx is an unsigned proposal to create a new network. The struct +// IS the wire: it embeds spendingTx (the envelope) and adds the owner header +// inline (threshold + locktime + addr-list ptr). +// +// Wire: zap header + object{ envelope@0..76, Owner{ threshold:u32@77, +// locktime:u64@81, addrs:listptr@89 } } (kind=kindCreateNetwork). type CreateNetworkTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // Who is authorized to manage this chain - Owner fx.Owner `serialize:"true" json:"owner"` + spendingTx } -// InitRuntime sets the FxID fields in the inputs and outputs of this -// [CreateNetworkTx]. Also sets the [rt] to the given [vm.rt] so that -// the addresses can be json marshalled into human readable format -func (tx *CreateNetworkTx) InitRuntime(rt *runtime.Runtime) { - tx.BaseTx.InitRuntime(rt) - // Owner doesn't have InitRuntime method +const ( + offCreateNetOwnerThreshold = spendSize // u32 + offCreateNetOwnerLocktime = 81 // u64 + offCreateNetOwnerAddrs = 89 // list ptr (8B) + sizeCreateNet = 97 +) + +// NewCreateNetworkTx builds the tx into a fresh zap buffer. +func NewCreateNetworkTx(base *lux.BaseTx, owner fx.Owner) (*CreateNetworkTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 256 + sizeCreateNet) + p, err := writeSpending(b, base) + if err != nil { + return nil, err + } + threshold, locktime, addrOff, addrCount, err := writeOwner(b, owner) + if err != nil { + return nil, err + } + ob := b.StartObject(sizeCreateNet) + setEnvelope(ob, kindCreateNetwork, base, p) + setOwner(ob, offCreateNetOwnerThreshold, offCreateNetOwnerLocktime, offCreateNetOwnerAddrs, threshold, locktime, addrOff, addrCount) + ob.FinishAsRoot() + msg, err := zap.Parse(b.Finish()) + if err != nil { + return nil, err + } + return &CreateNetworkTx{spendingTx{msg}}, nil } -// SyntacticVerify verifies that this transaction is well-formed +// Owner is who is authorized to manage this network (offset read). +func (tx *CreateNetworkTx) Owner() fx.Owner { + return readOwner(tx.root(), offCreateNetOwnerThreshold, offCreateNetOwnerLocktime, offCreateNetOwnerAddrs) +} + +// SyntacticVerify verifies that this transaction is well-formed. func (tx *CreateNetworkTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil } - - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - if err := tx.Owner.Verify(); err != nil { - return err - } - - tx.SyntacticallyVerified = true - return nil + return tx.Owner().Verify() } func (tx *CreateNetworkTx) Visit(visitor Visitor) error { return visitor.CreateNetworkTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime -func (tx *CreateNetworkTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here - return nil -} +func (tx *CreateNetworkTx) Initialize(context.Context) error { return nil } diff --git a/vms/platformvm/txs/disable_l1_validator_tx.go b/vms/platformvm/txs/disable_l1_validator_tx.go index 02b6a109e..34104e714 100644 --- a/vms/platformvm/txs/disable_l1_validator_tx.go +++ b/vms/platformvm/txs/disable_l1_validator_tx.go @@ -1,44 +1,75 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs import ( - "github.com/luxfi/runtime" - "github.com/luxfi/ids" "github.com/luxfi/node/vms/components/verify" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var _ UnsignedTx = (*DisableL1ValidatorTx)(nil) +// DisableL1ValidatorTx disables an L1 validator, proving authorization with a +// credential. The struct IS the wire: it embeds spendingTx (the envelope) and +// adds the validation id plus an auth sig-index list. +// +// Wire: zap header + object{ envelope@0..76, ValidationID:32B@77, +// DisableAuth:listptr@109 } (kind=kindDisableL1Validator). type DisableL1ValidatorTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // ID corresponding to the validator - ValidationID ids.ID `serialize:"true" json:"validationID"` - // Authorizes this validator to be disabled - DisableAuth verify.Verifiable `serialize:"true" json:"disableAuthorization"` + spendingTx +} + +const ( + offDisableValidationID = spendSize // 32B + offDisableAuth = 109 // list ptr (8B) + sizeDisable = 117 +) + +// NewDisableL1ValidatorTx builds the tx into a fresh zap buffer. +func NewDisableL1ValidatorTx(base *lux.BaseTx, validationID ids.ID, disableAuth verify.Verifiable) (*DisableL1ValidatorTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 256 + sizeDisable) + p, err := writeSpending(b, base) + if err != nil { + return nil, err + } + authOff, authCount, err := writeAuth(b, disableAuth) + if err != nil { + return nil, err + } + ob := b.StartObject(sizeDisable) + setEnvelope(ob, kindDisableL1Validator, base, p) + setID(ob, offDisableValidationID, validationID) + ob.SetList(offDisableAuth, authOff, authCount) + ob.FinishAsRoot() + msg, err := zap.Parse(b.Finish()) + if err != nil { + return nil, err + } + return &DisableL1ValidatorTx{spendingTx{msg}}, nil +} + +// ValidationID is the id of the validator being disabled (offset read). +func (tx *DisableL1ValidatorTx) ValidationID() ids.ID { + return readID(tx.root(), offDisableValidationID) +} + +// DisableAuth authorizes this validator to be disabled (offset read). +func (tx *DisableL1ValidatorTx) DisableAuth() verify.Verifiable { + return readAuth(tx.root(), offDisableAuth) } func (tx *DisableL1ValidatorTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: - // already passed syntactic verification - return nil } - - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - if err := tx.DisableAuth.Verify(); err != nil { - return err - } - - tx.SyntacticallyVerified = true - return nil + return tx.DisableAuth().Verify() } func (tx *DisableL1ValidatorTx) Visit(visitor Visitor) error { diff --git a/vms/platformvm/txs/export_tx.go b/vms/platformvm/txs/export_tx.go index d00220a17..14d91447c 100644 --- a/vms/platformvm/txs/export_tx.go +++ b/vms/platformvm/txs/export_tx.go @@ -1,20 +1,18 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs import ( "context" - - "github.com/luxfi/runtime" - "errors" "fmt" "github.com/luxfi/ids" - lux "github.com/luxfi/utxo" "github.com/luxfi/node/vms/platformvm/stakeable" - "github.com/luxfi/utxo/secp256k1fx" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( @@ -24,44 +22,71 @@ var ( errNoExportOutputs = errors.New("no export outputs") ) -// ExportTx is an unsigned exportTx +// ExportTx sends funds to another chain. The struct IS the wire: it embeds the +// spending envelope and reads its delta fields by offset. +// +// Delta layout (fixed section, after the 77-byte spending envelope): +// +// DestinationChain 32B @ 77 (id: chain to send the funds to) +// ExportedOutputs 8B @ 109 (output list ptr) +// OwnerAddrs 8B @ 117 (shared owner-address array ptr) type ExportTx struct { - BaseTx `serialize:"true"` - - // Which chain to send the funds to - DestinationChain ids.ID `serialize:"true" json:"destinationChain"` - - // Outputs that are exported to the chain - ExportedOutputs []*lux.TransferableOutput `serialize:"true" json:"exportedOutputs"` + spendingTx } -// InitRuntime sets the FxID fields in the inputs and outputs of this -// [UnsignedExportTx]. Also sets the [rt] to the given [vm.rt] so that -// the addresses can be json marshalled into human readable format -func (tx *ExportTx) InitRuntime(rt *runtime.Runtime) { - tx.BaseTx.InitRuntime(rt) - for _, out := range tx.ExportedOutputs { - out.FxID = secp256k1fx.ID - out.InitRuntime(rt) +const ( + offExportDestChain = spendSize // 77 + offExportOutputs = 109 + offExportAddrs = 117 + sizeExport = 125 +) + +// NewExportTx builds the tx into a fresh zap buffer. +func NewExportTx(base *lux.BaseTx, destinationChain ids.ID, exportedOutputs []*lux.TransferableOutput) (*ExportTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 512 + sizeExport) + p, err := writeSpending(b, base) + if err != nil { + return nil, err } + listOff, listCount, addrOff, addrCount, err := writeExtraOuts(b, exportedOutputs) + if err != nil { + return nil, err + } + + ob := b.StartObject(sizeExport) + setEnvelope(ob, kindExport, base, p) + setID(ob, offExportDestChain, destinationChain) + ob.SetList(offExportOutputs, listOff, listCount) + ob.SetList(offExportAddrs, addrOff, addrCount) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &ExportTx{spendingTx{msg}}, nil } -// SyntacticVerify this transaction is well-formed +// DestinationChain is the chain the exported funds are sent to (offset read). +func (tx *ExportTx) DestinationChain() ids.ID { return readID(tx.root(), offExportDestChain) } + +// ExportedOutputs are the outputs exported to the destination chain. +func (tx *ExportTx) ExportedOutputs() []*lux.TransferableOutput { + return readExtraOuts(tx.root(), offExportOutputs, offExportAddrs) +} + +// SyntacticVerify this transaction is well-formed. func (tx *ExportTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil - case len(tx.ExportedOutputs) == 0: + } + outs := tx.ExportedOutputs() + if len(outs) == 0 { return errNoExportOutputs } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - for _, out := range tx.ExportedOutputs { + for _, out := range outs { if err := out.Verify(); err != nil { return fmt.Errorf("output failed verification: %w", err) } @@ -69,11 +94,9 @@ func (tx *ExportTx) SyntacticVerify(rt *runtime.Runtime) error { return ErrWrongLocktime } } - if !lux.IsSortedTransferableOutputs(tx.ExportedOutputs) { + if !lux.IsSortedTransferableOutputs(outs) { return errOutputsNotSorted } - - tx.SyntacticallyVerified = true return nil } @@ -81,8 +104,5 @@ func (tx *ExportTx) Visit(visitor Visitor) error { return visitor.ExportTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime -func (tx *ExportTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here - return nil -} +// Initialize is a no-op; the struct is already the wire. +func (tx *ExportTx) Initialize(ctx context.Context) error { return nil } diff --git a/vms/platformvm/txs/import_tx.go b/vms/platformvm/txs/import_tx.go index 0c3d06723..1db892f4a 100644 --- a/vms/platformvm/txs/import_tx.go +++ b/vms/platformvm/txs/import_tx.go @@ -1,21 +1,19 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs import ( "context" - - "github.com/luxfi/runtime" - "errors" "fmt" "github.com/luxfi/ids" "github.com/luxfi/math/set" - lux "github.com/luxfi/utxo" + "github.com/luxfi/runtime" "github.com/luxfi/utils" - "github.com/luxfi/utxo/secp256k1fx" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( @@ -24,67 +22,93 @@ var ( errNoImportInputs = errors.New("tx has no imported inputs") ) -// ImportTx is an unsigned importTx +// ImportTx consumes funds produced on another chain. The struct IS the wire: +// it embeds the spending envelope and reads its delta fields by offset. +// +// Delta layout (fixed section, after the 77-byte spending envelope): +// +// SourceChain 32B @ 77 (id: chain to consume the funds from) +// ImportedInputs 8B @ 109 (input list ptr) +// SigIndices 8B @ 117 (shared input sig-index array ptr) type ImportTx struct { - BaseTx `serialize:"true"` - - // Which chain to consume the funds from - SourceChain ids.ID `serialize:"true" json:"sourceChain"` - - // Inputs that consume UTXOs produced on the chain - ImportedInputs []*lux.TransferableInput `serialize:"true" json:"importedInputs"` + spendingTx } -// InitRuntime sets the FxID fields in the inputs and outputs of this -// [ImportTx]. Also sets the [rt] to the given [vm.rt] so that -// the addresses can be json marshalled into human readable format -func (tx *ImportTx) InitRuntime(rt *runtime.Runtime) { - tx.BaseTx.InitRuntime(rt) - for _, in := range tx.ImportedInputs { - in.FxID = secp256k1fx.ID +const ( + offImportSourceChain = spendSize // 77 + offImportInputs = 109 + offImportSigIndices = 117 + sizeImport = 125 +) + +// NewImportTx builds the tx into a fresh zap buffer. +func NewImportTx(base *lux.BaseTx, sourceChain ids.ID, importedInputs []*lux.TransferableInput) (*ImportTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 512 + sizeImport) + p, err := writeSpending(b, base) + if err != nil { + return nil, err } + listOff, listCount, sigOff, sigCount, err := writeExtraIns(b, importedInputs) + if err != nil { + return nil, err + } + + ob := b.StartObject(sizeImport) + setEnvelope(ob, kindImport, base, p) + setID(ob, offImportSourceChain, sourceChain) + ob.SetList(offImportInputs, listOff, listCount) + ob.SetList(offImportSigIndices, sigOff, sigCount) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &ImportTx{spendingTx{msg}}, nil } -// InputUTXOs returns the UTXOIDs of the imported funds +// SourceChain is the chain the imported funds are consumed from (offset read). +func (tx *ImportTx) SourceChain() ids.ID { return readID(tx.root(), offImportSourceChain) } + +// ImportedInputs are the inputs consuming UTXOs produced on the source chain. +func (tx *ImportTx) ImportedInputs() []*lux.TransferableInput { + return readExtraIns(tx.root(), offImportInputs, offImportSigIndices) +} + +// InputUTXOs returns the UTXOIDs of the imported funds. func (tx *ImportTx) InputUTXOs() set.Set[ids.ID] { - set := set.NewSet[ids.ID](len(tx.ImportedInputs)) - for _, in := range tx.ImportedInputs { - set.Add(in.InputID()) + ins := tx.ImportedInputs() + s := set.NewSet[ids.ID](len(ins)) + for _, in := range ins { + s.Add(in.InputID()) } - return set + return s } func (tx *ImportTx) InputIDs() set.Set[ids.ID] { - inputs := tx.BaseTx.InputIDs() - atomicInputs := tx.InputUTXOs() - return inputs.Union(atomicInputs) + inputs := tx.spendingTx.InputIDs() + return inputs.Union(tx.InputUTXOs()) } -// SyntacticVerify this transaction is well-formed +// SyntacticVerify this transaction is well-formed. func (tx *ImportTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil - case len(tx.ImportedInputs) == 0: + } + ins := tx.ImportedInputs() + if len(ins) == 0 { return errNoImportInputs } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - for _, in := range tx.ImportedInputs { + for _, in := range ins { if err := in.Verify(); err != nil { return fmt.Errorf("input failed verification: %w", err) } } - if !utils.IsSortedAndUnique(tx.ImportedInputs) { + if !utils.IsSortedAndUnique(ins) { return errInputsNotSortedUnique } - - tx.SyntacticallyVerified = true return nil } @@ -92,8 +116,5 @@ func (tx *ImportTx) Visit(visitor Visitor) error { return visitor.ImportTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime -func (tx *ImportTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here - return nil -} +// Initialize is a no-op; the struct is already the wire. +func (tx *ImportTx) Initialize(ctx context.Context) error { return nil } diff --git a/vms/platformvm/txs/increase_l1_validator_balance_tx.go b/vms/platformvm/txs/increase_l1_validator_balance_tx.go index ccf0a5587..042ed07d4 100644 --- a/vms/platformvm/txs/increase_l1_validator_balance_tx.go +++ b/vms/platformvm/txs/increase_l1_validator_balance_tx.go @@ -1,14 +1,15 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs import ( - "github.com/luxfi/runtime" - "errors" "github.com/luxfi/ids" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( @@ -17,32 +18,60 @@ var ( ErrZeroBalance = errors.New("balance must be greater than 0") ) +// IncreaseL1ValidatorBalanceTx tops up an L1 validator's continuous-fee +// balance. The struct IS the wire: it embeds spendingTx (the envelope) and +// adds two delta fields read by offset. +// +// Wire: zap header + object{ envelope@0..76, ValidationID:32B@77, +// Balance:u64@109 } (kind=kindIncreaseL1ValidatorBalance). type IncreaseL1ValidatorBalanceTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // ID corresponding to the validator - ValidationID ids.ID `serialize:"true" json:"validationID"` - // Balance <= sum($LUX inputs) - sum($LUX outputs) - TxFee - Balance uint64 `serialize:"true" json:"balance"` + spendingTx +} + +const ( + offIncreaseValidationID = spendSize // 32B + offIncreaseBalance = 109 // u64 + sizeIncrease = 117 +) + +// NewIncreaseL1ValidatorBalanceTx builds the tx into a fresh zap buffer. +func NewIncreaseL1ValidatorBalanceTx(base *lux.BaseTx, validationID ids.ID, balance uint64) (*IncreaseL1ValidatorBalanceTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 256 + sizeIncrease) + p, err := writeSpending(b, base) + if err != nil { + return nil, err + } + ob := b.StartObject(sizeIncrease) + setEnvelope(ob, kindIncreaseL1ValidatorBalance, base, p) + setID(ob, offIncreaseValidationID, validationID) + ob.SetUint64(offIncreaseBalance, balance) + ob.FinishAsRoot() + msg, err := zap.Parse(b.Finish()) + if err != nil { + return nil, err + } + return &IncreaseL1ValidatorBalanceTx{spendingTx{msg}}, nil +} + +// ValidationID is the id of the validator being topped up (offset read). +func (tx *IncreaseL1ValidatorBalanceTx) ValidationID() ids.ID { + return readID(tx.root(), offIncreaseValidationID) +} + +// Balance is the amount to add; Balance <= sum($LUX inputs) - sum($LUX outputs) +// - TxFee (offset read). +func (tx *IncreaseL1ValidatorBalanceTx) Balance() uint64 { + return tx.root().Uint64(offIncreaseBalance) } func (tx *IncreaseL1ValidatorBalanceTx) SyntacticVerify(rt *runtime.Runtime) error { switch { case tx == nil: return ErrNilTx - case tx.SyntacticallyVerified: - // already passed syntactic verification - return nil - case tx.Balance == 0: + case tx.Balance() == 0: return ErrZeroBalance } - - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { - return err - } - - tx.SyntacticallyVerified = true - return nil + return verifyBaseTx(tx.baseTx(), rt) } func (tx *IncreaseL1ValidatorBalanceTx) Visit(visitor Visitor) error { diff --git a/vms/platformvm/txs/register_l1_validator_tx.go b/vms/platformvm/txs/register_l1_validator_tx.go index bde692fa8..15d09ab5a 100644 --- a/vms/platformvm/txs/register_l1_validator_tx.go +++ b/vms/platformvm/txs/register_l1_validator_tx.go @@ -1,44 +1,80 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs import ( - "github.com/luxfi/runtime" - "github.com/luxfi/crypto/bls" - "github.com/luxfi/vm/types" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var _ UnsignedTx = (*RegisterL1ValidatorTx)(nil) +// RegisterL1ValidatorTx registers an L1 validator. The struct IS the wire: it +// embeds the spending envelope and reads its delta fields by offset. +// +// Delta layout (fixed section, after the 77-byte spending envelope): +// +// Balance u64 @ 77 (Balance <= sum($LUX inputs) - sum($LUX outputs) - TxFee) +// ProofOfPossession 96B @ 85 (fixed, BLS PoP of the key in the Message) +// Message 8B @ 181 (bytes ptr: signed Warp AddressedCall payload) type RegisterL1ValidatorTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // Balance <= sum($LUX inputs) - sum($LUX outputs) - TxFee. - Balance uint64 `serialize:"true" json:"balance"` - // ProofOfPossession of the BLS key that is included in the Message. - ProofOfPossession [bls.SignatureLen]byte `serialize:"true" json:"proofOfPossession"` - // Message is expected to be a signed Warp message containing an - // AddressedCall payload with the RegisterL1Validator message. - Message types.JSONByteSlice `serialize:"true" json:"message"` + spendingTx +} + +const ( + offRegisterBalance = spendSize // 77 + offRegisterPoP = 85 + offRegisterMessage = 181 + sizeRegisterL1Validator = 189 +) + +// NewRegisterL1ValidatorTx builds the tx into a fresh zap buffer. +func NewRegisterL1ValidatorTx(base *lux.BaseTx, balance uint64, proofOfPossession [bls.SignatureLen]byte, message []byte) (*RegisterL1ValidatorTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 512 + sizeRegisterL1Validator) + p, err := writeSpending(b, base) + if err != nil { + return nil, err + } + + ob := b.StartObject(sizeRegisterL1Validator) + setEnvelope(ob, kindRegisterL1Validator, base, p) + ob.SetUint64(offRegisterBalance, balance) + ob.SetBytesFixed(offRegisterPoP, proofOfPossession[:]) + ob.SetBytes(offRegisterMessage, message) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &RegisterL1ValidatorTx{spendingTx{msg}}, nil +} + +// Balance is the initial balance funding this L1 validator (offset read). +func (tx *RegisterL1ValidatorTx) Balance() uint64 { return tx.root().Uint64(offRegisterBalance) } + +// ProofOfPossession is the BLS PoP of the key included in the Message. +func (tx *RegisterL1ValidatorTx) ProofOfPossession() [bls.SignatureLen]byte { + var pop [bls.SignatureLen]byte + copy(pop[:], tx.root().BytesFixedSlice(offRegisterPoP, bls.SignatureLen)) + return pop +} + +// Message is the signed Warp message carrying the RegisterL1Validator payload. +func (tx *RegisterL1ValidatorTx) Message() []byte { + if m := tx.root().Bytes(offRegisterMessage); len(m) > 0 { + return append([]byte(nil), m...) + } + return nil } func (tx *RegisterL1ValidatorTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: - // already passed syntactic verification - return nil } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { - return err - } - - tx.SyntacticallyVerified = true - return nil + base := BaseTx{BaseTx: tx.baseTx()} + return base.SyntacticVerify(rt) } func (tx *RegisterL1ValidatorTx) Visit(visitor Visitor) error { diff --git a/vms/platformvm/txs/remove_chain_validator_tx.go b/vms/platformvm/txs/remove_chain_validator_tx.go index 80087d3a8..13ed21779 100644 --- a/vms/platformvm/txs/remove_chain_validator_tx.go +++ b/vms/platformvm/txs/remove_chain_validator_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -7,10 +7,12 @@ import ( "context" "errors" - "github.com/luxfi/runtime" "github.com/luxfi/constants" "github.com/luxfi/ids" "github.com/luxfi/node/vms/components/verify" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( @@ -19,45 +21,78 @@ var ( ErrRemovePrimaryNetworkValidator = errors.New("can't remove primary network validator with RemoveChainValidatorTx") ) -// RemoveChainValidatorTx removes a validator from a chain. +// RemoveChainValidatorTx removes a validator from a chain. The struct IS the +// wire: it embeds spendingTx (the envelope) and adds the node id, chain id, +// and an auth sig-index list. +// +// Wire: zap header + object{ envelope@0..76, NodeID:20B@77, Chain:32B@97, +// ChainAuth:listptr@129 } (kind=kindRemoveChainValidator). type RemoveChainValidatorTx struct { - BaseTx `serialize:"true"` - // The node to remove from the chain. - NodeID ids.NodeID `serialize:"true" json:"nodeID"` - // The chain to remove the node from. - Chain ids.ID `serialize:"true" json:"chainID"` - // Proves that the issuer has the right to remove the node from the chain. - ChainAuth verify.Verifiable `serialize:"true" json:"chainAuthorization"` + spendingTx +} + +const ( + offRemoveNodeID = spendSize // 20B (ids.NodeIDLen) + offRemoveChain = 97 // 32B + offRemoveChainAuth = 129 // list ptr (8B) + sizeRemove = 137 +) + +// NewRemoveChainValidatorTx builds the tx into a fresh zap buffer. +func NewRemoveChainValidatorTx(base *lux.BaseTx, nodeID ids.NodeID, chain ids.ID, chainAuth verify.Verifiable) (*RemoveChainValidatorTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 256 + sizeRemove) + p, err := writeSpending(b, base) + if err != nil { + return nil, err + } + authOff, authCount, err := writeAuth(b, chainAuth) + if err != nil { + return nil, err + } + ob := b.StartObject(sizeRemove) + setEnvelope(ob, kindRemoveChainValidator, base, p) + setNodeID(ob, offRemoveNodeID, nodeID) + setID(ob, offRemoveChain, chain) + ob.SetList(offRemoveChainAuth, authOff, authCount) + ob.FinishAsRoot() + msg, err := zap.Parse(b.Finish()) + if err != nil { + return nil, err + } + return &RemoveChainValidatorTx{spendingTx{msg}}, nil +} + +// NodeID is the node to remove from the chain (offset read). +func (tx *RemoveChainValidatorTx) NodeID() ids.NodeID { + return readNodeID(tx.root(), offRemoveNodeID) +} + +// Chain is the chain to remove the node from (offset read). +func (tx *RemoveChainValidatorTx) Chain() ids.ID { + return readID(tx.root(), offRemoveChain) +} + +// ChainAuth proves the issuer's right to remove the node from the chain +// (offset read). +func (tx *RemoveChainValidatorTx) ChainAuth() verify.Verifiable { + return readAuth(tx.root(), offRemoveChainAuth) } func (tx *RemoveChainValidatorTx) SyntacticVerify(rt *runtime.Runtime) error { switch { case tx == nil: return ErrNilTx - case tx.SyntacticallyVerified: - // already passed syntactic verification - return nil - case tx.Chain == constants.PrimaryNetworkID: + case tx.Chain() == constants.PrimaryNetworkID: return ErrRemovePrimaryNetworkValidator } - - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - if err := tx.ChainAuth.Verify(); err != nil { - return err - } - - tx.SyntacticallyVerified = true - return nil + return tx.ChainAuth().Verify() } func (tx *RemoveChainValidatorTx) Visit(visitor Visitor) error { return visitor.RemoveChainValidatorTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime -func (tx *RemoveChainValidatorTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here - return nil -} +func (tx *RemoveChainValidatorTx) Initialize(context.Context) error { return nil } diff --git a/vms/platformvm/txs/reward_validator_tx.go b/vms/platformvm/txs/reward_validator_tx.go index be5fac204..4fda4a7b8 100644 --- a/vms/platformvm/txs/reward_validator_tx.go +++ b/vms/platformvm/txs/reward_validator_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -6,11 +6,11 @@ package txs import ( "context" - "github.com/luxfi/runtime" - "github.com/luxfi/ids" "github.com/luxfi/math/set" + "github.com/luxfi/runtime" lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var _ UnsignedTx = (*RewardValidatorTx)(nil) @@ -25,41 +25,44 @@ var _ UnsignedTx = (*RewardValidatorTx)(nil) // If this transaction is accepted and the next block accepted is an Abort // block, the validator is removed and the address that the validator specified // receives the staked LUX but no reward. +// +// The struct IS the wire: it holds the zap buffer and reads its field by +// offset. No codec, no marshal. +// +// Wire: zap header + object{ kind:u8@0, TxID:32B@1 }. type RewardValidatorTx struct { - // ID of the tx that created the delegator/validator being removed/rewarded - TxID ids.ID `serialize:"true" json:"txID"` - - unsignedBytes []byte // Unsigned byte representation of this data + msg *zap.Message } -func (tx *RewardValidatorTx) SetBytes(unsignedBytes []byte) { - tx.unsignedBytes = unsignedBytes +// offRewardTxID is the wire position of the removed staker's tx id (after +// kind@0). +const offRewardTxID = 1 // 32B, after kind@0 + +// NewRewardValidatorTx builds the tx into a fresh zap buffer — the one place a +// field becomes bytes (construction, not serialization). +func NewRewardValidatorTx(txID ids.ID) *RewardValidatorTx { + b := zap.NewBuilder(zap.HeaderSize + 16 + 33) + ob := b.StartObject(33) + ob.SetUint8(offKind, uint8(kindRewardValidator)) + setID(ob, offRewardTxID, txID) + ob.FinishAsRoot() + msg, _ := zap.Parse(b.Finish()) + return &RewardValidatorTx{msg: msg} } -func (*RewardValidatorTx) InitRuntime(*runtime.Runtime) {} +// TxID is the id of the tx that created the delegator/validator being +// removed/rewarded (offset read). +func (tx *RewardValidatorTx) TxID() ids.ID { return readID(tx.msg.Root(), offRewardTxID) } -func (tx *RewardValidatorTx) Bytes() []byte { - return tx.unsignedBytes -} +// Bytes returns the underlying zap buffer literally — no encode. +func (tx *RewardValidatorTx) Bytes() []byte { return tx.msg.Bytes() } -func (*RewardValidatorTx) InputIDs() set.Set[ids.ID] { - return nil -} +// SetBytes wraps b as this tx's buffer (zero copy); the Parse dispatch uses it. +func (tx *RewardValidatorTx) SetBytes(b []byte) { tx.msg, _ = zap.Parse(b) } -func (*RewardValidatorTx) Outputs() []*lux.TransferableOutput { - return nil -} - -func (*RewardValidatorTx) SyntacticVerify(*runtime.Runtime) error { - return nil -} - -func (tx *RewardValidatorTx) Visit(visitor Visitor) error { - return visitor.RewardValidatorTx(tx) -} - -// InitializeWithRuntime initializes the transaction with Runtime -func (tx *RewardValidatorTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here - return nil -} +func (*RewardValidatorTx) InitRuntime(*runtime.Runtime) {} +func (*RewardValidatorTx) InputIDs() set.Set[ids.ID] { return nil } +func (*RewardValidatorTx) Outputs() []*lux.TransferableOutput { return nil } +func (*RewardValidatorTx) SyntacticVerify(*runtime.Runtime) error { return nil } +func (tx *RewardValidatorTx) Visit(visitor Visitor) error { return visitor.RewardValidatorTx(tx) } +func (tx *RewardValidatorTx) Initialize(context.Context) error { return nil } diff --git a/vms/platformvm/txs/set_l1_validator_weight_tx.go b/vms/platformvm/txs/set_l1_validator_weight_tx.go index a54b548d6..278aefd89 100644 --- a/vms/platformvm/txs/set_l1_validator_weight_tx.go +++ b/vms/platformvm/txs/set_l1_validator_weight_tx.go @@ -1,39 +1,64 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs import ( "github.com/luxfi/runtime" - - "github.com/luxfi/vm/types" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var _ UnsignedTx = (*SetL1ValidatorWeightTx)(nil) +// SetL1ValidatorWeightTx carries a signed Warp message that sets an L1 +// validator's weight. The struct IS the wire: it embeds spendingTx (the +// envelope) and adds the Warp Message as a variable-length bytes tail. +// +// Wire: zap header + object{ envelope@0..76, Message:bytesptr@77 } +// (kind=kindSetL1ValidatorWeight). type SetL1ValidatorWeightTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // Message is expected to be a signed Warp message containing an - // AddressedCall payload with the SetL1ValidatorWeight message. - Message types.JSONByteSlice `serialize:"true" json:"message"` + spendingTx +} + +const ( + offSetWeightMessage = spendSize // bytes ptr (8B) + sizeSetWeight = 85 +) + +// NewSetL1ValidatorWeightTx builds the tx into a fresh zap buffer. +func NewSetL1ValidatorWeightTx(base *lux.BaseTx, message []byte) (*SetL1ValidatorWeightTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 256 + sizeSetWeight) + p, err := writeSpending(b, base) + if err != nil { + return nil, err + } + ob := b.StartObject(sizeSetWeight) + setEnvelope(ob, kindSetL1ValidatorWeight, base, p) + ob.SetBytes(offSetWeightMessage, message) + ob.FinishAsRoot() + msg, err := zap.Parse(b.Finish()) + if err != nil { + return nil, err + } + return &SetL1ValidatorWeightTx{spendingTx{msg}}, nil +} + +// Message is the signed Warp message (an AddressedCall carrying the +// SetL1ValidatorWeight payload). Returns a copy so callers can't mutate the +// buffer. +func (tx *SetL1ValidatorWeightTx) Message() []byte { + if m := tx.root().Bytes(offSetWeightMessage); len(m) > 0 { + return append([]byte(nil), m...) + } + return nil } func (tx *SetL1ValidatorWeightTx) SyntacticVerify(rt *runtime.Runtime) error { - switch { - case tx == nil: + if tx == nil { return ErrNilTx - case tx.SyntacticallyVerified: - // already passed syntactic verification - return nil } - - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { - return err - } - - tx.SyntacticallyVerified = true - return nil + return verifyBaseTx(tx.baseTx(), rt) } func (tx *SetL1ValidatorWeightTx) Visit(visitor Visitor) error { diff --git a/vms/platformvm/txs/transfer_chain_ownership_tx.go b/vms/platformvm/txs/transfer_chain_ownership_tx.go index 343930763..e97e6fa1b 100644 --- a/vms/platformvm/txs/transfer_chain_ownership_tx.go +++ b/vms/platformvm/txs/transfer_chain_ownership_tx.go @@ -1,20 +1,19 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs import ( "context" - - "github.com/luxfi/runtime" - "errors" "github.com/luxfi/constants" "github.com/luxfi/ids" "github.com/luxfi/node/vms/components/verify" "github.com/luxfi/node/vms/platformvm/fx" - "github.com/luxfi/utxo/secp256k1fx" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( @@ -23,56 +22,86 @@ var ( ErrTransferPermissionlessChain = errors.New("cannot transfer ownership of a permissionless chain") ) +// TransferChainOwnershipTx re-owns a chain. The struct IS the wire: it embeds +// the spending envelope and reads its delta fields by offset. +// +// Delta layout (fixed section, after the 77-byte spending envelope): +// +// Chain 32B @ 77 (id) +// ChainAuth 8B @ 109 (auth ptr: sig-index list) +// Owner.Threshold u32 @ 117 +// Owner.Locktime u64 @ 121 +// Owner.Addrs 8B @ 129 (addr list ptr) type TransferChainOwnershipTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // ID of the chain this tx is modifying - Chain ids.ID `serialize:"true" json:"chainID"` - // Proves that the issuer has the right to modify the chain. - ChainAuth verify.Verifiable `serialize:"true" json:"chainAuthorization"` - // Who is now authorized to manage this chain - Owner fx.Owner `serialize:"true" json:"newOwner"` + spendingTx } -// InitRuntime sets the FxID fields in the inputs and outputs of this -// [TransferChainOwnershipTx]. Also sets the [rt] to the given [vm.rt] so -// that the addresses can be json marshalled into human readable format -func (tx *TransferChainOwnershipTx) InitRuntime(rt *runtime.Runtime) { - tx.BaseTx.InitRuntime(rt) - // Owner doesn't have InitRuntime method - if owner, ok := tx.Owner.(*secp256k1fx.OutputOwners); ok { - owner.InitRuntime(rt) +const ( + offTransferChain = spendSize // 77 + offTransferChainAuth = 109 + offTransferOwnerThreshold = 117 + offTransferOwnerLocktime = 121 + offTransferOwnerAddrs = 129 + sizeTransferChainOwnership = 137 +) + +// NewTransferChainOwnershipTx builds the tx into a fresh zap buffer. +func NewTransferChainOwnershipTx(base *lux.BaseTx, chain ids.ID, chainAuth verify.Verifiable, owner fx.Owner) (*TransferChainOwnershipTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 512 + sizeTransferChainOwnership) + p, err := writeSpending(b, base) + if err != nil { + return nil, err } + authOff, authCount, err := writeAuth(b, chainAuth) + if err != nil { + return nil, err + } + threshold, locktime, addrOff, addrCount, err := writeOwner(b, owner) + if err != nil { + return nil, err + } + + ob := b.StartObject(sizeTransferChainOwnership) + setEnvelope(ob, kindTransferChainOwnership, base, p) + setID(ob, offTransferChain, chain) + ob.SetList(offTransferChainAuth, authOff, authCount) + setOwner(ob, offTransferOwnerThreshold, offTransferOwnerLocktime, offTransferOwnerAddrs, threshold, locktime, addrOff, addrCount) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &TransferChainOwnershipTx{spendingTx{msg}}, nil +} + +// Chain is the ID of the chain this tx is modifying (offset read). +func (tx *TransferChainOwnershipTx) Chain() ids.ID { return readID(tx.root(), offTransferChain) } + +// ChainAuth proves the issuer has the right to modify the chain. +func (tx *TransferChainOwnershipTx) ChainAuth() verify.Verifiable { + return readAuth(tx.root(), offTransferChainAuth) +} + +// Owner is who is now authorized to manage this chain. +func (tx *TransferChainOwnershipTx) Owner() fx.Owner { + return readOwner(tx.root(), offTransferOwnerThreshold, offTransferOwnerLocktime, offTransferOwnerAddrs) } func (tx *TransferChainOwnershipTx) SyntacticVerify(rt *runtime.Runtime) error { switch { case tx == nil: return ErrNilTx - case tx.SyntacticallyVerified: - // already passed syntactic verification - return nil - case tx.Chain == constants.PrimaryNetworkID: + case tx.Chain() == constants.PrimaryNetworkID: return ErrTransferPermissionlessChain } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - if err := verify.All(tx.ChainAuth, tx.Owner); err != nil { - return err - } - - tx.SyntacticallyVerified = true - return nil + return verify.All(tx.ChainAuth(), tx.Owner()) } func (tx *TransferChainOwnershipTx) Visit(visitor Visitor) error { return visitor.TransferChainOwnershipTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime -func (tx *TransferChainOwnershipTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here - return nil -} +// Initialize is a no-op; the struct is already the wire. +func (tx *TransferChainOwnershipTx) Initialize(ctx context.Context) error { return nil } diff --git a/vms/platformvm/txs/transform_chain_tx.go b/vms/platformvm/txs/transform_chain_tx.go index 7fdae595a..4bd18e7fe 100644 --- a/vms/platformvm/txs/transform_chain_tx.go +++ b/vms/platformvm/txs/transform_chain_tx.go @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package txs @@ -8,12 +8,13 @@ import ( "errors" "fmt" - "github.com/luxfi/runtime" - "github.com/luxfi/constants" "github.com/luxfi/ids" "github.com/luxfi/node/vms/components/verify" "github.com/luxfi/node/vms/platformvm/reward" + "github.com/luxfi/runtime" + lux "github.com/luxfi/utxo" + "github.com/luxfi/zap" ) var ( @@ -38,141 +39,230 @@ var ( errUptimeRequirementTooLarge = fmt.Errorf("uptime requirement must be less than or equal to %d", reward.PercentDenominator) ) -// TransformChainTx is an unsigned transformChainTx +// TransformChainTx transforms a chain into a permissionless one. The struct IS +// the wire: it embeds the spending envelope and reads its delta fields by +// offset. +// +// Delta layout (fixed section, after the 77-byte spending envelope): +// +// Chain 32B @ 77 +// AssetID 32B @ 109 +// InitialSupply u64 @ 141 +// MaximumSupply u64 @ 149 +// MinConsumptionRate u64 @ 157 +// MaxConsumptionRate u64 @ 165 +// MinValidatorStake u64 @ 173 +// MaxValidatorStake u64 @ 181 +// MinStakeDuration u32 @ 189 +// MaxStakeDuration u32 @ 193 +// MinDelegationFee u32 @ 197 +// MinDelegatorStake u64 @ 201 +// MaxValidatorWeightFactor u8 @ 209 +// UptimeRequirement u32 @ 210 +// ChainAuth 8B @ 214 (auth ptr: sig-index list) type TransformChainTx struct { - // Metadata, inputs and outputs - BaseTx `serialize:"true"` - // ID of the Chain to transform - // Restrictions: - // - Must not be the Primary Network ID - Chain ids.ID `serialize:"true" json:"chainID"` - // Asset to use when staking on the Net - // Restrictions: - // - Must not be the Empty ID - // - Must not be the LUX ID - AssetID ids.ID `serialize:"true" json:"assetID"` - // Amount to initially specify as the current supply - // Restrictions: - // - Must be > 0 - InitialSupply uint64 `serialize:"true" json:"initialSupply"` - // Amount to specify as the maximum token supply - // Restrictions: - // - Must be >= [InitialSupply] - MaximumSupply uint64 `serialize:"true" json:"maximumSupply"` - // MinConsumptionRate is the rate to allocate funds if the validator's stake - // duration is 0 - MinConsumptionRate uint64 `serialize:"true" json:"minConsumptionRate"` - // MaxConsumptionRate is the rate to allocate funds if the validator's stake - // duration is equal to the minting period - // Restrictions: - // - Must be >= [MinConsumptionRate] - // - Must be <= [reward.PercentDenominator] - MaxConsumptionRate uint64 `serialize:"true" json:"maxConsumptionRate"` - // MinValidatorStake is the minimum amount of funds required to become a - // validator. - // Restrictions: - // - Must be > 0 - // - Must be <= [InitialSupply] - MinValidatorStake uint64 `serialize:"true" json:"minValidatorStake"` - // MaxValidatorStake is the maximum amount of funds a single validator can - // be allocated, including delegated funds. - // Restrictions: - // - Must be >= [MinValidatorStake] - // - Must be <= [MaximumSupply] - MaxValidatorStake uint64 `serialize:"true" json:"maxValidatorStake"` - // MinStakeDuration is the minimum number of seconds a staker can stake for. - // Restrictions: - // - Must be > 0 - MinStakeDuration uint32 `serialize:"true" json:"minStakeDuration"` - // MaxStakeDuration is the maximum number of seconds a staker can stake for. - // Restrictions: - // - Must be >= [MinStakeDuration] - // - Must be <= [GlobalMaxStakeDuration] - MaxStakeDuration uint32 `serialize:"true" json:"maxStakeDuration"` - // MinDelegationFee is the minimum percentage a validator must charge a - // delegator for delegating. - // Restrictions: - // - Must be <= [reward.PercentDenominator] - MinDelegationFee uint32 `serialize:"true" json:"minDelegationFee"` - // MinDelegatorStake is the minimum amount of funds required to become a - // delegator. - // Restrictions: - // - Must be > 0 - MinDelegatorStake uint64 `serialize:"true" json:"minDelegatorStake"` - // MaxValidatorWeightFactor is the factor which calculates the maximum - // amount of delegation a validator can receive. - // Note: a value of 1 effectively disables delegation. - // Restrictions: - // - Must be > 0 - MaxValidatorWeightFactor byte `serialize:"true" json:"maxValidatorWeightFactor"` - // UptimeRequirement is the minimum percentage a validator must be online - // and responsive to receive a reward. - // Restrictions: - // - Must be <= [reward.PercentDenominator] - UptimeRequirement uint32 `serialize:"true" json:"uptimeRequirement"` - // Authorizes this transformation - ChainAuth verify.Verifiable `serialize:"true" json:"chainAuthorization"` + spendingTx +} + +const ( + offTransformChain = spendSize // 77 + offTransformAssetID = 109 + offTransformInitialSupply = 141 + offTransformMaximumSupply = 149 + offTransformMinConsumptionRate = 157 + offTransformMaxConsumptionRate = 165 + offTransformMinValidatorStake = 173 + offTransformMaxValidatorStake = 181 + offTransformMinStakeDuration = 189 + offTransformMaxStakeDuration = 193 + offTransformMinDelegationFee = 197 + offTransformMinDelegatorStake = 201 + offTransformMaxValidatorWeightFactor = 209 + offTransformUptimeRequirement = 210 + offTransformChainAuth = 214 + sizeTransformChain = 222 +) + +// NewTransformChainTx builds the tx into a fresh zap buffer. +func NewTransformChainTx( + base *lux.BaseTx, + chain ids.ID, + assetID ids.ID, + initialSupply uint64, + maximumSupply uint64, + minConsumptionRate uint64, + maxConsumptionRate uint64, + minValidatorStake uint64, + maxValidatorStake uint64, + minStakeDuration uint32, + maxStakeDuration uint32, + minDelegationFee uint32, + minDelegatorStake uint64, + maxValidatorWeightFactor byte, + uptimeRequirement uint32, + chainAuth verify.Verifiable, +) (*TransformChainTx, error) { + b := zap.NewBuilder(zap.HeaderSize + 512 + sizeTransformChain) + p, err := writeSpending(b, base) + if err != nil { + return nil, err + } + authOff, authCount, err := writeAuth(b, chainAuth) + if err != nil { + return nil, err + } + + ob := b.StartObject(sizeTransformChain) + setEnvelope(ob, kindTransformChain, base, p) + setID(ob, offTransformChain, chain) + setID(ob, offTransformAssetID, assetID) + ob.SetUint64(offTransformInitialSupply, initialSupply) + ob.SetUint64(offTransformMaximumSupply, maximumSupply) + ob.SetUint64(offTransformMinConsumptionRate, minConsumptionRate) + ob.SetUint64(offTransformMaxConsumptionRate, maxConsumptionRate) + ob.SetUint64(offTransformMinValidatorStake, minValidatorStake) + ob.SetUint64(offTransformMaxValidatorStake, maxValidatorStake) + ob.SetUint32(offTransformMinStakeDuration, minStakeDuration) + ob.SetUint32(offTransformMaxStakeDuration, maxStakeDuration) + ob.SetUint32(offTransformMinDelegationFee, minDelegationFee) + ob.SetUint64(offTransformMinDelegatorStake, minDelegatorStake) + ob.SetUint8(offTransformMaxValidatorWeightFactor, maxValidatorWeightFactor) + ob.SetUint32(offTransformUptimeRequirement, uptimeRequirement) + ob.SetList(offTransformChainAuth, authOff, authCount) + ob.FinishAsRoot() + + msg, _ := zap.Parse(b.Finish()) + return &TransformChainTx{spendingTx{msg}}, nil +} + +// Chain is the ID of the chain to transform (offset read). +func (tx *TransformChainTx) Chain() ids.ID { return readID(tx.root(), offTransformChain) } + +// AssetID is the asset to use when staking on the chain. +func (tx *TransformChainTx) AssetID() ids.ID { return readID(tx.root(), offTransformAssetID) } + +// InitialSupply is the amount to initially specify as the current supply. +func (tx *TransformChainTx) InitialSupply() uint64 { + return tx.root().Uint64(offTransformInitialSupply) +} + +// MaximumSupply is the amount to specify as the maximum token supply. +func (tx *TransformChainTx) MaximumSupply() uint64 { + return tx.root().Uint64(offTransformMaximumSupply) +} + +// MinConsumptionRate is the rate to allocate funds if the validator's stake +// duration is 0. +func (tx *TransformChainTx) MinConsumptionRate() uint64 { + return tx.root().Uint64(offTransformMinConsumptionRate) +} + +// MaxConsumptionRate is the rate to allocate funds if the validator's stake +// duration is equal to the minting period. +func (tx *TransformChainTx) MaxConsumptionRate() uint64 { + return tx.root().Uint64(offTransformMaxConsumptionRate) +} + +// MinValidatorStake is the minimum amount of funds required to become a +// validator. +func (tx *TransformChainTx) MinValidatorStake() uint64 { + return tx.root().Uint64(offTransformMinValidatorStake) +} + +// MaxValidatorStake is the maximum amount of funds a single validator can be +// allocated, including delegated funds. +func (tx *TransformChainTx) MaxValidatorStake() uint64 { + return tx.root().Uint64(offTransformMaxValidatorStake) +} + +// MinStakeDuration is the minimum number of seconds a staker can stake for. +func (tx *TransformChainTx) MinStakeDuration() uint32 { + return tx.root().Uint32(offTransformMinStakeDuration) +} + +// MaxStakeDuration is the maximum number of seconds a staker can stake for. +func (tx *TransformChainTx) MaxStakeDuration() uint32 { + return tx.root().Uint32(offTransformMaxStakeDuration) +} + +// MinDelegationFee is the minimum percentage a validator must charge a +// delegator for delegating. +func (tx *TransformChainTx) MinDelegationFee() uint32 { + return tx.root().Uint32(offTransformMinDelegationFee) +} + +// MinDelegatorStake is the minimum amount of funds required to become a +// delegator. +func (tx *TransformChainTx) MinDelegatorStake() uint64 { + return tx.root().Uint64(offTransformMinDelegatorStake) +} + +// MaxValidatorWeightFactor is the factor which calculates the maximum amount of +// delegation a validator can receive. +func (tx *TransformChainTx) MaxValidatorWeightFactor() byte { + return tx.root().Uint8(offTransformMaxValidatorWeightFactor) +} + +// UptimeRequirement is the minimum percentage a validator must be online and +// responsive to receive a reward. +func (tx *TransformChainTx) UptimeRequirement() uint32 { + return tx.root().Uint32(offTransformUptimeRequirement) +} + +// ChainAuth authorizes this transformation. +func (tx *TransformChainTx) ChainAuth() verify.Verifiable { + return readAuth(tx.root(), offTransformChainAuth) } func (tx *TransformChainTx) SyntacticVerify(rt *runtime.Runtime) error { switch { case tx == nil: return ErrNilTx - case tx.SyntacticallyVerified: // already passed syntactic verification - return nil - case tx.Chain == constants.PrimaryNetworkID: + case tx.Chain() == constants.PrimaryNetworkID: return errCantTransformPrimaryNetwork - case tx.AssetID == ids.Empty: + case tx.AssetID() == ids.Empty: return errEmptyAssetID - case tx.AssetID == rt.UTXOAssetID: + case tx.AssetID() == rt.UTXOAssetID: return errAssetIDCantBeLUX - case tx.InitialSupply == 0: + case tx.InitialSupply() == 0: return errInitialSupplyZero - case tx.InitialSupply > tx.MaximumSupply: + case tx.InitialSupply() > tx.MaximumSupply(): return errInitialSupplyGreaterThanMaxSupply - case tx.MinConsumptionRate > tx.MaxConsumptionRate: + case tx.MinConsumptionRate() > tx.MaxConsumptionRate(): return errMinConsumptionRateTooLarge - case tx.MaxConsumptionRate > reward.PercentDenominator: + case tx.MaxConsumptionRate() > reward.PercentDenominator: return errMaxConsumptionRateTooLarge - case tx.MinValidatorStake == 0: + case tx.MinValidatorStake() == 0: return errMinValidatorStakeZero - case tx.MinValidatorStake > tx.InitialSupply: + case tx.MinValidatorStake() > tx.InitialSupply(): return errMinValidatorStakeAboveSupply - case tx.MinValidatorStake > tx.MaxValidatorStake: + case tx.MinValidatorStake() > tx.MaxValidatorStake(): return errMinValidatorStakeAboveMax - case tx.MaxValidatorStake > tx.MaximumSupply: + case tx.MaxValidatorStake() > tx.MaximumSupply(): return errMaxValidatorStakeTooLarge - case tx.MinStakeDuration == 0: + case tx.MinStakeDuration() == 0: return errMinStakeDurationZero - case tx.MinStakeDuration > tx.MaxStakeDuration: + case tx.MinStakeDuration() > tx.MaxStakeDuration(): return errMinStakeDurationTooLarge - case tx.MinDelegationFee > reward.PercentDenominator: + case tx.MinDelegationFee() > reward.PercentDenominator: return errMinDelegationFeeTooLarge - case tx.MinDelegatorStake == 0: + case tx.MinDelegatorStake() == 0: return errMinDelegatorStakeZero - case tx.MaxValidatorWeightFactor == 0: + case tx.MaxValidatorWeightFactor() == 0: return errMaxValidatorWeightFactorZero - case tx.UptimeRequirement > reward.PercentDenominator: + case tx.UptimeRequirement() > reward.PercentDenominator: return errUptimeRequirementTooLarge } - if err := tx.BaseTx.SyntacticVerify(rt); err != nil { + if err := verifyBaseTx(tx.baseTx(), rt); err != nil { return err } - if err := tx.ChainAuth.Verify(); err != nil { - return err - } - - tx.SyntacticallyVerified = true - return nil + return tx.ChainAuth().Verify() } func (tx *TransformChainTx) Visit(visitor Visitor) error { return visitor.TransformChainTx(tx) } -// InitializeWithRuntime initializes the transaction with Runtime -func (tx *TransformChainTx) Initialize(ctx context.Context) error { - // Initialize any context-dependent fields here - return nil -} +// Initialize is a no-op; the struct is already the wire. +func (tx *TransformChainTx) Initialize(ctx context.Context) error { return nil }