txs+wallet: deprecate AddChainValidatorTx under LP-018 sovereign-L1

Validators validate networks; chains live on networks. AddValidatorTx
is the universal add-validator-to-a-network tx whether the target
network is Lux primary (1/2/3/1337) or any downstream sovereign L1's
own primary at its chosen networkID. AddChainValidatorTx is legacy
and kept for one release cycle of wire/codec compat with pre-LP-018
binaries.

- vms/platformvm/txs/add_chain_validator_tx.go: file-header + struct
  Deprecated: notice pointing to AddValidatorTx.
- vms/platformvm/txs/chain_validator.go: ChainValidator descriptor
  marked Deprecated.
- vms/platformvm/txs/add_validator_test.go: new
  TestAddValidatorTxSyntacticVerify_ArbitraryPrimaryNetworkIDs covers
  Lux primaries (1/2/3/1337) plus four synthetic IDs across the uint32
  range, asserting the tx body has no per-chain field and accepts any
  valid primary networkID.
- wallet/chain/p/wallet/wallet.go + with_options.go: Deprecated on
  IssueAddChainValidatorTx; IssueRemoveChainValidatorTx doc reworded
  to network-centric language.
- wallet/chain/p/builder.go + builder/builder.go: Deprecated on
  NewAddChainValidatorTx interfaces.
- wallet/network/primary/examples/{add-permissioned-chain-validator,
  bootstrap-hanzo, deploy-chains}/main.go: file-header notes that
  these examples exercise the legacy path; new code uses AddValidatorTx.
- node/validator_manager.go: 4 log strings + 1 comment block rewritten
  to talk about network validator (not chain validator).
- vms/platformvm/health.go: error string 'current chain validator of'
  → 'current network validator on'.
