Files
node/vms/platformvm/txs/add_validator_test.go
zeekay 7cf87b230b node-unified is the node
The module already called itself github.com/luxfi/node while containing no node
in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and
the Go column of the matrix measured a VM plugin. A repo that fetches the thing
it claims to be has not replaced it, so the Go sources move in — 1515 files,
the daemon and everything under it — and the build reaches outside this module
for nothing.

Rust and C++ sit beside it unchanged and are still measured against it by the
differential. That is the point of one repo: the reference implementation and
the two ports are the same checkout, so parity is a thing you can run rather
than a thing you assert.

Verified: builds, and the closure carries no lux-private — checked at build time
rather than believed. The daemon starts twelve chains on a local network
(P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2026-09-07 16:15:25 -07:00

164 lines
6.7 KiB
Go

// 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 "mainnet-1"
case 2:
return "testnet-2"
case 3:
return "local-3"
case 1337:
return "dev-1337"
default:
return fmt.Sprintf("primary-networkID-%d", id)
}
}