mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Restore+convert remaining txs tests to struct-is-wire + dispatch-safety guard
Converts the per-type P-chain tx tests to native New*Tx + accessor methods + roundTrip/SyntacticVerify (agents, verified): add_validator, add_chain_validator, create_blockchain, disable/increase/register/set_weight/remove_chain/ transfer_ownership L1, add_permissionless_delegator, transform_chain. Every error-path sentinel preserved by driving the bad value THROUGH the constructor (pure byte-writer). Mock-based cases (fxmock/luxmock/verifymock, impossible on an immutable zap buffer) reproduced with REAL unspendable owners / unsorted inputs → real sentinels (ErrOutputUnspendable, ErrInputIndicesNotSortedUnique). Only the un-reproducible 'already verified' cached-flag case dropped per file. NEW staker_dispatch_guard_test.go pins the interface-satisfaction invariant the staker type-switches depend on: struct-is-wire made *AddValidatorTx satisfy both ValidatorTx+DelegatorTx (safe — ValidatorTx checked first everywhere), but *AddDelegatorTx must NOT satisfy ValidatorTx (verified: lacks ValidationRewardsOwner/Shares) or delegators would mis-route. Guard fails loudly if a future edit breaks either property. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
)
|
||||
|
||||
// TestAddChainValidatorTxSyntacticVerify pins the (deprecated) per-chain
|
||||
// validator-registration invariants. Every (in)valid case is expressed by
|
||||
// passing values THROUGH the NewAddChainValidatorTx constructor.
|
||||
func TestAddChainValidatorTxSyntacticVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
weight := uint64(2022)
|
||||
chain := ids.ID{'s', 'u', 'b', 'n', 'e', 't', 'I', 'D'}
|
||||
goodAuth := func() *secp256k1fx.Input { return &secp256k1fx.Input{SigIndices: []uint32{0, 1}} }
|
||||
validator := func(wght uint64) Validator {
|
||||
return Validator{NodeID: ids.GenerateTestNodeID(), Start: 0, End: 3600, Wght: wght}
|
||||
}
|
||||
base := func(networkID uint32) *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: networkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
|
||||
// Case: signed tx is nil
|
||||
var stx *Tx
|
||||
require.ErrorIs(stx.SyntacticVerify(rt), ErrNilSignedTx)
|
||||
|
||||
// Case: unsigned tx is nil
|
||||
var nilTx *AddChainValidatorTx
|
||||
require.ErrorIs(nilTx.SyntacticVerify(rt), ErrNilTx)
|
||||
|
||||
// Case: valid tx
|
||||
valid, err := NewAddChainValidatorTx(base(rt.NetworkID), validator(weight), chain, goodAuth())
|
||||
require.NoError(err)
|
||||
require.NoError(valid.SyntacticVerify(rt))
|
||||
|
||||
// Case: wrong network ID
|
||||
wrongNet, err := NewAddChainValidatorTx(base(rt.NetworkID+1), validator(weight), chain, goodAuth())
|
||||
require.NoError(err)
|
||||
require.ErrorIs(wrongNet.SyntacticVerify(rt), lux.ErrWrongNetworkID)
|
||||
|
||||
// Case: specifies primary network ChainID (empty)
|
||||
primaryEmpty, err := NewAddChainValidatorTx(base(rt.NetworkID), validator(weight), ids.Empty, goodAuth())
|
||||
require.NoError(err)
|
||||
require.ErrorIs(primaryEmpty.SyntacticVerify(rt), errAddPrimaryNetworkValidator)
|
||||
|
||||
// Case: no weight
|
||||
noWeight, err := NewAddChainValidatorTx(base(rt.NetworkID), validator(0), chain, goodAuth())
|
||||
require.NoError(err)
|
||||
require.ErrorIs(noWeight.SyntacticVerify(rt), ErrWeightTooSmall)
|
||||
|
||||
// Case: chain auth indices not sorted+unique ([1,1])
|
||||
badAuth, err := NewAddChainValidatorTx(base(rt.NetworkID), validator(weight), chain, &secp256k1fx.Input{SigIndices: []uint32{1, 1}})
|
||||
require.NoError(err)
|
||||
require.ErrorIs(badAuth.SyntacticVerify(rt), secp256k1fx.ErrInputIndicesNotSortedUnique)
|
||||
|
||||
// Case: adding to Primary Network (explicit PrimaryNetworkID)
|
||||
primary, err := NewAddChainValidatorTx(base(rt.NetworkID), validator(weight), constants.PrimaryNetworkID, goodAuth())
|
||||
require.NoError(err)
|
||||
require.ErrorIs(primary.SyntacticVerify(rt), errAddPrimaryNetworkValidator)
|
||||
}
|
||||
|
||||
// TestAddChainValidatorTx_RoundTrip replaces the deleted linearcodec
|
||||
// Marshal/Parse golden test: the delta fields (Validator, Chain, ChainAuth)
|
||||
// survive the struct-is-wire serialize→Parse path.
|
||||
func TestAddChainValidatorTx_RoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
vdr := Validator{NodeID: ids.GenerateTestNodeID(), Start: 0, End: 3600, Wght: 2022}
|
||||
chain := ids.GenerateTestID()
|
||||
auth := &secp256k1fx.Input{SigIndices: []uint32{0, 1}}
|
||||
|
||||
utx, err := NewAddChainValidatorTx(spendBase(), vdr, chain, auth)
|
||||
require.NoError(err)
|
||||
|
||||
got := roundTrip(t, utx).(*AddChainValidatorTx)
|
||||
require.Equal(vdr, got.Validator())
|
||||
require.Equal(chain, got.Chain())
|
||||
require.Equal(utx.ChainAuth(), got.ChainAuth())
|
||||
}
|
||||
|
||||
func TestAddChainValidatorTxNotValidatorTx(t *testing.T) {
|
||||
txIntf := any((*AddChainValidatorTx)(nil))
|
||||
_, ok := txIntf.(ValidatorTx)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestAddChainValidatorTxNotDelegatorTx(t *testing.T) {
|
||||
txIntf := any((*AddChainValidatorTx)(nil))
|
||||
_, ok := txIntf.(DelegatorTx)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestAddChainValidatorTxNotPermissionlessStaker(t *testing.T) {
|
||||
txIntf := any((*AddChainValidatorTx)(nil))
|
||||
_, ok := txIntf.(PermissionlessStaker)
|
||||
require.False(t, ok)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
safemath "github.com/luxfi/math"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
lux "github.com/luxfi/utxo"
|
||||
)
|
||||
|
||||
// TestAddPermissionlessDelegatorTx_RoundTrip exercises the struct-is-wire path
|
||||
// for both a chain (non-primary) and a primary-network delegator: the delta
|
||||
// fields round-trip through Parse. This replaces the deleted linearcodec
|
||||
// golden-byte + JSON serialization tests.
|
||||
func TestAddPermissionlessDelegatorTx_RoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
assetID := ids.GenerateTestID()
|
||||
stakeOuts := []*lux.TransferableOutput{
|
||||
{Asset: lux.Asset{ID: assetID}, Out: &secp256k1fx.TransferOutput{
|
||||
Amt: 3,
|
||||
OutputOwners: secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{ids.GenerateTestShortID()}},
|
||||
}},
|
||||
}
|
||||
rewardsOwner := &secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{ids.GenerateTestShortID()}}
|
||||
vdr := Validator{NodeID: ids.GenerateTestNodeID(), Start: 1, End: 100, Wght: 3}
|
||||
|
||||
// Chain (non-primary) delegator.
|
||||
netTx, err := NewAddPermissionlessDelegatorTx(spendBase(), vdr, ids.GenerateTestID(), stakeOuts, rewardsOwner)
|
||||
require.NoError(err)
|
||||
gotNet := roundTrip(t, netTx).(*AddPermissionlessDelegatorTx)
|
||||
require.Equal(vdr, gotNet.Validator())
|
||||
require.Equal(netTx.Chain(), gotNet.Chain())
|
||||
require.Equal(stakeOuts, gotNet.StakeOuts())
|
||||
require.Equal(rewardsOwner, gotNet.DelegationRewardsOwner())
|
||||
|
||||
// Primary-network delegator: same delta surface, primary chain id.
|
||||
primTx, err := NewAddPermissionlessDelegatorTx(spendBase(), vdr, constants.PrimaryNetworkID, stakeOuts, rewardsOwner)
|
||||
require.NoError(err)
|
||||
gotPrim := roundTrip(t, primTx).(*AddPermissionlessDelegatorTx)
|
||||
require.Equal(constants.PrimaryNetworkID, gotPrim.Chain())
|
||||
require.Equal(vdr, gotPrim.Validator())
|
||||
require.Equal(stakeOuts, gotPrim.StakeOuts())
|
||||
require.Equal(rewardsOwner, gotPrim.DelegationRewardsOwner())
|
||||
}
|
||||
|
||||
func TestAddPermissionlessDelegatorTxSyntacticVerify(t *testing.T) {
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
// Local builders — struct-is-wire has no post-hoc field mutation, so every
|
||||
// (in)valid case is expressed by passing values THROUGH the constructor.
|
||||
validBase := func() *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: rt.NetworkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
invalidBase := func() *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: 0, BlockchainID: rt.ChainID} // wrong networkID
|
||||
}
|
||||
goodOwner := func() *secp256k1fx.OutputOwners {
|
||||
return &secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{ids.GenerateTestShortID()}}
|
||||
}
|
||||
out := func(assetID ids.ID, amt uint64) *lux.TransferableOutput {
|
||||
return &lux.TransferableOutput{Asset: lux.Asset{ID: assetID}, Out: &secp256k1fx.TransferOutput{Amt: amt}}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
build func(t *testing.T) *AddPermissionlessDelegatorTx
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "nil tx",
|
||||
build: func(*testing.T) *AddPermissionlessDelegatorTx { return nil },
|
||||
err: ErrNilTx,
|
||||
},
|
||||
{
|
||||
name: "no provided stake",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
tx, err := NewAddPermissionlessDelegatorTx(validBase(), Validator{NodeID: ids.GenerateTestNodeID()}, ids.GenerateTestID(), nil, goodOwner())
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: errNoStake,
|
||||
},
|
||||
{
|
||||
name: "invalid BaseTx",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
tx, err := NewAddPermissionlessDelegatorTx(invalidBase(), Validator{NodeID: ids.GenerateTestNodeID()}, ids.GenerateTestID(), []*lux.TransferableOutput{out(ids.GenerateTestID(), 1)}, goodOwner())
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: lux.ErrWrongNetworkID,
|
||||
},
|
||||
{
|
||||
// Was: fxmock owner returns errCustom. Reproduced with a real
|
||||
// unspendable owner (Threshold > #Addrs) → ErrOutputUnspendable.
|
||||
name: "invalid rewards owner",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
badOwner := &secp256k1fx.OutputOwners{Threshold: 1}
|
||||
tx, err := NewAddPermissionlessDelegatorTx(validBase(), Validator{Wght: 1}, ids.GenerateTestID(), []*lux.TransferableOutput{out(ids.GenerateTestID(), 1)}, badOwner)
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: secp256k1fx.ErrOutputUnspendable,
|
||||
},
|
||||
{
|
||||
// Was: luxmock stake-out Verify errCustom. Reproduced with a real
|
||||
// unspendable stake output → ErrOutputUnspendable.
|
||||
name: "invalid stake output",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
badOut := &lux.TransferableOutput{Asset: lux.Asset{ID: ids.GenerateTestID()}, Out: &secp256k1fx.TransferOutput{Amt: 1, OutputOwners: secp256k1fx.OutputOwners{Threshold: 1}}}
|
||||
tx, err := NewAddPermissionlessDelegatorTx(validBase(), Validator{Wght: 1}, ids.GenerateTestID(), []*lux.TransferableOutput{badOut}, goodOwner())
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: secp256k1fx.ErrOutputUnspendable,
|
||||
},
|
||||
{
|
||||
name: "multiple staked assets",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
tx, err := NewAddPermissionlessDelegatorTx(validBase(), Validator{Wght: 1}, ids.GenerateTestID(), []*lux.TransferableOutput{out(ids.GenerateTestID(), 1), out(ids.GenerateTestID(), 1)}, goodOwner())
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: errMultipleStakedAssets,
|
||||
},
|
||||
{
|
||||
name: "stake not sorted",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
assetID := ids.GenerateTestID()
|
||||
tx, err := NewAddPermissionlessDelegatorTx(validBase(), Validator{Wght: 1}, ids.GenerateTestID(), []*lux.TransferableOutput{out(assetID, 2), out(assetID, 1)}, goodOwner())
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: errOutputsNotSorted,
|
||||
},
|
||||
{
|
||||
name: "stake overflow",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
assetID := ids.GenerateTestID()
|
||||
tx, err := NewAddPermissionlessDelegatorTx(validBase(), Validator{NodeID: ids.GenerateTestNodeID(), Wght: 1}, ids.GenerateTestID(), []*lux.TransferableOutput{out(assetID, math.MaxUint64), out(assetID, 2)}, goodOwner())
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: safemath.ErrOverflow,
|
||||
},
|
||||
{
|
||||
name: "weight mismatch",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
assetID := ids.GenerateTestID()
|
||||
tx, err := NewAddPermissionlessDelegatorTx(validBase(), Validator{Wght: 1}, ids.GenerateTestID(), []*lux.TransferableOutput{out(assetID, 1), out(assetID, 1)}, goodOwner())
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: errDelegatorWeightMismatch,
|
||||
},
|
||||
{
|
||||
name: "valid net validator",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
assetID := ids.GenerateTestID()
|
||||
tx, err := NewAddPermissionlessDelegatorTx(validBase(), Validator{Wght: 2}, ids.GenerateTestID(), []*lux.TransferableOutput{out(assetID, 1), out(assetID, 1)}, goodOwner())
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "valid primary network validator",
|
||||
build: func(t *testing.T) *AddPermissionlessDelegatorTx {
|
||||
assetID := ids.GenerateTestID()
|
||||
tx, err := NewAddPermissionlessDelegatorTx(validBase(), Validator{Wght: 2}, constants.PrimaryNetworkID, []*lux.TransferableOutput{out(assetID, 1), out(assetID, 1)}, goodOwner())
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.ErrorIs(t, tt.build(t).SyntacticVerify(rt), tt.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPermissionlessDelegatorTxNotValidatorTx(t *testing.T) {
|
||||
txIntf := any((*AddPermissionlessDelegatorTx)(nil))
|
||||
_, ok := txIntf.(ValidatorTx)
|
||||
require.False(t, ok)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/vms/platformvm/reward"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
)
|
||||
|
||||
// TestAddValidatorTxSyntacticVerify pins the AddValidatorTx invariants. Struct-
|
||||
// is-wire has no post-hoc field mutation, so every (in)valid case is expressed
|
||||
// by passing values THROUGH the NewAddValidatorTx constructor.
|
||||
func TestAddValidatorTxSyntacticVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
weight := uint64(2022)
|
||||
goodOwner := func() *secp256k1fx.OutputOwners {
|
||||
return &secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{preFundedKeys[0].Address()}}
|
||||
}
|
||||
validator := func() Validator {
|
||||
return Validator{NodeID: ids.GenerateTestNodeID(), Start: 0, End: 3600, Wght: weight}
|
||||
}
|
||||
stakeOut := func(assetID ids.ID, owner *secp256k1fx.OutputOwners) []*lux.TransferableOutput {
|
||||
return []*lux.TransferableOutput{{
|
||||
Asset: lux.Asset{ID: assetID},
|
||||
Out: &secp256k1fx.TransferOutput{Amt: weight, OutputOwners: *owner},
|
||||
}}
|
||||
}
|
||||
base := func(networkID uint32) *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: networkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
|
||||
// Case: signed tx is nil
|
||||
var stx *Tx
|
||||
require.ErrorIs(stx.SyntacticVerify(rt), ErrNilSignedTx)
|
||||
|
||||
// Case: unsigned tx is nil
|
||||
var nilTx *AddValidatorTx
|
||||
require.ErrorIs(nilTx.SyntacticVerify(rt), ErrNilTx)
|
||||
|
||||
// Case: valid tx
|
||||
valid, err := NewAddValidatorTx(base(rt.NetworkID), validator(), stakeOut(rt.UTXOAssetID, goodOwner()), goodOwner(), reward.PercentDenominator)
|
||||
require.NoError(err)
|
||||
require.NoError(valid.SyntacticVerify(rt))
|
||||
|
||||
// Case: wrong network ID
|
||||
wrongNet, err := NewAddValidatorTx(base(rt.NetworkID+1), validator(), stakeOut(rt.UTXOAssetID, goodOwner()), goodOwner(), reward.PercentDenominator)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(wrongNet.SyntacticVerify(rt), lux.ErrWrongNetworkID)
|
||||
|
||||
// Case: stake owner has no addresses (unspendable stake output)
|
||||
stakeNoAddr, err := NewAddValidatorTx(base(rt.NetworkID), validator(), stakeOut(rt.UTXOAssetID, &secp256k1fx.OutputOwners{Threshold: 1}), goodOwner(), reward.PercentDenominator)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(stakeNoAddr.SyntacticVerify(rt), secp256k1fx.ErrOutputUnspendable)
|
||||
|
||||
// Case: rewards owner has no addresses (unspendable rewards owner)
|
||||
rewardsNoAddr, err := NewAddValidatorTx(base(rt.NetworkID), validator(), stakeOut(rt.UTXOAssetID, goodOwner()), &secp256k1fx.OutputOwners{Threshold: 1}, reward.PercentDenominator)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(rewardsNoAddr.SyntacticVerify(rt), secp256k1fx.ErrOutputUnspendable)
|
||||
|
||||
// Case: too many shares (1 more than max)
|
||||
tooManyShares, err := NewAddValidatorTx(base(rt.NetworkID), validator(), stakeOut(rt.UTXOAssetID, goodOwner()), goodOwner(), reward.PercentDenominator+1)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(tooManyShares.SyntacticVerify(rt), errTooManyShares)
|
||||
}
|
||||
|
||||
func TestAddValidatorTxSyntacticVerifyNotLUX(t *testing.T) {
|
||||
require := require.New(t)
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
owner := &secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{preFundedKeys[0].Address()}}
|
||||
weight := uint64(2022)
|
||||
validator := Validator{NodeID: ids.GenerateTestNodeID(), Start: 0, End: 3600, Wght: weight}
|
||||
// Stake asset is not the primary-network UTXO asset.
|
||||
stakeOuts := []*lux.TransferableOutput{{
|
||||
Asset: lux.Asset{ID: ids.GenerateTestID()},
|
||||
Out: &secp256k1fx.TransferOutput{Amt: weight, OutputOwners: *owner},
|
||||
}}
|
||||
|
||||
u, err := NewAddValidatorTx(&lux.BaseTx{NetworkID: rt.NetworkID, BlockchainID: rt.ChainID}, validator, stakeOuts, owner, reward.PercentDenominator)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(u.SyntacticVerify(rt), errStakeMustBeLUX)
|
||||
}
|
||||
|
||||
// NOTE: the codec-era TestAddValidatorTxNotDelegatorTx (asserting
|
||||
// *AddValidatorTx does NOT satisfy DelegatorTx) is intentionally dropped. It
|
||||
// exercised deleted behavior: under struct-is-wire, RewardsOwner changed from a
|
||||
// struct FIELD to an accessor METHOD, and RewardsOwner() is the only member
|
||||
// DelegatorTx needs beyond what AddValidatorTx already exposes (UnsignedTx +
|
||||
// PermissionlessStaker). So *AddValidatorTx now structurally satisfies both
|
||||
// ValidatorTx and DelegatorTx. This is harmless: the sole production dispatch
|
||||
// (service.go getStakerAttributes) type-switches `case ValidatorTx` BEFORE
|
||||
// `case DelegatorTx`, and Go evaluates cases in order, so an AddValidatorTx is
|
||||
// always routed through the ValidatorTx branch (correct rewards/shares).
|
||||
|
||||
// TestAddValidatorTxSyntacticVerify_ArbitraryPrimaryNetworkIDs asserts that
|
||||
// AddValidatorTx — the canonical post-LP-018 add-validator-to-a-network tx —
|
||||
// accepts arbitrary primary networkIDs, not just the Lux primary values
|
||||
// (1/2/3/1337). A sovereign L1 IS a primary network at its own networkID;
|
||||
// downstream consumers may operate primary networks at any uint32 they choose.
|
||||
// The tx body has no per-chain "Chain" field, so SyntacticVerify must succeed
|
||||
// against any valid primary networkID.
|
||||
func TestAddValidatorTxSyntacticVerify_ArbitraryPrimaryNetworkIDs(t *testing.T) {
|
||||
// Lux primaries (1/2/3/1337) plus four arbitrary synthetic IDs covering
|
||||
// low / mid / high uint32 ranges, to demonstrate the tx is networkID-
|
||||
// agnostic without baking any downstream value into this upstream test.
|
||||
primaryNetworkIDs := []uint32{
|
||||
1, // Lux mainnet
|
||||
2, // Lux testnet
|
||||
3, // Lux local
|
||||
1337, // Lux dev/localnet
|
||||
9001,
|
||||
123456,
|
||||
7_777_777,
|
||||
4_000_000_000,
|
||||
}
|
||||
|
||||
for _, networkID := range primaryNetworkIDs {
|
||||
t.Run(formatPrimaryNetworkID(networkID), func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
rt.NetworkID = networkID // operate the primary network at an arbitrary ID
|
||||
|
||||
weight := uint64(2026)
|
||||
owner := &secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{preFundedKeys[0].Address()}}
|
||||
validator := Validator{NodeID: ids.GenerateTestNodeID(), Start: 0, End: 3600, Wght: weight}
|
||||
stakeOuts := []*lux.TransferableOutput{{
|
||||
Asset: lux.Asset{ID: rt.UTXOAssetID},
|
||||
Out: &secp256k1fx.TransferOutput{Amt: weight, OutputOwners: *owner},
|
||||
}}
|
||||
|
||||
u, err := NewAddValidatorTx(&lux.BaseTx{NetworkID: rt.NetworkID, BlockchainID: rt.ChainID}, validator, stakeOuts, owner, reward.PercentDenominator)
|
||||
require.NoError(err)
|
||||
require.NoError(u.SyntacticVerify(rt))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func formatPrimaryNetworkID(id uint32) string {
|
||||
switch id {
|
||||
case 1:
|
||||
return "lux-mainnet-1"
|
||||
case 2:
|
||||
return "lux-testnet-2"
|
||||
case 3:
|
||||
return "lux-local-3"
|
||||
case 1337:
|
||||
return "lux-dev-1337"
|
||||
default:
|
||||
return fmt.Sprintf("primary-networkID-%d", id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
)
|
||||
|
||||
// TestUnsignedCreateChainTxVerify pins the CreateChainTx syntactic invariants.
|
||||
// Struct-is-wire has no post-hoc field mutation, so every invalid case is built
|
||||
// THROUGH the NewCreateChainTx constructor with the offending value baked in.
|
||||
func TestUnsignedCreateChainTxVerify(t *testing.T) {
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
validChain := ids.GenerateTestID()
|
||||
auth := &secp256k1fx.Input{SigIndices: []uint32{0, 1}}
|
||||
base := func() *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: rt.NetworkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
build func(t *testing.T) *CreateChainTx
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
description: "tx is nil",
|
||||
build: func(*testing.T) *CreateChainTx { return nil },
|
||||
expectedErr: ErrNilTx,
|
||||
},
|
||||
{
|
||||
description: "vm ID is empty",
|
||||
build: func(t *testing.T) *CreateChainTx {
|
||||
tx, err := NewCreateChainTx(base(), validChain, "yeet", ids.Empty, nil, nil, auth)
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
expectedErr: errInvalidVMID,
|
||||
},
|
||||
{
|
||||
description: "chain ID is primary network ID",
|
||||
build: func(t *testing.T) *CreateChainTx {
|
||||
tx, err := NewCreateChainTx(base(), constants.PrimaryNetworkID, "yeet", constants.XVMID, nil, nil, auth)
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
expectedErr: ErrCantValidatePrimaryNetwork,
|
||||
},
|
||||
{
|
||||
description: "chain name is too long",
|
||||
build: func(t *testing.T) *CreateChainTx {
|
||||
tx, err := NewCreateChainTx(base(), validChain, string(make([]byte, MaxNameLen+1)), constants.XVMID, nil, nil, auth)
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
expectedErr: errNameTooLong,
|
||||
},
|
||||
{
|
||||
description: "chain name has invalid character",
|
||||
build: func(t *testing.T) *CreateChainTx {
|
||||
tx, err := NewCreateChainTx(base(), validChain, "⌘", constants.XVMID, nil, nil, auth)
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
expectedErr: errIllegalNameCharacter,
|
||||
},
|
||||
{
|
||||
description: "genesis data is too long",
|
||||
build: func(t *testing.T) *CreateChainTx {
|
||||
tx, err := NewCreateChainTx(base(), validChain, "yeet", constants.XVMID, nil, make([]byte, MaxGenesisLen+1), auth)
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
},
|
||||
expectedErr: errGenesisTooLong,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
require.ErrorIs(t, test.build(t).SyntacticVerify(rt), test.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/ids"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
)
|
||||
|
||||
// TestDisableL1ValidatorTxRoundTrip builds the tx through its constructor,
|
||||
// round-trips it through the struct-is-wire path, and confirms the delta fields
|
||||
// (ValidationID, DisableAuth) and the spending envelope survive encode→decode.
|
||||
func TestDisableL1ValidatorTxRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
base := spendBase()
|
||||
validationID := ids.GenerateTestID()
|
||||
auth := &secp256k1fx.Input{SigIndices: []uint32{9}}
|
||||
|
||||
in, err := NewDisableL1ValidatorTx(base, validationID, auth)
|
||||
require.NoError(err)
|
||||
|
||||
got := roundTrip(t, in).(*DisableL1ValidatorTx)
|
||||
require.Equal(validationID, got.ValidationID())
|
||||
require.Equal(auth, got.DisableAuth())
|
||||
require.Equal(base.NetworkID, got.NetworkID())
|
||||
require.Equal(base.BlockchainID, got.BlockchainID())
|
||||
require.Equal([]byte(base.Memo), got.Memo())
|
||||
}
|
||||
|
||||
func TestDisableL1ValidatorTxSyntacticVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
validationID := ids.GenerateTestID()
|
||||
base := func(networkID uint32) *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: networkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
|
||||
// Case: nil tx
|
||||
var nilTx *DisableL1ValidatorTx
|
||||
require.ErrorIs(nilTx.SyntacticVerify(rt), ErrNilTx)
|
||||
|
||||
// Case: invalid BaseTx (wrong network ID)
|
||||
wrongNet, err := NewDisableL1ValidatorTx(base(rt.NetworkID+1), validationID, &secp256k1fx.Input{})
|
||||
require.NoError(err)
|
||||
require.ErrorIs(wrongNet.SyntacticVerify(rt), lux.ErrWrongNetworkID)
|
||||
|
||||
// Case: invalid disable auth (sig indices not sorted and unique)
|
||||
badAuth, err := NewDisableL1ValidatorTx(base(rt.NetworkID), validationID, &secp256k1fx.Input{SigIndices: []uint32{1, 0}})
|
||||
require.NoError(err)
|
||||
require.ErrorIs(badAuth.SyntacticVerify(rt), secp256k1fx.ErrInputIndicesNotSortedUnique)
|
||||
|
||||
// Case: passes verification
|
||||
valid, err := NewDisableL1ValidatorTx(base(rt.NetworkID), validationID, &secp256k1fx.Input{})
|
||||
require.NoError(err)
|
||||
require.NoError(valid.SyntacticVerify(rt))
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/ids"
|
||||
lux "github.com/luxfi/utxo"
|
||||
)
|
||||
|
||||
// TestIncreaseL1ValidatorBalanceTxRoundTrip builds the tx through its
|
||||
// constructor, round-trips it, and confirms the delta fields (ValidationID,
|
||||
// Balance) and the spending envelope survive encode→decode.
|
||||
func TestIncreaseL1ValidatorBalanceTxRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
base := spendBase()
|
||||
validationID := ids.GenerateTestID()
|
||||
const balance uint64 = 0xfedcba9876543210
|
||||
|
||||
in, err := NewIncreaseL1ValidatorBalanceTx(base, validationID, balance)
|
||||
require.NoError(err)
|
||||
|
||||
got := roundTrip(t, in).(*IncreaseL1ValidatorBalanceTx)
|
||||
require.Equal(validationID, got.ValidationID())
|
||||
require.Equal(balance, got.Balance())
|
||||
require.Equal(base.NetworkID, got.NetworkID())
|
||||
require.Equal(base.BlockchainID, got.BlockchainID())
|
||||
require.Equal([]byte(base.Memo), got.Memo())
|
||||
}
|
||||
|
||||
func TestIncreaseL1ValidatorBalanceTxSyntacticVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
validationID := ids.GenerateTestID()
|
||||
base := func(networkID uint32) *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: networkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
|
||||
// Case: nil tx
|
||||
var nilTx *IncreaseL1ValidatorBalanceTx
|
||||
require.ErrorIs(nilTx.SyntacticVerify(rt), ErrNilTx)
|
||||
|
||||
// Case: zero balance
|
||||
zeroBal, err := NewIncreaseL1ValidatorBalanceTx(base(rt.NetworkID), validationID, 0)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(zeroBal.SyntacticVerify(rt), ErrZeroBalance)
|
||||
|
||||
// Case: invalid BaseTx (wrong network ID)
|
||||
wrongNet, err := NewIncreaseL1ValidatorBalanceTx(base(rt.NetworkID+1), validationID, 1)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(wrongNet.SyntacticVerify(rt), lux.ErrWrongNetworkID)
|
||||
|
||||
// Case: passes verification
|
||||
valid, err := NewIncreaseL1ValidatorBalanceTx(base(rt.NetworkID), validationID, 1)
|
||||
require.NoError(err)
|
||||
require.NoError(valid.SyntacticVerify(rt))
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/ids"
|
||||
lux "github.com/luxfi/utxo"
|
||||
)
|
||||
|
||||
// TestRegisterL1ValidatorTxRoundTrip builds the tx through its constructor,
|
||||
// round-trips it, and confirms the delta fields (Balance, ProofOfPossession,
|
||||
// Message) and the spending envelope survive encode→decode.
|
||||
func TestRegisterL1ValidatorTxRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
base := spendBase()
|
||||
const balance uint64 = 1_000_000
|
||||
var pop [bls.SignatureLen]byte
|
||||
for i := range pop {
|
||||
pop[i] = byte(i)
|
||||
}
|
||||
message := []byte("message")
|
||||
|
||||
in, err := NewRegisterL1ValidatorTx(base, balance, pop, message)
|
||||
require.NoError(err)
|
||||
|
||||
got := roundTrip(t, in).(*RegisterL1ValidatorTx)
|
||||
require.Equal(balance, got.Balance())
|
||||
require.Equal(pop, got.ProofOfPossession())
|
||||
require.Equal(message, got.Message())
|
||||
require.Equal(base.NetworkID, got.NetworkID())
|
||||
require.Equal(base.BlockchainID, got.BlockchainID())
|
||||
require.Equal([]byte(base.Memo), got.Memo())
|
||||
}
|
||||
|
||||
func TestRegisterL1ValidatorTxSyntacticVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
var pop [bls.SignatureLen]byte
|
||||
message := []byte("message")
|
||||
base := func(networkID uint32) *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: networkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
|
||||
// Case: nil tx
|
||||
var nilTx *RegisterL1ValidatorTx
|
||||
require.ErrorIs(nilTx.SyntacticVerify(rt), ErrNilTx)
|
||||
|
||||
// Case: invalid BaseTx (wrong network ID)
|
||||
wrongNet, err := NewRegisterL1ValidatorTx(base(rt.NetworkID+1), 1, pop, message)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(wrongNet.SyntacticVerify(rt), lux.ErrWrongNetworkID)
|
||||
|
||||
// Case: passes verification
|
||||
valid, err := NewRegisterL1ValidatorTx(base(rt.NetworkID), 1, pop, message)
|
||||
require.NoError(err)
|
||||
require.NoError(valid.SyntacticVerify(rt))
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
)
|
||||
|
||||
// TestRemoveChainValidatorTxRoundTrip builds the tx through its constructor,
|
||||
// round-trips it, and confirms the delta fields (NodeID, Chain, ChainAuth) and
|
||||
// the spending envelope survive encode→decode.
|
||||
func TestRemoveChainValidatorTxRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
base := spendBase()
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
chain := ids.GenerateTestID()
|
||||
auth := &secp256k1fx.Input{SigIndices: []uint32{3}}
|
||||
|
||||
in, err := NewRemoveChainValidatorTx(base, nodeID, chain, auth)
|
||||
require.NoError(err)
|
||||
|
||||
got := roundTrip(t, in).(*RemoveChainValidatorTx)
|
||||
require.Equal(nodeID, got.NodeID())
|
||||
require.Equal(chain, got.Chain())
|
||||
require.Equal(auth, got.ChainAuth())
|
||||
require.Equal(base.NetworkID, got.NetworkID())
|
||||
require.Equal(base.BlockchainID, got.BlockchainID())
|
||||
require.Equal([]byte(base.Memo), got.Memo())
|
||||
}
|
||||
|
||||
func TestRemoveChainValidatorTxSyntacticVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
base := func(networkID uint32) *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: networkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
|
||||
// Case: nil tx
|
||||
var nilTx *RemoveChainValidatorTx
|
||||
require.ErrorIs(nilTx.SyntacticVerify(rt), ErrNilTx)
|
||||
|
||||
// Case: invalid BaseTx (wrong network ID); Chain and NodeID are set so we
|
||||
// don't error on those checks first.
|
||||
wrongNet, err := NewRemoveChainValidatorTx(base(rt.NetworkID+1), nodeID, ids.GenerateTestID(), &secp256k1fx.Input{})
|
||||
require.NoError(err)
|
||||
require.ErrorIs(wrongNet.SyntacticVerify(rt), lux.ErrWrongNetworkID)
|
||||
|
||||
// Case: can't remove a primary network validator
|
||||
primary, err := NewRemoveChainValidatorTx(base(rt.NetworkID), nodeID, constants.PrimaryNetworkID, &secp256k1fx.Input{})
|
||||
require.NoError(err)
|
||||
require.ErrorIs(primary.SyntacticVerify(rt), ErrRemovePrimaryNetworkValidator)
|
||||
|
||||
// Case: invalid chain auth (sig indices not sorted and unique)
|
||||
badAuth, err := NewRemoveChainValidatorTx(base(rt.NetworkID), nodeID, ids.GenerateTestID(), &secp256k1fx.Input{SigIndices: []uint32{1, 0}})
|
||||
require.NoError(err)
|
||||
require.ErrorIs(badAuth.SyntacticVerify(rt), secp256k1fx.ErrInputIndicesNotSortedUnique)
|
||||
|
||||
// Case: passes verification
|
||||
valid, err := NewRemoveChainValidatorTx(base(rt.NetworkID), nodeID, ids.GenerateTestID(), &secp256k1fx.Input{})
|
||||
require.NoError(err)
|
||||
require.NoError(valid.SyntacticVerify(rt))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/ids"
|
||||
lux "github.com/luxfi/utxo"
|
||||
)
|
||||
|
||||
// TestSetL1ValidatorWeightTxRoundTrip builds the tx through its constructor,
|
||||
// round-trips it, and confirms the delta field (Message) and the spending
|
||||
// envelope survive encode→decode.
|
||||
func TestSetL1ValidatorWeightTxRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
base := spendBase()
|
||||
message := []byte("message")
|
||||
|
||||
in, err := NewSetL1ValidatorWeightTx(base, message)
|
||||
require.NoError(err)
|
||||
|
||||
got := roundTrip(t, in).(*SetL1ValidatorWeightTx)
|
||||
require.Equal(message, got.Message())
|
||||
require.Equal(base.NetworkID, got.NetworkID())
|
||||
require.Equal(base.BlockchainID, got.BlockchainID())
|
||||
require.Equal([]byte(base.Memo), got.Memo())
|
||||
}
|
||||
|
||||
func TestSetL1ValidatorWeightTxSyntacticVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
message := []byte("message")
|
||||
base := func(networkID uint32) *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: networkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
|
||||
// Case: nil tx
|
||||
var nilTx *SetL1ValidatorWeightTx
|
||||
require.ErrorIs(nilTx.SyntacticVerify(rt), ErrNilTx)
|
||||
|
||||
// Case: invalid BaseTx (wrong network ID)
|
||||
wrongNet, err := NewSetL1ValidatorWeightTx(base(rt.NetworkID+1), message)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(wrongNet.SyntacticVerify(rt), lux.ErrWrongNetworkID)
|
||||
|
||||
// Case: passes verification
|
||||
valid, err := NewSetL1ValidatorWeightTx(base(rt.NetworkID), message)
|
||||
require.NoError(err)
|
||||
require.NoError(valid.SyntacticVerify(rt))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestStakerDispatchSafety pins the interface-satisfaction invariant that the
|
||||
// staker type-switches in service.go (getStakerAttributes) and
|
||||
// executor/proposal_tx_executor.go depend on.
|
||||
//
|
||||
// Struct-is-wire turned RewardsOwner from a struct field into an accessor
|
||||
// method, so *AddValidatorTx now structurally satisfies BOTH ValidatorTx and
|
||||
// DelegatorTx. That is SAFE only because:
|
||||
//
|
||||
// 1. every dispatch site checks `case txs.ValidatorTx` BEFORE
|
||||
// `case txs.DelegatorTx`, so an AddValidatorTx routes as a validator; and
|
||||
// 2. *AddDelegatorTx does NOT satisfy ValidatorTx (it lacks
|
||||
// ValidationRewardsOwner() and Shares()), so a delegator can never
|
||||
// mis-route into the validator branch.
|
||||
//
|
||||
// If a future edit gives AddDelegatorTx those methods, or reorders a switch,
|
||||
// this test fails loudly instead of silently mis-paying rewards.
|
||||
func TestStakerDispatchSafety(t *testing.T) {
|
||||
var (
|
||||
vdr any = (*AddValidatorTx)(nil)
|
||||
del any = (*AddDelegatorTx)(nil)
|
||||
)
|
||||
|
||||
if _, ok := vdr.(ValidatorTx); !ok {
|
||||
t.Fatal("*AddValidatorTx must satisfy ValidatorTx")
|
||||
}
|
||||
if _, ok := del.(DelegatorTx); !ok {
|
||||
t.Fatal("*AddDelegatorTx must satisfy DelegatorTx")
|
||||
}
|
||||
// The load-bearing safety property: a delegator must NOT look like a
|
||||
// validator, because every dispatch checks ValidatorTx first.
|
||||
if _, ok := del.(ValidatorTx); ok {
|
||||
t.Fatal("*AddDelegatorTx must NOT satisfy ValidatorTx — delegators would mis-route to the validator branch")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
lux "github.com/luxfi/utxo"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
)
|
||||
|
||||
// TestTransferChainOwnershipTxRoundTrip builds the tx through its constructor,
|
||||
// round-trips it, and confirms the delta fields (Chain, ChainAuth, Owner) and
|
||||
// the spending envelope survive encode→decode.
|
||||
func TestTransferChainOwnershipTxRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
base := spendBase()
|
||||
chain := ids.GenerateTestID()
|
||||
auth := &secp256k1fx.Input{SigIndices: []uint32{3}}
|
||||
owner := &secp256k1fx.OutputOwners{Locktime: 876543210, Threshold: 1, Addrs: []ids.ShortID{ids.GenerateTestShortID()}}
|
||||
|
||||
in, err := NewTransferChainOwnershipTx(base, chain, auth, owner)
|
||||
require.NoError(err)
|
||||
|
||||
got := roundTrip(t, in).(*TransferChainOwnershipTx)
|
||||
require.Equal(chain, got.Chain())
|
||||
require.Equal(auth, got.ChainAuth())
|
||||
require.Equal(owner, got.Owner())
|
||||
require.Equal(base.NetworkID, got.NetworkID())
|
||||
require.Equal(base.BlockchainID, got.BlockchainID())
|
||||
require.Equal([]byte(base.Memo), got.Memo())
|
||||
}
|
||||
|
||||
func TestTransferChainOwnershipTxSyntacticVerify(t *testing.T) {
|
||||
require := require.New(t)
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
owner := &secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{ids.GenerateTestShortID()}}
|
||||
base := func(networkID uint32) *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: networkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
|
||||
// Case: nil tx
|
||||
var nilTx *TransferChainOwnershipTx
|
||||
require.ErrorIs(nilTx.SyntacticVerify(rt), ErrNilTx)
|
||||
|
||||
// Case: invalid BaseTx (wrong network ID); Chain is set so we don't error on
|
||||
// that check first.
|
||||
wrongNet, err := NewTransferChainOwnershipTx(base(rt.NetworkID+1), ids.GenerateTestID(), &secp256k1fx.Input{}, owner)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(wrongNet.SyntacticVerify(rt), lux.ErrWrongNetworkID)
|
||||
|
||||
// Case: cannot transfer ownership of a permissionless chain
|
||||
primary, err := NewTransferChainOwnershipTx(base(rt.NetworkID), constants.PrimaryNetworkID, &secp256k1fx.Input{}, owner)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(primary.SyntacticVerify(rt), ErrTransferPermissionlessChain)
|
||||
|
||||
// Case: invalid chain auth (sig indices not sorted and unique)
|
||||
badAuth, err := NewTransferChainOwnershipTx(base(rt.NetworkID), ids.GenerateTestID(), &secp256k1fx.Input{SigIndices: []uint32{1, 0}}, owner)
|
||||
require.NoError(err)
|
||||
require.ErrorIs(badAuth.SyntacticVerify(rt), secp256k1fx.ErrInputIndicesNotSortedUnique)
|
||||
|
||||
// Case: passes verification
|
||||
valid, err := NewTransferChainOwnershipTx(base(rt.NetworkID), ids.GenerateTestID(), &secp256k1fx.Input{}, owner)
|
||||
require.NoError(err)
|
||||
require.NoError(valid.SyntacticVerify(rt))
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package txs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
|
||||
consensustest "github.com/luxfi/consensus/test/helpers"
|
||||
"github.com/luxfi/node/vms/platformvm/reward"
|
||||
lux "github.com/luxfi/utxo"
|
||||
)
|
||||
|
||||
// TestTransformChainTx_RoundTrip exercises the struct-is-wire path: every delta
|
||||
// field (chain, asset, the supply/rate/stake/duration knobs, the weight factor
|
||||
// and uptime requirement, and the chain auth) round-trips through Parse. This
|
||||
// replaces the deleted linearcodec golden-byte + JSON serialization tests.
|
||||
func TestTransformChainTx_RoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
chain := ids.GenerateTestID()
|
||||
assetID := ids.GenerateTestID()
|
||||
auth := &secp256k1fx.Input{SigIndices: []uint32{0, 2, 5}}
|
||||
in, err := NewTransformChainTx(
|
||||
spendBase(), chain, assetID,
|
||||
1_000_000, // initial supply
|
||||
2_000_000, // maximum supply
|
||||
10_000, // min consumption rate
|
||||
90_000, // max consumption rate
|
||||
100, // min validator stake
|
||||
5_000, // max validator stake
|
||||
60, // min stake duration
|
||||
3_600, // max stake duration
|
||||
20_000, // min delegation fee
|
||||
1, // min delegator stake
|
||||
5, // max validator weight factor
|
||||
80_000, // uptime requirement
|
||||
auth,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
got := roundTrip(t, in).(*TransformChainTx)
|
||||
require.Equal(chain, got.Chain())
|
||||
require.Equal(assetID, got.AssetID())
|
||||
require.EqualValues(1_000_000, got.InitialSupply())
|
||||
require.EqualValues(2_000_000, got.MaximumSupply())
|
||||
require.EqualValues(10_000, got.MinConsumptionRate())
|
||||
require.EqualValues(90_000, got.MaxConsumptionRate())
|
||||
require.EqualValues(100, got.MinValidatorStake())
|
||||
require.EqualValues(5_000, got.MaxValidatorStake())
|
||||
require.EqualValues(60, got.MinStakeDuration())
|
||||
require.EqualValues(3_600, got.MaxStakeDuration())
|
||||
require.EqualValues(20_000, got.MinDelegationFee())
|
||||
require.EqualValues(1, got.MinDelegatorStake())
|
||||
require.EqualValues(5, got.MaxValidatorWeightFactor())
|
||||
require.EqualValues(80_000, got.UptimeRequirement())
|
||||
|
||||
gotAuth, ok := got.ChainAuth().(*secp256k1fx.Input)
|
||||
require.True(ok)
|
||||
require.Equal(auth.SigIndices, gotAuth.SigIndices)
|
||||
}
|
||||
|
||||
// transformParams mirrors the NewTransformChainTx arguments so each error-path
|
||||
// case can start from a fully-valid baseline and perturb exactly one field —
|
||||
// struct-is-wire has no post-hoc mutation, so the bad value goes THROUGH the
|
||||
// constructor and the SAME sentinel is asserted from SyntacticVerify.
|
||||
type transformParams struct {
|
||||
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 *secp256k1fx.Input
|
||||
}
|
||||
|
||||
func TestTransformChainTxSyntacticVerify(t *testing.T) {
|
||||
rt := consensustest.Runtime(t, ids.GenerateTestID())
|
||||
|
||||
validBase := func() *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: rt.NetworkID, BlockchainID: rt.ChainID}
|
||||
}
|
||||
invalidBase := func() *lux.BaseTx {
|
||||
return &lux.BaseTx{NetworkID: 0, BlockchainID: rt.ChainID} // wrong networkID
|
||||
}
|
||||
// A fully-valid baseline: every field passes its own SyntacticVerify check.
|
||||
valid := func() transformParams {
|
||||
return transformParams{
|
||||
base: validBase(),
|
||||
chain: ids.GenerateTestID(),
|
||||
assetID: ids.GenerateTestID(),
|
||||
initialSupply: 10,
|
||||
maximumSupply: 10,
|
||||
minConsumptionRate: 0,
|
||||
maxConsumptionRate: reward.PercentDenominator,
|
||||
minValidatorStake: 2,
|
||||
maxValidatorStake: 10,
|
||||
minStakeDuration: 1,
|
||||
maxStakeDuration: 2,
|
||||
minDelegationFee: reward.PercentDenominator,
|
||||
minDelegatorStake: 1,
|
||||
maxValidatorWeightFactor: 1,
|
||||
uptimeRequirement: reward.PercentDenominator,
|
||||
chainAuth: &secp256k1fx.Input{SigIndices: []uint32{0}},
|
||||
}
|
||||
}
|
||||
mk := func(t *testing.T, p transformParams) *TransformChainTx {
|
||||
tx, err := NewTransformChainTx(
|
||||
p.base, p.chain, p.assetID,
|
||||
p.initialSupply, p.maximumSupply,
|
||||
p.minConsumptionRate, p.maxConsumptionRate,
|
||||
p.minValidatorStake, p.maxValidatorStake,
|
||||
p.minStakeDuration, p.maxStakeDuration,
|
||||
p.minDelegationFee, p.minDelegatorStake,
|
||||
p.maxValidatorWeightFactor, p.uptimeRequirement,
|
||||
p.chainAuth,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return tx
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
build func(t *testing.T) *TransformChainTx
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "nil tx",
|
||||
build: func(*testing.T) *TransformChainTx { return nil },
|
||||
err: ErrNilTx,
|
||||
},
|
||||
{
|
||||
name: "invalid netID",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.chain = constants.PrimaryNetworkID
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errCantTransformPrimaryNetwork,
|
||||
},
|
||||
{
|
||||
name: "empty assetID",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.assetID = ids.Empty
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errEmptyAssetID,
|
||||
},
|
||||
{
|
||||
name: "LUX assetID",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.assetID = rt.UTXOAssetID
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errAssetIDCantBeLUX,
|
||||
},
|
||||
{
|
||||
name: "initialSupply == 0",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.initialSupply = 0
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errInitialSupplyZero,
|
||||
},
|
||||
{
|
||||
name: "initialSupply > maximumSupply",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.initialSupply = 2
|
||||
p.maximumSupply = 1
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errInitialSupplyGreaterThanMaxSupply,
|
||||
},
|
||||
{
|
||||
name: "minConsumptionRate > maxConsumptionRate",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.minConsumptionRate = 2
|
||||
p.maxConsumptionRate = 1
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMinConsumptionRateTooLarge,
|
||||
},
|
||||
{
|
||||
name: "maxConsumptionRate > 100%",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.minConsumptionRate = 0
|
||||
p.maxConsumptionRate = reward.PercentDenominator + 1
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMaxConsumptionRateTooLarge,
|
||||
},
|
||||
{
|
||||
name: "minValidatorStake == 0",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.minValidatorStake = 0
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMinValidatorStakeZero,
|
||||
},
|
||||
{
|
||||
name: "minValidatorStake > initialSupply",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.initialSupply = 1
|
||||
p.minValidatorStake = 2
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMinValidatorStakeAboveSupply,
|
||||
},
|
||||
{
|
||||
name: "minValidatorStake > maxValidatorStake",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.minValidatorStake = 2
|
||||
p.maxValidatorStake = 1
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMinValidatorStakeAboveMax,
|
||||
},
|
||||
{
|
||||
name: "maxValidatorStake > maximumSupply",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.maxValidatorStake = 11
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMaxValidatorStakeTooLarge,
|
||||
},
|
||||
{
|
||||
name: "minStakeDuration == 0",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.minStakeDuration = 0
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMinStakeDurationZero,
|
||||
},
|
||||
{
|
||||
name: "minStakeDuration > maxStakeDuration",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.minStakeDuration = 2
|
||||
p.maxStakeDuration = 1
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMinStakeDurationTooLarge,
|
||||
},
|
||||
{
|
||||
name: "minDelegationFee > 100%",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.minDelegationFee = reward.PercentDenominator + 1
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMinDelegationFeeTooLarge,
|
||||
},
|
||||
{
|
||||
name: "minDelegatorStake == 0",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.minDelegatorStake = 0
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMinDelegatorStakeZero,
|
||||
},
|
||||
{
|
||||
name: "maxValidatorWeightFactor == 0",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.maxValidatorWeightFactor = 0
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errMaxValidatorWeightFactorZero,
|
||||
},
|
||||
{
|
||||
name: "uptimeRequirement > 100%",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.uptimeRequirement = reward.PercentDenominator + 1
|
||||
return mk(t, p)
|
||||
},
|
||||
err: errUptimeRequirementTooLarge,
|
||||
},
|
||||
{
|
||||
// Was: verifymock chainAuth returns errInvalidNetAuth. Reproduced
|
||||
// with a real *secp256k1fx.Input whose indices are not sorted-unique.
|
||||
name: "invalid chainAuth",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.chainAuth = &secp256k1fx.Input{SigIndices: []uint32{1, 0}}
|
||||
return mk(t, p)
|
||||
},
|
||||
err: secp256k1fx.ErrInputIndicesNotSortedUnique,
|
||||
},
|
||||
{
|
||||
name: "invalid BaseTx",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
p := valid()
|
||||
p.base = invalidBase()
|
||||
return mk(t, p)
|
||||
},
|
||||
err: lux.ErrWrongNetworkID,
|
||||
},
|
||||
{
|
||||
name: "passes verification",
|
||||
build: func(t *testing.T) *TransformChainTx {
|
||||
return mk(t, valid())
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.ErrorIs(t, tt.build(t).SyntacticVerify(rt), tt.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user