This commit is contained in:
Hanzo AI
2026-06-08 16:02:58 -07:00
parent 8e44bfebb2
commit 1486f1ec47
12 changed files with 222 additions and 18 deletions
+10 -7
View File
@@ -109,20 +109,23 @@ func (v *ValidatorManager) Connected(nodeID ids.NodeID, nodeVersion *version.App
)
}
// Also add to ALL tracked chain validator sets so chain consensus
// engines can find validators for their chains. Without this, non-primary
// chains can't gossip blocks because the validator set is empty.
// Also add to ALL tracked network validator sets so each
// network's consensus engine can find validators for the chains
// it hosts. Without this, non-primary networks can't gossip
// blocks because the validator set is empty. Under the
// canonical model, validators validate networks; chains live on
// networks.
for _, networkID := range v.trackedNetworks {
networkTxID := ids.Empty
copy(networkTxID[:], nodeID.Bytes())
if err := v.vdrs.AddStaker(networkID, nodeID, nil, networkTxID, v.weight); err != nil {
v.log.Debug("failed to add chain validator on connect",
v.log.Debug("failed to add network validator on connect",
log.Stringer("nodeID", nodeID),
log.Stringer("networkID", networkID),
log.Reflect("error", err),
)
} else {
v.log.Info("added chain validator on connect (sybil protection disabled)",
v.log.Info("added network validator on connect (sybil protection disabled)",
log.Stringer("nodeID", nodeID),
log.Stringer("networkID", networkID),
)
@@ -173,10 +176,10 @@ func (v *ValidatorManager) Disconnected(nodeID ids.NodeID) {
)
}
// Also remove from all tracked chain validator sets
// Also remove from all tracked network validator sets.
for _, networkID := range v.trackedNetworks {
if err := v.vdrs.RemoveWeight(networkID, nodeID, v.weight); err != nil {
v.log.Debug("failed to remove chain validator on disconnect",
v.log.Debug("failed to remove network validator on disconnect",
log.Stringer("nodeID", nodeID),
log.Stringer("networkID", networkID),
)
+1 -1
View File
@@ -38,7 +38,7 @@ func (vm *VM) HealthCheck(context.Context) (chain.HealthResult, error) {
case database.ErrNotFound:
vm.metrics.SetTimeUntilNetUnstake(chainID, 0)
default:
return chain.HealthResult{}, fmt.Errorf("couldn't get current chain validator of %q: %w", chainID, err)
return chain.HealthResult{}, fmt.Errorf("couldn't get current network validator on %q: %w", chainID, err)
}
}
return chain.HealthResult{Healthy: true, Details: nil}, nil
+24 -1
View File
@@ -1,5 +1,19 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//
// File deprecation notice: this file defines AddChainValidatorTx, the
// legacy per-chain validator registration tx. Under LP-018
// (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.
package txs
@@ -21,7 +35,16 @@ var (
errAddPrimaryNetworkValidator = errors.New("can't add primary network validator with AddChainValidatorTx")
)
// AddChainValidatorTx is an unsigned addChainValidatorTx
// AddChainValidatorTx is the legacy per-chain (legacy: per-subnet)
// validator registration tx.
//
// 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.
type AddChainValidatorTx struct {
// Metadata, inputs and outputs
BaseTx `serialize:"true"`
+121
View File
@@ -252,3 +252,124 @@ func TestAddValidatorTxNotDelegatorTx(t *testing.T) {
_, ok := txIntf.(DelegatorTx)
require.False(t, ok)
}
// 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. This test pins that contract: 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 {
networkID := networkID
t.Run(formatPrimaryNetworkID(networkID), func(t *testing.T) {
require := require.New(t)
clk := mockable.Clock{}
utxoAssetID := ids.GenerateTestID()
nodeID := ids.GenerateTestNodeID()
testChainID := ids.GenerateTestID()
rt := &runtime.Runtime{
NetworkID: networkID,
ChainID: testChainID,
UTXOAssetID: utxoAssetID,
NodeID: nodeID,
}
signers := [][]*secp256k1.PrivateKey{preFundedKeys}
validatorWeight := uint64(2026)
rewardAddress := preFundedKeys[0].Address()
inputs := []*lux.TransferableInput{{
UTXOID: lux.UTXOID{
TxID: ids.ID{'l', 'p', '0', '1', '8'},
OutputIndex: 0,
},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: uint64(5678),
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}}
outputs := []*lux.TransferableOutput{{
Asset: lux.Asset{ID: utxoAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: uint64(1234),
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{preFundedKeys[0].Address()},
},
},
}}
stakes := []*lux.TransferableOutput{{
Asset: lux.Asset{ID: utxoAssetID},
Out: &stakeable.LockOut{
Locktime: uint64(clk.Time().Add(time.Second).Unix()),
TransferableOut: &secp256k1fx.TransferOutput{
Amt: validatorWeight,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{preFundedKeys[0].Address()},
},
},
},
}}
addValidatorTx := &AddValidatorTx{
BaseTx: BaseTx{BaseTx: lux.BaseTx{
NetworkID: rt.NetworkID,
BlockchainID: rt.ChainID,
Ins: inputs,
Outs: outputs,
}},
Validator: Validator{
NodeID: rt.NodeID,
Start: uint64(clk.Time().Unix()),
End: uint64(clk.Time().Add(time.Hour).Unix()),
Wght: validatorWeight,
},
StakeOuts: stakes,
RewardsOwner: &secp256k1fx.OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{rewardAddress},
},
DelegationShares: reward.PercentDenominator,
}
stx, err := NewSigned(addValidatorTx, Codec, signers)
require.NoError(err)
require.NoError(stx.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)
}
}
+9 -1
View File
@@ -8,7 +8,15 @@ import (
"github.com/luxfi/ids"
)
// ChainValidator validates a chain on the Lux network.
// ChainValidator is the legacy per-chain validator descriptor used by
// AddChainValidatorTx. The Chain field is the network ID this validator
// registers under (legacy: subnet ID).
//
// Deprecated: Use Validator with AddValidatorTx. Under LP-018
// sovereign-L1, validators validate networks — not chains. Chains live
// on networks (created via CreateChainTx); validators no longer
// register per-chain. Retained for one release cycle for wire/codec
// compat with pre-LP-018 binaries.
type ChainValidator struct {
Validator `serialize:"true"`
+7 -1
View File
@@ -77,10 +77,16 @@ type Builder interface {
options ...common.Option,
) (*txs.AddValidatorTx, error)
// NewAddChainValidatorTx creates a new validator of a chain.
// NewAddChainValidatorTx builds an unsigned legacy per-chain
// validator registration tx.
//
// - [vdr] specifies all the details of the validation period such as the
// startTime, endTime, sampling weight, nodeID, and netID.
//
// Deprecated: Use NewAddValidatorTx. Under LP-018 sovereign-L1,
// validators join a network — never a chain. A sovereign L1 IS a
// primary network at its own networkID. Retained for one release
// cycle for wire/codec compat with pre-LP-018 binaries.
NewAddChainValidatorTx(
vdr *txs.ChainValidator,
options ...common.Option,
+7 -1
View File
@@ -84,10 +84,16 @@ type Builder interface {
options ...common.Option,
) (*txs.AddValidatorTx, error)
// NewAddChainValidatorTx creates a new validator of a chain.
// NewAddChainValidatorTx builds an unsigned legacy per-chain
// validator registration tx.
//
// - [vdr] specifies all the details of the validation period such as the
// startTime, endTime, sampling weight, nodeID, and netID.
//
// Deprecated: Use NewAddValidatorTx. Under LP-018 sovereign-L1,
// validators join a network — never a chain. A sovereign L1 IS a
// primary network at its own networkID. Retained for one release
// cycle for wire/codec compat with pre-LP-018 binaries.
NewAddChainValidatorTx(
vdr *txs.ChainValidator,
options ...common.Option,
+18 -5
View File
@@ -63,20 +63,31 @@ type Wallet interface {
options ...common.Option,
) (*txs.Tx, error)
// IssueAddChainValidatorTx creates, signs, and issues a new validator of a
// chain.
// IssueAddChainValidatorTx creates, signs, and issues a legacy
// per-chain validator registration tx.
//
// - [vdr] specifies all the details of the validation period such as the
// startTime, endTime, sampling weight, nodeID, and chainID.
//
// Deprecated: Use IssueAddValidatorTx. Under LP-018 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 (Lux
// primaries at 1/2/3/1337; any downstream consumer's primary at
// whatever uint32 it picks). Retained for one release cycle for
// wire/codec compat with pre-LP-018 binaries.
IssueAddChainValidatorTx(
vdr *txs.ChainValidator,
options ...common.Option,
) (*txs.Tx, error)
// IssueRemoveChainValidatorTx creates, signs, and issues a transaction
// that removes a validator of a chain.
// IssueRemoveChainValidatorTx creates, signs, and issues a
// transaction that removes a validator from a network (legacy:
// from a chain's permissioned validator set).
//
// - [nodeID] is the validator being removed from [chainID].
// - [nodeID] is the validator being removed from the network
// referenced by [chainID].
IssueRemoveChainValidatorTx(
nodeID ids.NodeID,
chainID ids.ID,
@@ -365,6 +376,8 @@ func (w *wallet) IssueAddValidatorTx(
return w.IssueUnsignedTx(utx, options...)
}
// Deprecated: Use IssueAddValidatorTx. See the interface declaration
// for the LP-018 migration note.
func (w *wallet) IssueAddChainValidatorTx(
vdr *txs.ChainValidator,
options ...common.Option,
+3 -1
View File
@@ -70,7 +70,9 @@ func (w *withOptions) IssueAddValidatorTx(
)
}
// Removed in regenesis
// Deprecated: Use IssueAddValidatorTx. Under LP-018 sovereign-L1,
// validators join a network — never a chain. See the wallet interface
// declaration for the full migration note.
func (w *withOptions) IssueAddChainValidatorTx(
vdr *txs.ChainValidator,
options ...common.Option,
@@ -1,5 +1,13 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//
// Deprecated example: this binary exercises the legacy
// AddChainValidatorTx flow (validator scoped to a non-primary network
// id). Under LP-018 sovereign-L1, validators join a network — never a
// chain — via AddValidatorTx. A sovereign L1 IS a primary network at
// its own networkID; new code should issue AddValidatorTx against that
// primary network. This example is kept to exercise the deprecated tx
// path for one release cycle.
package main
@@ -6,6 +6,13 @@
// MNEMONIC="..." go run . --uri=http://localhost:19640 --network=testnet \
// --genesis=/Users/z/work/lux/genesis/configs/hanzo-testnet/genesis.json \
// --hrp=test --bip44-idx=5
//
// Deprecated post-chain-create flow: after CreateChainTx, this binary
// adds validators to the created network via the legacy
// IssueAddChainValidatorTx. Under LP-018 sovereign-L1, validators join
// a network via AddValidatorTx — chains live on networks, validators
// do not register per-chain. The legacy call is preserved for one
// release cycle for wire/codec compat with pre-LP-018 binaries.
package main
import (
@@ -5,6 +5,13 @@
// MNEMONIC="word1 word2 ..." go run . --network=mainnet
// go run . --network=mainnet mainnet-key-01
// MNEMONIC="word1 word2 ..." go run . --network=mainnet --chain=zoo
//
// Deprecated post-chain-create flow: after CreateChainTx, this binary
// adds validators to the created network via the legacy
// IssueAddChainValidatorTx. Under LP-018 sovereign-L1, validators join
// a network via AddValidatorTx — chains live on networks, validators
// do not register per-chain. The legacy call is preserved for one
// release cycle for wire/codec compat with pre-LP-018 binaries.
package main
import (