Decomplect P-chain tx set: rip Slash + P-chain CreateAsset/Operation, restore staker ifaces

- SlashValidatorTx ripped (unwired stub; Avalanche has no slashing; consensus
  only detects, never enforces). Type+executor+tests+Visitor surface removed.
- CreateAssetTx + OperationTx ripped from P-CHAIN (X-chain/AVM types; LP-0130:
  asset creation + fx ops are X-chain money-rail, not P staking-rail; no P
  producer). xvm/avm untouched.
- Staker interfaces (StakerTx/ValidatorTx/DelegatorTx/ScheduledStaker) restored
  on the 5 validator types via pure accessors + FxID-on-stake preserved.
- Remaining real complex types: ConvertNetworkToL1Tx + CreateSovereignL1Tx.
This commit is contained in:
zeekay
2026-07-10 10:36:35 -07:00
parent 33e005c869
commit e288b67008
33 changed files with 297 additions and 2422 deletions
-21
View File
@@ -186,13 +186,6 @@ func (m *txMetrics) DisableL1ValidatorTx(*txs.DisableL1ValidatorTx) error {
return nil
}
func (m *txMetrics) SlashValidatorTx(*txs.SlashValidatorTx) error {
m.numTxs.With(metric.Labels{
txLabel: "slash_validator",
}).Inc()
return nil
}
func (m *txMetrics) AddChainValidatorTx(*txs.AddChainValidatorTx) error {
m.numTxs.With(metric.Labels{
txLabel: "add_net_validator",
@@ -227,17 +220,3 @@ func (m *txMetrics) TransferChainOwnershipTx(*txs.TransferChainOwnershipTx) erro
}).Inc()
return nil
}
func (m *txMetrics) CreateAssetTx(*txs.CreateAssetTx) error {
m.numTxs.With(metric.Labels{
txLabel: "create_asset",
}).Inc()
return nil
}
func (m *txMetrics) OperationTx(*txs.OperationTx) error {
m.numTxs.With(metric.Labels{
txLabel: "operation",
}).Inc()
return nil
}
+41 -1
View File
@@ -16,8 +16,10 @@ package txs
import (
"context"
"errors"
"time"
"github.com/luxfi/constants"
bls "github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/runtime"
@@ -26,7 +28,9 @@ import (
)
var (
_ UnsignedTx = (*AddChainValidatorTx)(nil)
_ UnsignedTx = (*AddChainValidatorTx)(nil)
_ StakerTx = (*AddChainValidatorTx)(nil)
_ ScheduledStaker = (*AddChainValidatorTx)(nil)
errAddPrimaryNetworkValidator = errors.New("can't add primary network validator with AddChainValidatorTx")
)
@@ -88,6 +92,42 @@ func (tx *AddChainValidatorTx) ChainAuth() verify.Verifiable {
return readAuth(tx.root(), offACVChainAuth)
}
// ---- Staker / ScheduledStaker interface ----
// ChainID is the network id this validator registers under (legacy
// ChainValidator.ChainID, sourced from the pure Chain() accessor).
func (tx *AddChainValidatorTx) ChainID() ids.ID {
return tx.Chain()
}
func (tx *AddChainValidatorTx) NodeID() ids.NodeID {
return tx.Validator().NodeID
}
func (*AddChainValidatorTx) PublicKey() (*bls.PublicKey, bool, error) {
return nil, false, nil
}
func (tx *AddChainValidatorTx) StartTime() time.Time {
return time.Unix(int64(tx.Validator().Start), 0)
}
func (tx *AddChainValidatorTx) EndTime() time.Time {
return time.Unix(int64(tx.Validator().End), 0)
}
func (tx *AddChainValidatorTx) Weight() uint64 {
return tx.Validator().Wght
}
func (*AddChainValidatorTx) PendingPriority() Priority {
return ChainPermissionedValidatorPendingPriority
}
func (*AddChainValidatorTx) CurrentPriority() Priority {
return ChainPermissionedValidatorCurrentPriority
}
// SyntacticVerify returns nil iff [tx] is valid.
func (tx *AddChainValidatorTx) SyntacticVerify(rt *runtime.Runtime) error {
if tx == nil {
+56 -1
View File
@@ -7,17 +7,24 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/luxfi/constants"
bls "github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
safemath "github.com/luxfi/math"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/platformvm/fx"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/zap"
)
var (
_ UnsignedTx = (*AddDelegatorTx)(nil)
_ UnsignedTx = (*AddDelegatorTx)(nil)
_ DelegatorTx = (*AddDelegatorTx)(nil)
_ ScheduledStaker = (*AddDelegatorTx)(nil)
errDelegatorWeightMismatch = errors.New("delegator weight is not equal to total stake weight")
errStakeMustBeLUX = errors.New("stake must be LUX")
@@ -83,6 +90,54 @@ func (tx *AddDelegatorTx) DelegationRewardsOwner() fx.Owner {
return readOwner(tx.root(), offADRewardsThreshold, offADRewardsLocktime, offADRewardsAddrs)
}
// ---- Staker / DelegatorTx / ScheduledStaker interface ----
func (*AddDelegatorTx) ChainID() ids.ID {
return constants.PrimaryNetworkID
}
func (tx *AddDelegatorTx) NodeID() ids.NodeID {
return tx.Validator().NodeID
}
func (*AddDelegatorTx) PublicKey() (*bls.PublicKey, bool, error) {
return nil, false, nil
}
func (tx *AddDelegatorTx) StartTime() time.Time {
return time.Unix(int64(tx.Validator().Start), 0)
}
func (tx *AddDelegatorTx) EndTime() time.Time {
return time.Unix(int64(tx.Validator().End), 0)
}
func (tx *AddDelegatorTx) Weight() uint64 {
return tx.Validator().Wght
}
func (*AddDelegatorTx) PendingPriority() Priority {
return PrimaryNetworkDelegatorLegacyPendingPriority
}
func (*AddDelegatorTx) CurrentPriority() Priority {
return PrimaryNetworkDelegatorCurrentPriority
}
// Stake is where staked tokens go when done validating, with FxID set on
// each output (secp256k1fx) as the legacy InitRuntime did.
func (tx *AddDelegatorTx) Stake() []*lux.TransferableOutput {
outs := tx.StakeOuts()
for _, out := range outs {
out.FxID = secp256k1fx.ID
}
return outs
}
func (tx *AddDelegatorTx) RewardsOwner() fx.Owner {
return tx.DelegationRewardsOwner()
}
// SyntacticVerify returns nil iff [tx] is valid.
func (tx *AddDelegatorTx) SyntacticVerify(rt *runtime.Runtime) error {
if tx == nil {
@@ -6,17 +6,25 @@ package txs
import (
"context"
"fmt"
"time"
"github.com/luxfi/constants"
bls "github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
safemath "github.com/luxfi/math"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/platformvm/fx"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/zap"
)
var _ UnsignedTx = (*AddPermissionlessDelegatorTx)(nil)
var (
_ UnsignedTx = (*AddPermissionlessDelegatorTx)(nil)
_ DelegatorTx = (*AddPermissionlessDelegatorTx)(nil)
_ ScheduledStaker = (*AddPermissionlessDelegatorTx)(nil)
)
// AddPermissionlessDelegatorTx is an unsigned addPermissionlessDelegatorTx.
// The struct IS the wire: it holds the zap buffer and reads its fields by
@@ -86,6 +94,60 @@ func (tx *AddPermissionlessDelegatorTx) DelegationRewardsOwner() fx.Owner {
return readOwner(tx.root(), offAPDRewardsThreshold, offAPDRewardsLocktime, offAPDRewardsAddrs)
}
// ---- Staker / DelegatorTx / ScheduledStaker interface ----
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) StartTime() time.Time {
return time.Unix(int64(tx.Validator().Start), 0)
}
func (tx *AddPermissionlessDelegatorTx) EndTime() time.Time {
return time.Unix(int64(tx.Validator().End), 0)
}
func (tx *AddPermissionlessDelegatorTx) Weight() uint64 {
return tx.Validator().Wght
}
func (tx *AddPermissionlessDelegatorTx) PendingPriority() Priority {
if tx.Chain() == constants.PrimaryNetworkID {
return PrimaryNetworkDelegatorPermissionlessPendingPriority
}
return ChainPermissionlessDelegatorPendingPriority
}
func (tx *AddPermissionlessDelegatorTx) CurrentPriority() Priority {
if tx.Chain() == constants.PrimaryNetworkID {
return PrimaryNetworkDelegatorCurrentPriority
}
return ChainPermissionlessDelegatorCurrentPriority
}
// Stake is where staked tokens go when done validating, with FxID set on
// each output (secp256k1fx) as the legacy InitRuntime did.
func (tx *AddPermissionlessDelegatorTx) Stake() []*lux.TransferableOutput {
outs := tx.StakeOuts()
for _, out := range outs {
out.FxID = secp256k1fx.ID
}
return outs
}
func (tx *AddPermissionlessDelegatorTx) RewardsOwner() fx.Owner {
return tx.DelegationRewardsOwner()
}
// SyntacticVerify returns nil iff [tx] is valid.
func (tx *AddPermissionlessDelegatorTx) SyntacticVerify(rt *runtime.Runtime) error {
if tx == nil {
@@ -7,8 +7,10 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/luxfi/constants"
bls "github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
safemath "github.com/luxfi/math"
"github.com/luxfi/node/vms/components/verify"
@@ -17,11 +19,14 @@ import (
"github.com/luxfi/node/vms/platformvm/signer"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/zap"
)
var (
_ UnsignedTx = (*AddPermissionlessValidatorTx)(nil)
_ UnsignedTx = (*AddPermissionlessValidatorTx)(nil)
_ ValidatorTx = (*AddPermissionlessValidatorTx)(nil)
_ ScheduledStaker = (*AddPermissionlessValidatorTx)(nil)
errEmptyNodeID = errors.New("validator nodeID cannot be empty")
errNoStake = errors.New("no stake")
@@ -129,6 +134,73 @@ func (tx *AddPermissionlessValidatorTx) DelegationShares() uint32 {
return tx.root().Uint32(offAPVDelegationShares)
}
// ---- Staker / ValidatorTx / ScheduledStaker interface ----
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) {
sig := tx.Signer()
if err := sig.Verify(); err != nil {
return nil, false, err
}
key := sig.Key()
return key, key != nil, nil
}
func (tx *AddPermissionlessValidatorTx) StartTime() time.Time {
return time.Unix(int64(tx.Validator().Start), 0)
}
func (tx *AddPermissionlessValidatorTx) EndTime() time.Time {
return time.Unix(int64(tx.Validator().End), 0)
}
func (tx *AddPermissionlessValidatorTx) Weight() uint64 {
return tx.Validator().Wght
}
func (tx *AddPermissionlessValidatorTx) PendingPriority() Priority {
if tx.Chain() == constants.PrimaryNetworkID {
return PrimaryNetworkValidatorPendingPriority
}
return ChainPermissionlessValidatorPendingPriority
}
func (tx *AddPermissionlessValidatorTx) CurrentPriority() Priority {
if tx.Chain() == constants.PrimaryNetworkID {
return PrimaryNetworkValidatorCurrentPriority
}
return ChainPermissionlessValidatorCurrentPriority
}
// Stake is where staked tokens go when done validating, with FxID set on
// each output (secp256k1fx) as the legacy InitRuntime did.
func (tx *AddPermissionlessValidatorTx) Stake() []*lux.TransferableOutput {
outs := tx.StakeOuts()
for _, out := range outs {
out.FxID = secp256k1fx.ID
}
return outs
}
func (tx *AddPermissionlessValidatorTx) ValidationRewardsOwner() fx.Owner {
return tx.ValidatorRewardsOwner()
}
func (tx *AddPermissionlessValidatorTx) DelegationRewardsOwner() fx.Owner {
return tx.DelegatorRewardsOwner()
}
func (tx *AddPermissionlessValidatorTx) Shares() uint32 {
return tx.DelegationShares()
}
// SyntacticVerify returns nil iff [tx] is valid.
func (tx *AddPermissionlessValidatorTx) SyntacticVerify(rt *runtime.Runtime) error {
if tx == nil {
+64 -1
View File
@@ -6,18 +6,25 @@ package txs
import (
"context"
"fmt"
"time"
"github.com/luxfi/constants"
bls "github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
safemath "github.com/luxfi/math"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/platformvm/fx"
"github.com/luxfi/node/vms/platformvm/reward"
"github.com/luxfi/runtime"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/zap"
)
var (
_ UnsignedTx = (*AddValidatorTx)(nil)
_ UnsignedTx = (*AddValidatorTx)(nil)
_ ValidatorTx = (*AddValidatorTx)(nil)
_ ScheduledStaker = (*AddValidatorTx)(nil)
errTooManyShares = fmt.Errorf("a staker can only require at most %d shares from delegators", reward.PercentDenominator)
)
@@ -87,6 +94,62 @@ func (tx *AddValidatorTx) RewardsOwner() fx.Owner {
// DelegationShares is the fee (times 10,000) charged to delegators.
func (tx *AddValidatorTx) DelegationShares() uint32 { return tx.root().Uint32(offAVDelegationShares) }
// ---- Staker / ValidatorTx / ScheduledStaker interface ----
func (*AddValidatorTx) ChainID() ids.ID {
return constants.PrimaryNetworkID
}
func (tx *AddValidatorTx) NodeID() ids.NodeID {
return tx.Validator().NodeID
}
func (*AddValidatorTx) PublicKey() (*bls.PublicKey, bool, error) {
return nil, false, nil
}
func (tx *AddValidatorTx) StartTime() time.Time {
return time.Unix(int64(tx.Validator().Start), 0)
}
func (tx *AddValidatorTx) EndTime() time.Time {
return time.Unix(int64(tx.Validator().End), 0)
}
func (tx *AddValidatorTx) Weight() uint64 {
return tx.Validator().Wght
}
func (*AddValidatorTx) PendingPriority() Priority {
return PrimaryNetworkValidatorPendingPriority
}
func (*AddValidatorTx) CurrentPriority() Priority {
return PrimaryNetworkValidatorCurrentPriority
}
// Stake is where staked tokens go when done validating, with FxID set on
// each output (secp256k1fx) as the legacy InitRuntime did.
func (tx *AddValidatorTx) Stake() []*lux.TransferableOutput {
outs := tx.StakeOuts()
for _, out := range outs {
out.FxID = secp256k1fx.ID
}
return outs
}
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.
func (tx *AddValidatorTx) SyntacticVerify(rt *runtime.Runtime) error {
if tx == nil {
-190
View File
@@ -1,190 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Adversarial regression tests targeting Red team findings.
// Every test here MUST fail on vulnerable code and pass on fixed code.
package txs
import (
"math"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/reward"
"github.com/stretchr/testify/require"
)
// ============================================================================
// Finding 3: Nil runtime guard (HIGH)
// Red demonstrated that calling SyntacticVerify(nil) panicked on a valid
// SlashValidatorTx because the nil runtime was dereferenced in BaseTx.
// The fix adds an explicit nil check that returns errNilRuntime.
// ============================================================================
// TestSlashValidatorTx_SyntacticVerify_NilRuntime_NoPanic proves that
// calling SyntacticVerify with a nil runtime returns an error instead
// of panicking.
func TestSlashValidatorTx_SyntacticVerify_NilRuntime_NoPanic(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
msgA := []byte("block-A")
msgB := []byte("block-B")
sigA := bls.SignatureToBytes(bls.Sign(sk, msgA))
sigB := bls.SignatureToBytes(bls.Sign(sk, msgB))
tx := &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
Evidence: SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: msgA,
SignatureA: sigA,
MessageB: msgB,
SignatureB: sigB,
},
SlashPercentage: 100_000,
}
// This MUST NOT panic. It must return errNilRuntime.
require.NotPanics(t, func() {
err = tx.SyntacticVerify(nil)
}, "CRITICAL: SyntacticVerify(nil) panicked instead of returning error")
require.Error(t, err)
require.ErrorIs(t, err, errNilRuntime,
"SyntacticVerify(nil) must return errNilRuntime")
}
// TestSlashValidatorTx_SyntacticVerify_NilTx_NilRuntime proves that
// a nil tx is caught before the nil runtime check.
func TestSlashValidatorTx_SyntacticVerify_NilTx_NilRuntime(t *testing.T) {
var tx *SlashValidatorTx
err := tx.SyntacticVerify(nil)
require.ErrorIs(t, err, ErrNilTx,
"nil tx must be caught before nil runtime")
}
// ============================================================================
// Finding 4: Slash amount overflow (HIGH)
// Red demonstrated that with large validator weights, the naive formula
// weight * slashPercentage / PercentDenominator overflows uint64.
// The fixed formula splits the computation to avoid overflow:
// (weight / denom) * pct + (weight % denom) * pct / denom
// ============================================================================
// TestSlashAmount_LargeWeight_NoOverflow proves that the slash calculation
// does not overflow for large validator weights.
func TestSlashAmount_LargeWeight_NoOverflow(t *testing.T) {
// This test replicates the exact arithmetic from
// slash_validator_tx_executor.go lines 81-84.
tests := []struct {
name string
weight uint64
slashPercentage uint32
expectedSlash uint64
}{
{
name: "half of MaxUint64/2 -- 50% slash",
weight: math.MaxUint64 / 2,
slashPercentage: 500_000, // 50%
expectedSlash: math.MaxUint64 / 2 / 2,
},
{
name: "MaxUint64/2 -- 10% slash",
weight: math.MaxUint64 / 2,
slashPercentage: 100_000, // 10%
expectedSlash: math.MaxUint64 / 2 / 10,
},
{
name: "large weight near MaxUint64 -- 50% slash",
weight: math.MaxUint64 - 1,
slashPercentage: 500_000, // 50%
expectedSlash: (math.MaxUint64 - 1) / 2,
},
{
name: "exact denominator boundary",
weight: uint64(reward.PercentDenominator),
slashPercentage: 500_000, // 50%
expectedSlash: 500_000,
},
{
name: "small weight -- at least 1",
weight: 1,
slashPercentage: 100_000, // 10%
expectedSlash: 1, // floor is 1
},
{
name: "weight 10 -- 10% is exactly 1",
weight: 10,
slashPercentage: 100_000, // 10%
expectedSlash: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Replicate the exact executor formula
pct := uint64(tt.slashPercentage)
denom := uint64(reward.PercentDenominator)
slashAmount := (tt.weight/denom)*pct + (tt.weight%denom)*pct/denom
if slashAmount == 0 {
slashAmount = 1
}
// The naive formula would overflow: weight * pct / denom
// For MaxUint64/2 * 500_000, the intermediate product exceeds uint64.
require.Equal(t, tt.expectedSlash, slashAmount,
"slash amount must be computed without overflow")
// Verify the result is less than the original weight
require.LessOrEqual(t, slashAmount, tt.weight,
"slash amount must not exceed original weight")
// Verify the remaining weight is sane
newWeight := tt.weight - slashAmount
require.Less(t, newWeight, tt.weight,
"new weight must be less than original")
})
}
}
// TestSlashAmount_NaiveFormula_Overflows_Proof demonstrates that the naive
// formula DOES overflow for large weights, proving the fix was necessary.
func TestSlashAmount_NaiveFormula_Overflows_Proof(t *testing.T) {
weight := uint64(math.MaxUint64 / 2)
pct := uint64(500_000)
denom := uint64(reward.PercentDenominator)
// Naive formula: weight * pct overflows uint64
naiveProduct := weight * pct // This wraps around
naiveResult := naiveProduct / denom
// Safe formula from executor
safeResult := (weight/denom)*pct + (weight%denom)*pct/denom
// The naive result is wrong due to overflow
expectedResult := weight / 2 // 50% of weight
// Safe formula must match expected
require.Equal(t, expectedResult, safeResult,
"safe formula must produce correct result")
// Naive formula must NOT match (proves the overflow existed)
require.NotEqual(t, expectedResult, naiveResult,
"naive formula must overflow for large weights -- this proves the vulnerability")
}
// TestSlashPercentage_AllValidTypes proves all evidence types have
// well-defined slash percentages.
func TestSlashPercentage_AllValidTypes(t *testing.T) {
// Double vote = 10%
require.Equal(t, uint32(100_000), uint32(100_000),
"DoubleVoteSlashPercent = 10%%")
// Double sign = 50%
require.Equal(t, uint32(500_000), uint32(500_000),
"DoubleSignSlashPercent = 50%%")
}
-127
View File
@@ -1,127 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"errors"
"fmt"
"strings"
"unicode"
"github.com/luxfi/runtime"
"github.com/luxfi/utils"
"github.com/luxfi/utxo/secp256k1fx"
)
// CreateAssetTx semantics for the P-only primary network.
//
// PlatformVM uses a single Fx (secp256k1fx). InitialState therefore drops the
// FxIndex/Fx-slice indirection that XVM needs and only carries the initial
// outputs that should be minted for the new asset. The on-the-wire layout
// matches `vms/xvm/txs/create_asset_tx.go` so cross-VM atomic UTXOs and
// historical wallet code see an identical byte shape.
const (
minNameLen = 1
maxNameLen = 128
minSymbolLen = 1
maxSymbolLen = 4
maxDenomination = 32
)
var (
_ UnsignedTx = (*CreateAssetTx)(nil)
_ secp256k1fx.UnsignedTx = (*CreateAssetTx)(nil)
ErrNilCreateAssetTx = errors.New("create asset tx is nil")
ErrNameTooShort = fmt.Errorf("asset name is too short, minimum size is %d", minNameLen)
ErrNameTooLong = fmt.Errorf("asset name is too long, maximum size is %d", maxNameLen)
ErrSymbolTooShort = fmt.Errorf("asset symbol is too short, minimum size is %d", minSymbolLen)
ErrSymbolTooLong = fmt.Errorf("asset symbol is too long, maximum size is %d", maxSymbolLen)
ErrNoInitialStates = errors.New("asset must declare at least one initial state output")
ErrIllegalNameChar = errors.New("asset name must be made up of only letters, numbers and spaces")
ErrIllegalSymbolChar = errors.New("asset symbol must be all upper-case letters")
ErrUnexpectedWhitespace = errors.New("unexpected whitespace in asset name")
ErrDenominationTooLarge = errors.New("denomination is too large")
ErrInitialStatesNotSorted = errors.New("initial states are not sorted and unique")
)
// CreateAssetTx mints a brand new UTXO asset on the P-Chain. The minted asset
// is owned by the outputs declared in [States]; the fee for asset creation is
// paid out of [BaseTx.Ins] / [BaseTx.Outs].
type CreateAssetTx struct {
BaseTx `serialize:"true"`
Name string `serialize:"true" json:"name"`
Symbol string `serialize:"true" json:"symbol"`
Denomination byte `serialize:"true" json:"denomination"`
States []*InitialState `serialize:"true" json:"initialStates"`
}
// InitRuntime initialises the embedded BaseTx and every InitialState output.
func (tx *CreateAssetTx) InitRuntime(rt *runtime.Runtime) {
tx.BaseTx.InitRuntime(rt)
for _, state := range tx.States {
state.InitRuntime(rt)
}
}
// InitialStates returns the slice of initial states for this asset. The
// returned slice MUST NOT be mutated.
func (tx *CreateAssetTx) InitialStates() []*InitialState {
return tx.States
}
// SyntacticVerify returns nil iff this tx is well formed.
func (tx *CreateAssetTx) SyntacticVerify(rt *runtime.Runtime) error {
switch {
case tx == nil:
return ErrNilCreateAssetTx
case tx.SyntacticallyVerified:
return nil
case len(tx.Name) < minNameLen:
return ErrNameTooShort
case len(tx.Name) > maxNameLen:
return ErrNameTooLong
case len(tx.Symbol) < minSymbolLen:
return ErrSymbolTooShort
case len(tx.Symbol) > maxSymbolLen:
return ErrSymbolTooLong
case len(tx.States) == 0:
return ErrNoInitialStates
case tx.Denomination > maxDenomination:
return ErrDenominationTooLarge
case strings.TrimSpace(tx.Name) != tx.Name:
return ErrUnexpectedWhitespace
}
for _, r := range tx.Name {
if r > unicode.MaxASCII || (!unicode.IsLetter(r) && !unicode.IsNumber(r) && r != ' ') {
return ErrIllegalNameChar
}
}
for _, r := range tx.Symbol {
if r > unicode.MaxASCII || !unicode.IsUpper(r) {
return ErrIllegalSymbolChar
}
}
if err := tx.BaseTx.SyntacticVerify(rt); err != nil {
return err
}
for _, state := range tx.States {
if err := state.Verify(Codec); err != nil {
return fmt.Errorf("initial state failed verification: %w", err)
}
}
if !utils.IsSortedAndUnique(tx.States) {
return ErrInitialStatesNotSorted
}
tx.SyntacticallyVerified = true
return nil
}
func (tx *CreateAssetTx) Visit(v Visitor) error {
return v.CreateAssetTx(tx)
}
-219
View File
@@ -1,219 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"strings"
"testing"
"github.com/luxfi/ids"
"github.com/luxfi/runtime"
"github.com/luxfi/node/vms/components/verify"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/stretchr/testify/require"
)
func newRuntime(t *testing.T) *runtime.Runtime {
t.Helper()
return &runtime.Runtime{
NetworkID: 1337,
ChainID: ids.GenerateTestID(),
UTXOAssetID: ids.GenerateTestID(),
}
}
func validBaseTxForAsset(rt *runtime.Runtime) BaseTx {
return BaseTx{
BaseTx: lux.BaseTx{
NetworkID: rt.NetworkID,
BlockchainID: rt.ChainID,
Ins: []*lux.TransferableInput{},
Outs: []*lux.TransferableOutput{},
},
}
}
func TestCreateAssetTx_SyntacticVerify_NameTooShort(t *testing.T) {
rt := newRuntime(t)
tx := &CreateAssetTx{
BaseTx: validBaseTxForAsset(rt),
Name: "",
Symbol: "LUX",
}
require.ErrorIs(t, tx.SyntacticVerify(rt), ErrNameTooShort)
}
func TestCreateAssetTx_SyntacticVerify_NameTooLong(t *testing.T) {
rt := newRuntime(t)
tx := &CreateAssetTx{
BaseTx: validBaseTxForAsset(rt),
Name: strings.Repeat("a", maxNameLen+1),
Symbol: "LUX",
}
require.ErrorIs(t, tx.SyntacticVerify(rt), ErrNameTooLong)
}
func TestCreateAssetTx_SyntacticVerify_SymbolBadChar(t *testing.T) {
rt := newRuntime(t)
tx := &CreateAssetTx{
BaseTx: validBaseTxForAsset(rt),
Name: "ValidAsset",
Symbol: "lux", // must be upper-case
States: []*InitialState{{FxIndex: 0, Outs: []verify.State{}}},
}
require.ErrorIs(t, tx.SyntacticVerify(rt), ErrIllegalSymbolChar)
}
func TestCreateAssetTx_SyntacticVerify_DenominationTooLarge(t *testing.T) {
rt := newRuntime(t)
tx := &CreateAssetTx{
BaseTx: validBaseTxForAsset(rt),
Name: "LUX",
Symbol: "LUX",
Denomination: maxDenomination + 1,
States: []*InitialState{{FxIndex: 0, Outs: []verify.State{}}},
}
require.ErrorIs(t, tx.SyntacticVerify(rt), ErrDenominationTooLarge)
}
func TestCreateAssetTx_SyntacticVerify_NoInitialStates(t *testing.T) {
rt := newRuntime(t)
tx := &CreateAssetTx{
BaseTx: validBaseTxForAsset(rt),
Name: "LUX",
Symbol: "LUX",
States: nil,
}
require.ErrorIs(t, tx.SyntacticVerify(rt), ErrNoInitialStates)
}
func TestCreateAssetTx_SyntacticVerify_NonZeroFxIndexRejected(t *testing.T) {
rt := newRuntime(t)
tx := &CreateAssetTx{
BaseTx: validBaseTxForAsset(rt),
Name: "LUX",
Symbol: "LUX",
Denomination: 9,
States: []*InitialState{{FxIndex: 1, Outs: []verify.State{}}},
}
require.ErrorIs(t, tx.SyntacticVerify(rt), ErrInitialStateNonZero)
}
func TestCreateAssetTx_CodecRoundtrip(t *testing.T) {
require := require.New(t)
tx := &CreateAssetTx{
BaseTx: BaseTx{
BaseTx: lux.BaseTx{
NetworkID: 1337,
BlockchainID: ids.GenerateTestID(),
},
},
Name: "LUX",
Symbol: "LUX",
Denomination: 9,
States: []*InitialState{
{FxIndex: 0, Outs: []verify.State{
&secp256k1fx.TransferOutput{
Amt: 1,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{ids.GenerateTestShortID()},
},
},
}},
},
}
var unsigned UnsignedTx = tx
bytes, err := Codec.Marshal(CodecVersion, &unsigned)
require.NoError(err)
var roundtripped UnsignedTx
_, err = Codec.Unmarshal(bytes, &roundtripped)
require.NoError(err)
got, ok := roundtripped.(*CreateAssetTx)
require.True(ok)
require.Equal(tx.Name, got.Name)
require.Equal(tx.Symbol, got.Symbol)
require.Equal(tx.Denomination, got.Denomination)
require.Equal(len(tx.States), len(got.States))
}
func TestOperationTx_SyntacticVerify_NoOperations(t *testing.T) {
rt := newRuntime(t)
tx := &OperationTx{
BaseTx: validBaseTxForAsset(rt),
Ops: nil,
}
require.ErrorIs(t, tx.SyntacticVerify(rt), ErrNoOperations)
}
func TestOperationTx_VisitorDispatches(t *testing.T) {
require := require.New(t)
tx := &OperationTx{}
called := false
v := &mockVisitor{operationFn: func(*OperationTx) error {
called = true
return nil
}}
require.NoError(tx.Visit(v))
require.True(called)
}
func TestCreateAssetTx_VisitorDispatches(t *testing.T) {
require := require.New(t)
tx := &CreateAssetTx{}
called := false
v := &mockVisitor{createAssetFn: func(*CreateAssetTx) error {
called = true
return nil
}}
require.NoError(tx.Visit(v))
require.True(called)
}
// mockVisitor stubs every Visitor method but the two we exercise here.
type mockVisitor struct {
createAssetFn func(*CreateAssetTx) error
operationFn func(*OperationTx) error
}
func (*mockVisitor) AddValidatorTx(*AddValidatorTx) error { return nil }
func (*mockVisitor) AddChainValidatorTx(*AddChainValidatorTx) error { return nil }
func (*mockVisitor) AddDelegatorTx(*AddDelegatorTx) error { return nil }
func (*mockVisitor) CreateNetworkTx(*CreateNetworkTx) error { return nil }
func (*mockVisitor) CreateChainTx(*CreateChainTx) error { return nil }
func (*mockVisitor) ImportTx(*ImportTx) error { return nil }
func (*mockVisitor) ExportTx(*ExportTx) error { return nil }
func (*mockVisitor) AdvanceTimeTx(*AdvanceTimeTx) error { return nil }
func (*mockVisitor) RewardValidatorTx(*RewardValidatorTx) error { return nil }
func (*mockVisitor) RemoveChainValidatorTx(*RemoveChainValidatorTx) error { return nil }
func (*mockVisitor) TransformChainTx(*TransformChainTx) error { return nil }
func (*mockVisitor) AddPermissionlessValidatorTx(*AddPermissionlessValidatorTx) error { return nil }
func (*mockVisitor) AddPermissionlessDelegatorTx(*AddPermissionlessDelegatorTx) error { return nil }
func (*mockVisitor) TransferChainOwnershipTx(*TransferChainOwnershipTx) error { return nil }
func (*mockVisitor) BaseTx(*BaseTx) error { return nil }
func (*mockVisitor) ConvertNetworkToL1Tx(*ConvertNetworkToL1Tx) error { return nil }
func (*mockVisitor) CreateSovereignL1Tx(*CreateSovereignL1Tx) error { return nil }
func (*mockVisitor) RegisterL1ValidatorTx(*RegisterL1ValidatorTx) error { return nil }
func (*mockVisitor) SetL1ValidatorWeightTx(*SetL1ValidatorWeightTx) error { return nil }
func (*mockVisitor) IncreaseL1ValidatorBalanceTx(*IncreaseL1ValidatorBalanceTx) error { return nil }
func (*mockVisitor) DisableL1ValidatorTx(*DisableL1ValidatorTx) error { return nil }
func (*mockVisitor) SlashValidatorTx(*SlashValidatorTx) error { return nil }
func (m *mockVisitor) CreateAssetTx(tx *CreateAssetTx) error {
if m.createAssetFn != nil {
return m.createAssetFn(tx)
}
return nil
}
func (m *mockVisitor) OperationTx(tx *OperationTx) error {
if m.operationFn != nil {
return m.operationFn(tx)
}
return nil
}
@@ -132,18 +132,6 @@ func (*atomicTxExecutor) DisableL1ValidatorTx(*txs.DisableL1ValidatorTx) error {
return ErrWrongTxType
}
func (*atomicTxExecutor) SlashValidatorTx(*txs.SlashValidatorTx) error {
return ErrWrongTxType
}
func (*atomicTxExecutor) CreateAssetTx(*txs.CreateAssetTx) error {
return ErrWrongTxType
}
func (*atomicTxExecutor) OperationTx(*txs.OperationTx) error {
return ErrWrongTxType
}
func (e *atomicTxExecutor) ImportTx(*txs.ImportTx) error {
return e.atomicTx()
}
@@ -1,88 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package executor
import (
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/platformvm/status"
"github.com/luxfi/node/vms/platformvm/txs"
)
// CreateAssetTx mints a new UTXO asset on the P-Chain.
//
// The asset ID is, by convention with the X-Chain wire format, the tx ID.
// The fee charged is computed by the fee calculator (static config →
// `CreateAssetTxFee`; dynamic fee → gas complexity); the minted asset is
// produced as UTXOs owned by [tx.States[*].Outs] keyed by the new asset ID.
func (e *standardTxExecutor) CreateAssetTx(tx *txs.CreateAssetTx) error {
// Verify the tx is well-formed first.
if err := e.tx.SyntacticVerify(e.backend.Runtime); err != nil {
return err
}
// Fee flow check.
fee, err := e.feeCalculator.CalculateFee(tx)
if err != nil {
return err
}
if err := e.backend.FlowChecker.VerifySpend(
tx,
e.state,
tx.Ins,
tx.Outs,
e.tx.Creds,
map[ids.ID]uint64{
e.backend.Runtime.UTXOAssetID: fee,
},
); err != nil {
return err
}
txID := e.tx.ID()
// Consume the fee inputs, produce the fee change outputs (BaseTx outs).
lux.Consume(e.state, tx.Ins)
lux.Produce(e.state, txID, tx.Outs)
// Produce the freshly minted asset UTXOs. The asset ID is the tx ID.
//
// Output indices come from the InitialState slot position, NOT from the
// count of UTXOs we actually wrote. Mirrors XVM semantics: a mint output
// at States[0].Outs[0] gets `OutputIndex = len(tx.Outs)` even though the
// executor doesn't write the mint UTXO here (mint authority is held in
// the asset registry, materialised on first OperationTx mint). A holder
// transfer output at States[0].Outs[1] gets `OutputIndex = len(tx.Outs)+1`.
// Compacting indices would break OperationTx references back to mint
// outputs (RED V4 outputIndex-phantom-UTXO vector).
outputIndex := uint32(len(tx.Outs))
for _, state := range tx.States {
for _, out := range state.Outs {
if transferOut, ok := out.(lux.TransferableOut); ok {
utxo := &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: outputIndex,
},
Asset: lux.Asset{ID: txID},
Out: transferOut,
}
e.state.AddUTXO(utxo)
}
// Non-TransferableOut (e.g. MintOutput) — index slot reserved
// for OperationTx references; UTXO materialised on first mint.
outputIndex++
}
}
// Persist the create-asset tx itself so future ops, SemanticVerifier-style
// lookups (`State.GetTx(assetID)`) and indexer projections
// (`chain.PrimaryAlias(assetID)`) resolve to the original tx with all
// metadata. The asset ID is the tx ID by construction (line 43 above);
// `state.AddTx` is the canonical PlatformVM persistence API used by
// CreateChainTx / CreateNetworkTx / committed-block paths.
e.state.AddTx(e.tx, status.Committed)
return nil
}
-124
View File
@@ -1,124 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package executor
import (
"errors"
"fmt"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/platformvm/status"
"github.com/luxfi/node/vms/platformvm/txs"
)
var (
errOpUTXOMissingAssetID = errors.New("op consumes a UTXO whose asset ID does not match the op asset")
)
// OperationTx applies the operations carried by [tx] to the P-Chain UTXO set.
//
// On the P-only primary network we accept a single Fx (secp256k1fx). Each
// Operation consumes a UTXO set whose asset ID matches [op.Asset.ID]. The
// emitted side effects are encoded by the Fx-specific Op, and every Op is
// authorised by an explicit credential — mirroring `xvm/txs/executor/
// semantic_verifier.go::verifyOperation`. A missing or invalid credential
// rejects the entire tx before any state mutation.
func (e *standardTxExecutor) OperationTx(tx *txs.OperationTx) error {
if err := e.tx.SyntacticVerify(e.backend.Runtime); err != nil {
return err
}
// Defensive cred-count guard. The Tx wire format is fixed by the codec
// but a malicious peer could ship a payload that decoded with fewer
// creds than [NumCredentials]. Without this check the BaseTx cred
// slice below — and the per-Op cred lookup further down — would slice
// past the end and panic. Mirrors xvm/txs/executor/syntactic_verifier
// .go::OperationTx.
expectedCreds := tx.NumCredentials()
if len(e.tx.Creds) != expectedCreds {
return fmt.Errorf("%w: %d != %d",
txs.ErrWrongNumberOfCredentials,
len(e.tx.Creds),
expectedCreds,
)
}
// Charge the standard TxFee out of the BaseTx inputs / outputs.
fee, err := e.feeCalculator.CalculateFee(tx)
if err != nil {
return err
}
if err := e.backend.FlowChecker.VerifySpend(
tx,
e.state,
tx.Ins,
tx.Outs,
e.tx.Creds[:len(tx.Ins)],
map[ids.ID]uint64{
e.backend.Runtime.UTXOAssetID: fee,
},
); err != nil {
return fmt.Errorf("op tx fee verification: %w", err)
}
// Authorise + collect each operation against the chain state. We loop
// twice on purpose:
//
// 1. Resolve every consumed UTXO and call fx.VerifyOperation so that
// ANY auth failure aborts the tx before we mutate state.
// 2. Once every op has been authorised, delete the consumed UTXOs.
//
// The two-pass shape matches xvm's verify-then-apply discipline: any
// auth failure leaves the UTXO set untouched.
opCredOffset := len(tx.Ins)
consumedUTXOIDs := make([]ids.ID, 0, len(tx.Ops))
for i, op := range tx.Ops {
if err := e.verifyOperation(tx, op, e.tx.Creds[opCredOffset+i]); err != nil {
return fmt.Errorf("op %d verification: %w", i, err)
}
for _, utxoID := range op.UTXOIDs {
consumedUTXOIDs = append(consumedUTXOIDs, utxoID.InputID())
}
}
for _, id := range consumedUTXOIDs {
e.state.DeleteUTXO(id)
}
txID := e.tx.ID()
// Consume the BaseTx fee inputs / produce BaseTx outputs.
lux.Consume(e.state, tx.Ins)
lux.Produce(e.state, txID, tx.Outs)
// Persist the tx itself so future operations / indexer queries resolve
// `state.GetTx(txID)` to the committed payload. Aligns with how
// CreateChainTx and CreateNetworkTx persist their on-chain receipt.
e.state.AddTx(e.tx, status.Committed)
return nil
}
// verifyOperation resolves the UTXOs consumed by [op], asserts they share
// the op's asset ID, then delegates to fx.VerifyOperation so the Fx checks
// the credential against the consumed outputs.
func (e *standardTxExecutor) verifyOperation(
utx txs.UnsignedTx,
op *txs.Operation,
cred interface{},
) error {
opAssetID := op.AssetID()
utxos := make([]interface{}, len(op.UTXOIDs))
for i, utxoID := range op.UTXOIDs {
utxo, err := e.state.GetUTXO(utxoID.InputID())
if err != nil {
return fmt.Errorf("op references unknown utxo %s: %w", utxoID.InputID(), err)
}
if utxo.AssetID() != opAssetID {
return errOpUTXOMissingAssetID
}
utxos[i] = utxo.Out
}
return e.backend.Fx.VerifyOperation(utx, op.Op, cred, utxos)
}
@@ -151,18 +151,6 @@ func (*proposalTxExecutor) DisableL1ValidatorTx(*txs.DisableL1ValidatorTx) error
return ErrWrongTxType
}
func (*proposalTxExecutor) SlashValidatorTx(*txs.SlashValidatorTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) CreateAssetTx(*txs.CreateAssetTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) OperationTx(*txs.OperationTx) error {
return ErrWrongTxType
}
func (*proposalTxExecutor) AddValidatorTx(*txs.AddValidatorTx) error {
// Proposal-style AddValidatorTx is permanently rejected; only standard
// blocks may carry staker txs.
@@ -1,125 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package executor
import (
"errors"
"fmt"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/bls"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/platformvm/reward"
"github.com/luxfi/node/vms/platformvm/txs"
)
var (
errValidatorNotFound = errors.New("validator not found in current staker set")
errValidatorNoBLSKey = errors.New("validator has no BLS public key registered")
errInvalidEvidenceSignature = errors.New("evidence signature does not verify against validator BLS key")
errSlashPercentMismatch = errors.New("slash percentage does not match expected value for evidence type")
)
// Default slash percentages per evidence type.
const (
DoubleVoteSlashPercent = 100_000 // 10% of reward.PercentDenominator (1_000_000)
DoubleSignSlashPercent = 500_000 // 50%
)
func (e *standardTxExecutor) SlashValidatorTx(tx *txs.SlashValidatorTx) error {
if err := e.tx.SyntacticVerify(e.backend.Runtime); err != nil {
return err
}
// Verify slash percentage matches evidence type
var expectedPercent uint32
switch tx.Evidence.Type {
case txs.DoubleVoteEvidence:
expectedPercent = DoubleVoteSlashPercent
case txs.DoubleSignEvidence:
expectedPercent = DoubleSignSlashPercent
default:
return fmt.Errorf("unknown evidence type: %d", tx.Evidence.Type)
}
if tx.SlashPercentage != expectedPercent {
return fmt.Errorf(
"%w: got %d, expected %d for evidence type %d",
errSlashPercentMismatch,
tx.SlashPercentage,
expectedPercent,
tx.Evidence.Type,
)
}
// Look up the validator in the current staker set
staker, err := e.state.GetCurrentValidator(constants.PrimaryNetworkID, tx.NodeID)
if err != nil {
return fmt.Errorf("%w: %w", errValidatorNotFound, err)
}
if staker.PublicKey == nil {
return errValidatorNoBLSKey
}
// Verify both evidence signatures against the validator's BLS key
if err := verifyEvidenceSignatures(staker.PublicKey, &tx.Evidence); err != nil {
return err
}
// Calculate slash amount using overflow-safe arithmetic.
// Instead of weight * pct / denom (which overflows for large weights),
// split into: (weight / denom) * pct + (weight % denom) * pct / denom
weight := staker.Weight
pct := uint64(tx.SlashPercentage)
denom := uint64(reward.PercentDenominator)
slashAmount := (weight/denom)*pct + (weight%denom)*pct/denom
if slashAmount == 0 {
slashAmount = 1 // Slash at least 1 unit
}
newWeight := staker.Weight - slashAmount
if newWeight < e.backend.Config.MinValidatorStake {
// Remove the validator entirely -- weight below minimum
e.state.DeleteCurrentValidator(staker)
} else {
// Update the validator's weight in the staker set
e.state.DeleteCurrentValidator(staker)
updatedStaker := *staker
updatedStaker.Weight = newWeight
if err := e.state.PutCurrentValidator(&updatedStaker); err != nil {
return fmt.Errorf("failed to update slashed validator: %w", err)
}
}
txID := e.tx.ID()
lux.Consume(e.state, tx.Ins)
lux.Produce(e.state, txID, tx.Outs)
return nil
}
// verifyEvidenceSignatures checks that both signatures in the evidence
// verify against the given BLS public key.
func verifyEvidenceSignatures(pk *bls.PublicKey, evidence *txs.SlashEvidence) error {
sigA, err := bls.SignatureFromBytes(evidence.SignatureA)
if err != nil {
return fmt.Errorf("%w: failed to parse signature A: %w", errInvalidEvidenceSignature, err)
}
if !bls.Verify(pk, sigA, evidence.MessageA) {
return fmt.Errorf("%w: signature A does not verify", errInvalidEvidenceSignature)
}
sigB, err := bls.SignatureFromBytes(evidence.SignatureB)
if err != nil {
return fmt.Errorf("%w: failed to parse signature B: %w", errInvalidEvidenceSignature, err)
}
if !bls.Verify(pk, sigB, evidence.MessageB) {
return fmt.Errorf("%w: signature B does not verify", errInvalidEvidenceSignature)
}
return nil
}
@@ -1,201 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package executor
import (
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/stretchr/testify/require"
)
func TestVerifyEvidenceSignatures(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
pk := bls.PublicFromSecretKey(sk)
msgA := []byte("block-hash-A-at-height-100")
msgB := []byte("block-hash-B-at-height-100")
sigA := bls.Sign(sk, msgA)
sigB := bls.Sign(sk, msgB)
validEvidence := &txs.SlashEvidence{
Height: 100,
Type: txs.DoubleVoteEvidence,
MessageA: msgA,
SignatureA: bls.SignatureToBytes(sigA),
MessageB: msgB,
SignatureB: bls.SignatureToBytes(sigB),
}
t.Run("valid signatures", func(t *testing.T) {
require.NoError(t, verifyEvidenceSignatures(pk, validEvidence))
})
t.Run("wrong public key", func(t *testing.T) {
otherSK, err := bls.NewSecretKey()
require.NoError(t, err)
otherPK := bls.PublicFromSecretKey(otherSK)
err = verifyEvidenceSignatures(otherPK, validEvidence)
require.ErrorIs(t, err, errInvalidEvidenceSignature)
})
t.Run("swapped signatures", func(t *testing.T) {
swappedEvidence := &txs.SlashEvidence{
Height: 100,
Type: txs.DoubleVoteEvidence,
MessageA: msgA,
SignatureA: bls.SignatureToBytes(sigB), // sig for msgB applied to msgA
MessageB: msgB,
SignatureB: bls.SignatureToBytes(sigA),
}
err := verifyEvidenceSignatures(pk, swappedEvidence)
require.ErrorIs(t, err, errInvalidEvidenceSignature)
})
t.Run("corrupted signature bytes", func(t *testing.T) {
corruptedSigA := make([]byte, bls.SignatureLen)
copy(corruptedSigA, bls.SignatureToBytes(sigA))
corruptedSigA[0] ^= 0xFF
corruptedEvidence := &txs.SlashEvidence{
Height: 100,
Type: txs.DoubleVoteEvidence,
MessageA: msgA,
SignatureA: corruptedSigA,
MessageB: msgB,
SignatureB: bls.SignatureToBytes(sigB),
}
err := verifyEvidenceSignatures(pk, corruptedEvidence)
require.ErrorIs(t, err, errInvalidEvidenceSignature)
})
}
func TestSlashPercentages(t *testing.T) {
// Verify the constants match expected values
require.Equal(t, uint32(100_000), uint32(DoubleVoteSlashPercent)) // 10%
require.Equal(t, uint32(500_000), uint32(DoubleSignSlashPercent)) // 50%
}
// --- Additional inversion tests for the executor ---
// TestVerifyEvidenceSignatures_ForgedByDifferentKey verifies that evidence
// signed by a completely different BLS key is rejected.
func TestVerifyEvidenceSignatures_ForgedByDifferentKey(t *testing.T) {
// Real validator key
realSK, err := bls.NewSecretKey()
require.NoError(t, err)
realPK := bls.PublicFromSecretKey(realSK)
// Forger key
forgerSK, err := bls.NewSecretKey()
require.NoError(t, err)
msgA := []byte("block-A-height-100")
msgB := []byte("block-B-height-100")
// Forger signs with their own key
ev := &txs.SlashEvidence{
Height: 100,
Type: txs.DoubleVoteEvidence,
MessageA: msgA,
SignatureA: bls.SignatureToBytes(bls.Sign(forgerSK, msgA)),
MessageB: msgB,
SignatureB: bls.SignatureToBytes(bls.Sign(forgerSK, msgB)),
}
// Must fail: signatures don't verify against the real validator's key
err = verifyEvidenceSignatures(realPK, ev)
require.ErrorIs(t, err, errInvalidEvidenceSignature)
}
// TestVerifyEvidenceSignatures_PartialForge_SigAReal_SigBForged verifies
// that even if one signature is valid, the other being forged causes rejection.
func TestVerifyEvidenceSignatures_PartialForge_SigAReal_SigBForged(t *testing.T) {
realSK, err := bls.NewSecretKey()
require.NoError(t, err)
realPK := bls.PublicFromSecretKey(realSK)
forgerSK, err := bls.NewSecretKey()
require.NoError(t, err)
msgA := []byte("legitimate-block-A")
msgB := []byte("forged-block-B")
ev := &txs.SlashEvidence{
Height: 100,
Type: txs.DoubleVoteEvidence,
MessageA: msgA,
SignatureA: bls.SignatureToBytes(bls.Sign(realSK, msgA)), // valid
MessageB: msgB,
SignatureB: bls.SignatureToBytes(bls.Sign(forgerSK, msgB)), // forged
}
err = verifyEvidenceSignatures(realPK, ev)
require.ErrorIs(t, err, errInvalidEvidenceSignature)
}
// TestVerifyEvidenceSignatures_ZeroLengthSig verifies that a zero-length
// signature fails parsing.
func TestVerifyEvidenceSignatures_ZeroLengthSig(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
pk := bls.PublicFromSecretKey(sk)
msgA := []byte("msg-A")
msgB := []byte("msg-B")
ev := &txs.SlashEvidence{
Height: 100,
Type: txs.DoubleVoteEvidence,
MessageA: msgA,
SignatureA: []byte{}, // empty
MessageB: msgB,
SignatureB: bls.SignatureToBytes(bls.Sign(sk, msgB)),
}
err = verifyEvidenceSignatures(pk, ev)
require.ErrorIs(t, err, errInvalidEvidenceSignature)
}
// TestVerifyEvidenceSignatures_CorruptedSignatureB verifies that corrupted
// bytes in signature B are caught.
func TestVerifyEvidenceSignatures_CorruptedSignatureB(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
pk := bls.PublicFromSecretKey(sk)
msgA := []byte("block-A")
msgB := []byte("block-B")
sigA := bls.Sign(sk, msgA)
sigB := bls.Sign(sk, msgB)
// Corrupt signature B
corruptedB := make([]byte, bls.SignatureLen)
copy(corruptedB, bls.SignatureToBytes(sigB))
corruptedB[len(corruptedB)-1] ^= 0xFF
ev := &txs.SlashEvidence{
Height: 100,
Type: txs.DoubleVoteEvidence,
MessageA: msgA,
SignatureA: bls.SignatureToBytes(sigA),
MessageB: msgB,
SignatureB: corruptedB,
}
err = verifyEvidenceSignatures(pk, ev)
require.ErrorIs(t, err, errInvalidEvidenceSignature)
}
// TestSlashPercentMismatch verifies the executor constants are consistent:
// DoubleVote gets 10%, DoubleSign gets 50%.
func TestSlashPercentMismatch(t *testing.T) {
require.Equal(t, uint32(100_000), uint32(DoubleVoteSlashPercent))
require.Equal(t, uint32(500_000), uint32(DoubleSignSlashPercent))
require.Less(t, uint32(DoubleVoteSlashPercent), uint32(DoubleSignSlashPercent),
"double-sign must be penalized more severely than double-vote")
}
@@ -110,18 +110,6 @@ func (v *MempoolTxVerifier) DisableL1ValidatorTx(tx *txs.DisableL1ValidatorTx) e
return v.standardTx(tx)
}
func (v *MempoolTxVerifier) SlashValidatorTx(tx *txs.SlashValidatorTx) error {
return v.standardTx(tx)
}
func (v *MempoolTxVerifier) CreateAssetTx(tx *txs.CreateAssetTx) error {
return v.standardTx(tx)
}
func (v *MempoolTxVerifier) OperationTx(tx *txs.OperationTx) error {
return v.standardTx(tx)
}
func (v *MempoolTxVerifier) standardTx(tx txs.UnsignedTx) error {
baseState, err := v.standardBaseState()
if err != nil {
@@ -118,18 +118,6 @@ func (*warpVerifier) DisableL1ValidatorTx(*txs.DisableL1ValidatorTx) error {
return nil
}
func (*warpVerifier) SlashValidatorTx(*txs.SlashValidatorTx) error {
return nil
}
func (*warpVerifier) CreateAssetTx(*txs.CreateAssetTx) error {
return nil
}
func (*warpVerifier) OperationTx(*txs.OperationTx) error {
return nil
}
func (w *warpVerifier) RegisterL1ValidatorTx(tx *txs.RegisterL1ValidatorTx) error {
return w.verify(tx.Message)
}
-53
View File
@@ -242,18 +242,6 @@ var (
gas.DBRead: 1, // read staker
gas.DBWrite: 6, // write remaining balance utxo + weight diff + deactivated weight diff + public key diff + delete staker + write staker
}
IntrinsicSlashValidatorTxComplexities = gas.Dimensions{
gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] +
ids.NodeIDLen + // nodeID
pcodecs.LongLen + // evidence height
pcodecs.ByteLen + // evidence type
pcodecs.IntLen + // messageA length prefix
pcodecs.IntLen + // messageB length prefix
2*bls.SignatureLen + // two BLS signatures
pcodecs.IntLen, // slashPercentage
gas.DBRead: 1, // read validator
gas.DBWrite: 2, // delete + re-insert validator (or just delete if below minimum)
}
errUnsupportedOutput = errors.New("unsupported output type")
errUnsupportedInput = errors.New("unsupported input type")
@@ -861,23 +849,6 @@ func (c *complexityVisitor) DisableL1ValidatorTx(tx *txs.DisableL1ValidatorTx) e
return err
}
func (c *complexityVisitor) SlashValidatorTx(tx *txs.SlashValidatorTx) error {
baseTxComplexity, err := baseTxComplexity(&tx.BaseTx)
if err != nil {
return err
}
// Add bandwidth for the variable-length evidence messages
evidenceBandwidth := uint64(len(tx.Evidence.MessageA) + len(tx.Evidence.MessageB))
c.output, err = IntrinsicSlashValidatorTxComplexities.Add(
&baseTxComplexity,
)
if err != nil {
return err
}
c.output[gas.Bandwidth], err = math.Add(c.output[gas.Bandwidth], evidenceBandwidth)
return err
}
func baseTxComplexity(tx *txs.BaseTx) (gas.Dimensions, error) {
outputsComplexity, err := OutputComplexity(tx.Outs...)
if err != nil {
@@ -970,27 +941,3 @@ func (c *complexityVisitor) TransferChainOwnershipTx(tx *txs.TransferChainOwners
)
return err
}
// CreateAssetTx complexity is base + the initial-state output complexity
// (each InitialState carries TransferableOut-shaped outs that mint asset
// UTXOs). The fee charged is CreateAssetTxFee, applied by the fee calc.
func (c *complexityVisitor) CreateAssetTx(tx *txs.CreateAssetTx) error {
baseTxComplexity, err := baseTxComplexity(&tx.BaseTx)
if err != nil {
return err
}
c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity)
return err
}
// OperationTx complexity is base + per-operation overhead. We bound the
// per-op cost via the BaseTx complexity envelope on P-Chain (no Fx-specific
// op state cost path).
func (c *complexityVisitor) OperationTx(tx *txs.OperationTx) error {
baseTxComplexity, err := baseTxComplexity(&tx.BaseTx)
if err != nil {
return err
}
c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity)
return err
}
@@ -149,10 +149,6 @@ func (*staticVisitor) SetL1ValidatorWeightTx(*txs.SetL1ValidatorWeightTx) error
return ErrUnsupportedTx
}
func (*staticVisitor) SlashValidatorTx(*txs.SlashValidatorTx) error {
return ErrUnsupportedTx
}
func (v *staticVisitor) AddChainValidatorTx(*txs.AddChainValidatorTx) error {
v.fee = v.config.AddChainValidatorFee
return nil
@@ -177,13 +173,3 @@ func (v *staticVisitor) TransferChainOwnershipTx(*txs.TransferChainOwnershipTx)
v.fee = v.config.TxFee
return nil
}
func (v *staticVisitor) CreateAssetTx(*txs.CreateAssetTx) error {
v.fee = v.config.CreateAssetTxFee
return nil
}
func (v *staticVisitor) OperationTx(*txs.OperationTx) error {
v.fee = v.config.TxFee
return nil
}
-105
View File
@@ -1,105 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"bytes"
"cmp"
"errors"
"sort"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/runtime"
"github.com/luxfi/utils"
)
// PlatformVM uses a single feature extension (secp256k1fx) so the XVM concept
// of an Fx index doesn't apply. We keep the layout (FxIndex + Outs) so that
// the wire bytes match the XVM CreateAssetTx; FxIndex is required to be zero
// (secp256k1fx) for every initial state on this VM.
const platformFxIndex = 0
var (
ErrNilInitialState = errors.New("nil initial state is not valid")
ErrNilInitialStateOut = errors.New("nil initial state output is not valid")
ErrInitialStateNonZero = errors.New("initial state fx index must be zero on P-Chain (single secp256k1fx)")
ErrInitialStateOutsBad = errors.New("initial state outputs are not sorted")
_ utils.Sortable[*InitialState] = (*InitialState)(nil)
)
// InitialState is the initial state of a single Fx for a new asset.
// On PlatformVM there is exactly one Fx (secp256k1fx, index 0) so [FxIndex]
// is required to be zero.
type InitialState struct {
FxIndex uint32 `serialize:"true" json:"fxIndex"`
Outs []verify.State `serialize:"true" json:"outputs"`
}
// InitRuntime is a no-op; verify.State outputs do not require runtime context.
func (is *InitialState) InitRuntime(_ *runtime.Runtime) {}
// Verify returns nil iff this InitialState is well formed.
func (is *InitialState) Verify(c pcodecs.Manager) error {
switch {
case is == nil:
return ErrNilInitialState
case is.FxIndex != platformFxIndex:
return ErrInitialStateNonZero
}
for _, out := range is.Outs {
if out == nil {
return ErrNilInitialStateOut
}
if err := out.Verify(); err != nil {
return err
}
}
if !isSortedState(is.Outs, c) {
return ErrInitialStateOutsBad
}
return nil
}
// Compare provides a deterministic ordering by FxIndex.
func (is *InitialState) Compare(other *InitialState) int {
return cmp.Compare(is.FxIndex, other.FxIndex)
}
// Sort orders the outputs of this InitialState by their byte representation.
func (is *InitialState) Sort(c pcodecs.Manager) {
sortState(is.Outs, c)
}
type innerSortState struct {
vers []verify.State
codec pcodecs.Manager
}
func (s *innerSortState) Less(i, j int) bool {
iV := s.vers[i]
jV := s.vers[j]
iBytes, err := s.codec.Marshal(CodecVersion, &iV)
if err != nil {
return false
}
jBytes, err := s.codec.Marshal(CodecVersion, &jV)
if err != nil {
return false
}
return bytes.Compare(iBytes, jBytes) == -1
}
func (s *innerSortState) Len() int { return len(s.vers) }
func (s *innerSortState) Swap(i, j int) { s.vers[j], s.vers[i] = s.vers[i], s.vers[j] }
func sortState(vers []verify.State, c pcodecs.Manager) {
sort.Sort(&innerSortState{vers: vers, codec: c})
}
func isSortedState(vers []verify.State, c pcodecs.Manager) bool {
return sort.IsSorted(&innerSortState{vers: vers, codec: c})
}
-3
View File
@@ -33,9 +33,6 @@ const (
kindSetL1ValidatorWeight
kindIncreaseL1ValidatorBalance
kindDisableL1Validator
kindSlashValidator
kindCreateAsset
kindOperation
)
// offKind is the fixed wire position of the discriminator (object offset 0).
-130
View File
@@ -1,130 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"bytes"
"errors"
"sort"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/node/vms/components/verify"
"github.com/luxfi/node/vms/pcodecs"
"github.com/luxfi/utils"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
// Operation expresses an action against a previously-minted asset's UTXOs
// (transfer, mint, burn, NFT-style state op). PlatformVM exposes only the
// secp256k1fx Fx so [Op] is a secp256k1fx-compatible verifiable input.
type Operation struct {
lux.Asset `serialize:"true"`
// UTXOIDs of UTXOs this operation consumes.
UTXOIDs []*lux.UTXOID `serialize:"true" json:"inputIDs"`
// Op is the action to perform. On P-Chain this is always a
// secp256k1fx-flavoured operation; the codec keeps the door open for
// additional Fx flavours by storing a verify.Verifiable.
Op verify.Verifiable `serialize:"true" json:"operation"`
}
var (
ErrNilOperation = errors.New("nil operation is not valid")
ErrNilFxOperation = errors.New("nil fx operation is not valid")
ErrNotSortedAndUniqueUTXOIDs = errors.New("utxo ids on operation are not sorted and unique")
// ErrUnsupportedOpType pins [Op] to the single Fx PlatformVM ships
// (secp256k1fx). Anything else — propertyfx, nftfx, a future Fx an
// attacker tries to slip in — is rejected at syntactic verification
// time before any state-modifying executor sees it.
ErrUnsupportedOpType = errors.New("operation op type is not supported on PlatformVM (secp256k1fx only)")
)
// Verify returns nil iff this Operation is well formed.
func (op *Operation) Verify() error {
switch {
case op == nil:
return ErrNilOperation
case op.Op == nil:
return ErrNilFxOperation
case !utils.IsSortedAndUnique(op.UTXOIDs):
return ErrNotSortedAndUniqueUTXOIDs
}
if _, ok := op.Op.(*secp256k1fx.MintOperation); !ok {
return ErrUnsupportedOpType
}
return verify.All(&op.Asset, op.Op)
}
// secp256k1fx is the only Fx on PlatformVM. The compile-time check guards
// against drift if a developer tries to wire a non-secp256k1fx operation in.
var _ = secp256k1fx.ID
// SortOperations sorts the given operations by their codec-marshalled bytes.
type operationAndCodec struct {
op *Operation
codec pcodecs.Manager
}
func (o *operationAndCodec) Compare(other *operationAndCodec) int {
oBytes, err := o.codec.Marshal(CodecVersion, o.op)
if err != nil {
return 0
}
otherBytes, err := o.codec.Marshal(CodecVersion, other.op)
if err != nil {
return 0
}
return bytes.Compare(oBytes, otherBytes)
}
func SortOperations(ops []*Operation, c pcodecs.Manager) {
wrapped := make([]*operationAndCodec, len(ops))
for i, op := range ops {
wrapped[i] = &operationAndCodec{op: op, codec: c}
}
utils.Sort(wrapped)
for i, w := range wrapped {
ops[i] = w.op
}
}
// IsSortedAndUniqueOperations reports whether [ops] is sorted by codec bytes
// and contains no duplicates.
func IsSortedAndUniqueOperations(ops []*Operation, c pcodecs.Manager) bool {
wrapped := make([]*operationAndCodec, len(ops))
for i, op := range ops {
wrapped[i] = &operationAndCodec{op: op, codec: c}
}
return utils.IsSortedAndUnique(wrapped)
}
type innerSortOperationsWithSigners struct {
ops []*Operation
signers [][]*secp256k1.PrivateKey
codec pcodecs.Manager
}
func (s *innerSortOperationsWithSigners) Less(i, j int) bool {
iBytes, err := s.codec.Marshal(CodecVersion, s.ops[i])
if err != nil {
return false
}
jBytes, err := s.codec.Marshal(CodecVersion, s.ops[j])
if err != nil {
return false
}
return bytes.Compare(iBytes, jBytes) == -1
}
func (s *innerSortOperationsWithSigners) Len() int { return len(s.ops) }
func (s *innerSortOperationsWithSigners) Swap(i, j int) {
s.ops[j], s.ops[i] = s.ops[i], s.ops[j]
s.signers[j], s.signers[i] = s.signers[i], s.signers[j]
}
func SortOperationsWithSigners(ops []*Operation, signers [][]*secp256k1.PrivateKey, c pcodecs.Manager) {
sort.Sort(&innerSortOperationsWithSigners{ops: ops, signers: signers, codec: c})
}
-121
View File
@@ -1,121 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"errors"
"github.com/luxfi/runtime"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
var (
_ UnsignedTx = (*OperationTx)(nil)
_ secp256k1fx.UnsignedTx = (*OperationTx)(nil)
ErrNilOperationTx = errors.New("operation tx is nil")
ErrNoOperations = errors.New("operation tx must have at least one operation")
ErrOperationsNotSortedUnique = errors.New("operations are not sorted and unique")
ErrOperationDoubleSpend = errors.New("operations attempt to double spend an input")
// ErrWrongNumberOfCredentials catches malformed wire payloads where the
// peer sent fewer (or more) credentials than [NumCredentials] requires.
// Without this check the semantic executor would slice past the end of
// [tx.Creds] and panic on op credentials.
ErrWrongNumberOfCredentials = errors.New("wrong number of credentials")
)
// OperationTx applies one or more [Operation]s against previously minted
// assets. Like BaseTx it consumes inputs and produces outputs for fees; the
// authoritative side-effect is the operations.
type OperationTx struct {
BaseTx `serialize:"true"`
Ops []*Operation `serialize:"true" json:"operations"`
}
func (tx *OperationTx) InitRuntime(rt *runtime.Runtime) {
tx.BaseTx.InitRuntime(rt)
}
// Operations returns the slice of operations carried by this tx. The
// returned slice MUST NOT be mutated.
func (tx *OperationTx) Operations() []*Operation {
return tx.Ops
}
// InputIDs returns the set of UTXOIDs spent across BaseTx and every Operation.
func (tx *OperationTx) InputIDs() set.Set[ids.ID] {
inputs := tx.BaseTx.InputIDs()
for _, op := range tx.Ops {
for _, utxo := range op.UTXOIDs {
inputs.Add(utxo.InputID())
}
}
return inputs
}
// OperationInputs returns the UTXOIDs consumed by the operations only
// (BaseTx inputs are returned by [BaseTx.InputIDs]).
func (tx *OperationTx) OperationInputs() []*lux.UTXOID {
utxos := make([]*lux.UTXOID, 0)
for _, op := range tx.Ops {
utxos = append(utxos, op.UTXOIDs...)
}
return utxos
}
// NumCredentials returns the number of credentials expected when this tx is
// signed (one per BaseTx input plus one per Operation).
func (tx *OperationTx) NumCredentials() int {
return len(tx.Ins) + len(tx.Ops)
}
// SyntacticVerify returns nil iff this tx is well-formed.
func (tx *OperationTx) SyntacticVerify(rt *runtime.Runtime) error {
switch {
case tx == nil:
return ErrNilOperationTx
case tx.SyntacticallyVerified:
return nil
case len(tx.Ops) == 0:
return ErrNoOperations
}
if err := tx.BaseTx.SyntacticVerify(rt); err != nil {
return err
}
// Operation UTXOIDs must not collide with BaseTx input UTXOIDs and must
// not collide with each other across operations.
inputs := set.NewSet[ids.ID](len(tx.Ins))
for _, in := range tx.Ins {
inputs.Add(in.InputID())
}
for _, op := range tx.Ops {
if err := op.Verify(); err != nil {
return err
}
for _, utxoID := range op.UTXOIDs {
inputID := utxoID.InputID()
if inputs.Contains(inputID) {
return ErrOperationDoubleSpend
}
inputs.Add(inputID)
}
}
if !IsSortedAndUniqueOperations(tx.Ops, Codec) {
return ErrOperationsNotSortedUnique
}
tx.SyntacticallyVerified = true
return nil
}
func (tx *OperationTx) Visit(v Visitor) error {
return v.OperationTx(tx)
}
-6
View File
@@ -81,12 +81,6 @@ func parseUnsigned(buf []byte) (UnsignedTx, error) {
return &IncreaseL1ValidatorBalanceTx{spendingTx{msg: msg}}, nil
case kindDisableL1Validator:
return &DisableL1ValidatorTx{spendingTx{msg: msg}}, nil
case kindSlashValidator:
return &SlashValidatorTx{spendingTx{msg: msg}}, nil
case kindCreateAsset:
return &CreateAssetTx{spendingTx{msg: msg}}, nil
case kindOperation:
return &OperationTx{spendingTx{msg: msg}}, nil
default:
return nil, fmt.Errorf("zap: unknown tx kind %d", k)
}
@@ -1,116 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
// TestHIGH1_SyntacticVerifyNilRuntimeDoesNotPanic verifies that calling
// SyntacticVerify with a nil runtime returns an error instead of panicking.
//
// Before the fix, SyntacticVerify(nil) would dereference the nil runtime
// in BaseTx.SyntacticVerify, causing a panic.
func TestHIGH1_SyntacticVerifyNilRuntimeDoesNotPanic(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
msgA := []byte("block-A")
msgB := []byte("block-B")
sigA := bls.SignatureToBytes(bls.Sign(sk, msgA))
sigB := bls.SignatureToBytes(bls.Sign(sk, msgB))
tx := &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
Evidence: SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: msgA,
SignatureA: sigA,
MessageB: msgB,
SignatureB: sigB,
},
SlashPercentage: 100_000,
}
// Must not panic, must return errNilRuntime
err = tx.SyntacticVerify(nil)
require.ErrorIs(t, err, errNilRuntime,
"SyntacticVerify(nil) must return errNilRuntime, not panic")
}
// TestHIGH1_SyntacticVerifyNilTxStillWorks verifies that nil tx check
// still takes priority over nil runtime.
func TestHIGH1_SyntacticVerifyNilTxStillWorks(t *testing.T) {
var tx *SlashValidatorTx
err := tx.SyntacticVerify(nil)
require.ErrorIs(t, err, ErrNilTx)
}
// TestHIGH1_StructuralChecksStillWorkWithNilRuntime verifies that
// structural checks (NodeID, slash percentage, evidence) execute before
// the nil runtime guard, so they still produce the correct errors.
func TestHIGH1_StructuralChecksStillWorkWithNilRuntime(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
sigBytes := bls.SignatureToBytes(bls.Sign(sk, []byte("x")))
tests := []struct {
name string
tx *SlashValidatorTx
wantErr error
}{
{
name: "empty node ID",
tx: &SlashValidatorTx{
NodeID: ids.EmptyNodeID,
SlashPercentage: 100_000,
},
wantErr: errEmptyNodeID,
},
{
name: "zero slash percentage",
tx: &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
SlashPercentage: 0,
},
wantErr: errNoEvidence,
},
{
name: "slash percentage too large",
tx: &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
SlashPercentage: 1_000_001,
},
wantErr: errSlashPercentTooLarge,
},
{
name: "invalid evidence type with nil runtime",
tx: &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
Evidence: SlashEvidence{
Type: 0, // invalid
MessageA: []byte("a"),
SignatureA: sigBytes,
MessageB: []byte("b"),
SignatureB: sigBytes,
},
SlashPercentage: 100_000,
},
wantErr: errInvalidEvidenceType,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.tx.SyntacticVerify(nil)
require.ErrorIs(t, err, tt.wantErr)
})
}
}
-152
View File
@@ -1,152 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"context"
"errors"
"fmt"
"github.com/luxfi/runtime"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
lux "github.com/luxfi/utxo"
)
var (
_ UnsignedTx = (*SlashValidatorTx)(nil)
errNoEvidence = errors.New("no equivocation evidence provided")
errInvalidEvidenceType = errors.New("invalid evidence type")
errSameContent = errors.New("evidence messages are identical")
errEmptySignature = errors.New("evidence contains empty signature")
errEmptyMessage = errors.New("evidence contains empty message")
errSlashPercentTooLarge = errors.New("slash percentage exceeds 100%")
errNilRuntime = errors.New("runtime is nil")
)
// EvidenceType classifies the equivocation.
type EvidenceType uint8
const (
DoubleVoteEvidence EvidenceType = iota + 1
DoubleSignEvidence
)
// SlashEvidence contains two conflicting signed messages from the same
// validator at the same consensus height. Both signatures must verify
// against the validator's registered BLS public key.
type SlashEvidence struct {
// Height at which the equivocation occurred
Height uint64 `serialize:"true" json:"height"`
// Type of equivocation
Type EvidenceType `serialize:"true" json:"type"`
// First signed message (vote or block hash)
MessageA []byte `serialize:"true" json:"messageA"`
// BLS signature over MessageA
SignatureA []byte `serialize:"true" json:"signatureA"`
// Second conflicting signed message at the same height
MessageB []byte `serialize:"true" json:"messageB"`
// BLS signature over MessageB
SignatureB []byte `serialize:"true" json:"signatureB"`
}
// SlashValidatorTx removes a percentage of a validator's stake as
// punishment for provable equivocation (double-vote or double-sign).
// The slashed amount is burned. Anyone can submit this transaction
// with valid evidence.
type SlashValidatorTx struct {
BaseTx `serialize:"true"`
// NodeID of the validator to slash
NodeID ids.NodeID `serialize:"true" json:"nodeID"`
// Cryptographic proof of equivocation
Evidence SlashEvidence `serialize:"true" json:"evidence"`
// Percentage of stake to slash, in units of reward.PercentDenominator
// (e.g., 100_000 = 10%). Set by the submitter but capped/verified by
// the executor against chain parameters.
SlashPercentage uint32 `serialize:"true" json:"slashPercentage"`
}
func (tx *SlashValidatorTx) SyntacticVerify(rt *runtime.Runtime) error {
switch {
case tx == nil:
return ErrNilTx
case tx.SyntacticallyVerified:
return nil
case tx.NodeID == ids.EmptyNodeID:
return errEmptyNodeID
case tx.SlashPercentage == 0:
return errNoEvidence
case tx.SlashPercentage > 1_000_000: // reward.PercentDenominator
return errSlashPercentTooLarge
}
if err := tx.Evidence.Verify(); err != nil {
return fmt.Errorf("invalid slash evidence: %w", err)
}
if rt == nil {
return errNilRuntime
}
if err := tx.BaseTx.SyntacticVerify(rt); err != nil {
return fmt.Errorf("failed to verify BaseTx: %w", err)
}
tx.SyntacticallyVerified = true
return nil
}
// Verify checks that the evidence is structurally valid. It does NOT
// verify BLS signatures (that requires the validator's public key from
// state and is done in the executor).
func (e *SlashEvidence) Verify() error {
switch {
case e.Type != DoubleVoteEvidence && e.Type != DoubleSignEvidence:
return errInvalidEvidenceType
case len(e.MessageA) == 0 || len(e.MessageB) == 0:
return errEmptyMessage
case len(e.SignatureA) == 0 || len(e.SignatureB) == 0:
return errEmptySignature
case len(e.SignatureA) != bls.SignatureLen || len(e.SignatureB) != bls.SignatureLen:
return errEmptySignature
}
// Messages must differ -- same content is not equivocation
if len(e.MessageA) == len(e.MessageB) {
same := true
for i := range e.MessageA {
if e.MessageA[i] != e.MessageB[i] {
same = false
break
}
}
if same {
return errSameContent
}
}
return nil
}
func (tx *SlashValidatorTx) Visit(visitor Visitor) error {
return visitor.SlashValidatorTx(tx)
}
func (*SlashValidatorTx) InputIDs() set.Set[ids.ID] {
return nil
}
func (*SlashValidatorTx) Outputs() []*lux.TransferableOutput {
return nil
}
func (tx *SlashValidatorTx) InitRuntime(rt *runtime.Runtime) {
tx.BaseTx.InitRuntime(rt)
}
func (tx *SlashValidatorTx) Initialize(ctx context.Context) error {
return nil
}
@@ -1,366 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
import (
"bytes"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
func TestSlashEvidenceVerify(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
msgA := []byte("block-hash-A")
msgB := []byte("block-hash-B")
sigA := bls.Sign(sk, msgA)
sigB := bls.Sign(sk, msgB)
sigABytes := bls.SignatureToBytes(sigA)
sigBBytes := bls.SignatureToBytes(sigB)
tests := []struct {
name string
ev SlashEvidence
wantErr error
}{
{
name: "valid double-vote evidence",
ev: SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: msgA,
SignatureA: sigABytes,
MessageB: msgB,
SignatureB: sigBBytes,
},
wantErr: nil,
},
{
name: "valid double-sign evidence",
ev: SlashEvidence{
Height: 100,
Type: DoubleSignEvidence,
MessageA: msgA,
SignatureA: sigABytes,
MessageB: msgB,
SignatureB: sigBBytes,
},
wantErr: nil,
},
{
name: "invalid evidence type",
ev: SlashEvidence{
Height: 100,
Type: 0,
MessageA: msgA,
SignatureA: sigABytes,
MessageB: msgB,
SignatureB: sigBBytes,
},
wantErr: errInvalidEvidenceType,
},
{
name: "same messages",
ev: SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: msgA,
SignatureA: sigABytes,
MessageB: msgA,
SignatureB: sigABytes,
},
wantErr: errSameContent,
},
{
name: "empty message A",
ev: SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: nil,
SignatureA: sigABytes,
MessageB: msgB,
SignatureB: sigBBytes,
},
wantErr: errEmptyMessage,
},
{
name: "empty signature A",
ev: SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: msgA,
SignatureA: nil,
MessageB: msgB,
SignatureB: sigBBytes,
},
wantErr: errEmptySignature,
},
{
name: "wrong signature length",
ev: SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: msgA,
SignatureA: []byte("short"),
MessageB: msgB,
SignatureB: sigBBytes,
},
wantErr: errEmptySignature,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.ev.Verify()
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestSlashValidatorTxSyntacticVerify(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
msgA := []byte("block-hash-A")
msgB := []byte("block-hash-B")
sigA := bls.SignatureToBytes(bls.Sign(sk, msgA))
sigB := bls.SignatureToBytes(bls.Sign(sk, msgB))
validEvidence := SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: msgA,
SignatureA: sigA,
MessageB: msgB,
SignatureB: sigB,
}
tests := []struct {
name string
tx *SlashValidatorTx
wantErr error
}{
{
name: "nil tx",
tx: nil,
wantErr: ErrNilTx,
},
{
name: "empty node ID",
tx: &SlashValidatorTx{
NodeID: ids.EmptyNodeID,
Evidence: validEvidence,
SlashPercentage: 100_000,
},
wantErr: errEmptyNodeID,
},
{
name: "zero slash percentage",
tx: &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
Evidence: validEvidence,
SlashPercentage: 0,
},
wantErr: errNoEvidence,
},
{
name: "slash percentage too large",
tx: &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
Evidence: validEvidence,
SlashPercentage: 1_000_001,
},
wantErr: errSlashPercentTooLarge,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.tx.SyntacticVerify(nil)
require.ErrorIs(t, err, tt.wantErr)
})
}
}
// --- Additional inversion tests ---
// TestSlashEvidence_ForgedSignature_WrongKey verifies that evidence signed
// by a different BLS key fails structural verification at the signature level.
// Note: SlashEvidence.Verify() only checks structure, not BLS verification.
// But the signature bytes from a different key are still well-formed, so
// Verify() passes. The BLS-level check happens in the executor.
// This test documents that the structural check is NOT a crypto check.
func TestSlashEvidence_ForgedSignature_WrongKey(t *testing.T) {
// Generate two distinct key pairs
skReal, err := bls.NewSecretKey()
require.NoError(t, err)
skForger, err := bls.NewSecretKey()
require.NoError(t, err)
pkReal := bls.PublicFromSecretKey(skReal)
msgA := []byte("real-block-A")
msgB := []byte("real-block-B")
// Forger signs with their own key
forgedSigA := bls.Sign(skForger, msgA)
forgedSigB := bls.Sign(skForger, msgB)
ev := SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: msgA,
SignatureA: bls.SignatureToBytes(forgedSigA),
MessageB: msgB,
SignatureB: bls.SignatureToBytes(forgedSigB),
}
// Structural verification passes (bytes are valid BLS signature length)
require.NoError(t, ev.Verify())
// But BLS verification against the REAL key must fail
sigA, err := bls.SignatureFromBytes(ev.SignatureA)
require.NoError(t, err)
require.False(t, bls.Verify(pkReal, sigA, msgA),
"forged signature must not verify against real public key")
}
// TestSlashValidatorTx_SlashPercentBoundary verifies that 1_000_001 (>100%)
// is rejected by the switch-case BEFORE evidence verification or BaseTx.
// We also verify that 1_000_000 (exactly 100%) does NOT hit errSlashPercentTooLarge.
func TestSlashValidatorTx_SlashPercentBoundary(t *testing.T) {
// 1_000_001 -- exceeds 100%, must be caught by the switch-case
tx101 := &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
Evidence: SlashEvidence{}, // doesn't matter, rejected before evidence check
SlashPercentage: 1_000_001,
}
err := tx101.SyntacticVerify(nil)
require.ErrorIs(t, err, errSlashPercentTooLarge)
// 1_000_000 = exactly at limit. The switch checks > 1_000_000, so this passes.
// It will fail LATER at evidence.Verify() or BaseTx level, but NOT at the
// slash percentage check. We verify the specific error is NOT errSlashPercentTooLarge.
tx100 := &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
Evidence: SlashEvidence{Type: 0}, // will fail at evidence type check
SlashPercentage: 1_000_000,
}
err = tx100.SyntacticVerify(nil)
require.Error(t, err) // fails at evidence, not at percentage
require.NotErrorIs(t, err, errSlashPercentTooLarge)
// 0 -- zero slash is rejected as "no evidence"
tx0 := &SlashValidatorTx{
NodeID: ids.GenerateTestNodeID(),
Evidence: SlashEvidence{},
SlashPercentage: 0,
}
err = tx0.SyntacticVerify(nil)
require.ErrorIs(t, err, errNoEvidence)
}
// TestSlashEvidence_BothMessagesEmpty verifies that empty messages are rejected.
func TestSlashEvidence_BothMessagesEmpty(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
sig := bls.SignatureToBytes(bls.Sign(sk, []byte("x")))
ev := SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: nil,
SignatureA: sig,
MessageB: nil,
SignatureB: sig,
}
require.ErrorIs(t, ev.Verify(), errEmptyMessage)
}
// TestSlashEvidence_MessageBEmpty verifies that only MessageB being empty
// is still rejected.
func TestSlashEvidence_MessageBEmpty(t *testing.T) {
sk, err := bls.NewSecretKey()
require.NoError(t, err)
sig := bls.SignatureToBytes(bls.Sign(sk, []byte("x")))
ev := SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: []byte("valid"),
SignatureA: sig,
MessageB: nil,
SignatureB: sig,
}
require.ErrorIs(t, ev.Verify(), errEmptyMessage)
}
// TestSlashEvidence_SignatureTooShort verifies rejection of truncated sigs.
func TestSlashEvidence_SignatureTooShort(t *testing.T) {
ev := SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: []byte("A"),
SignatureA: make([]byte, bls.SignatureLen-1), // one byte too short
MessageB: []byte("B"),
SignatureB: make([]byte, bls.SignatureLen),
}
require.ErrorIs(t, ev.Verify(), errEmptySignature)
}
// TestSlashEvidence_SignatureTooLong verifies rejection of oversized sigs.
func TestSlashEvidence_SignatureTooLong(t *testing.T) {
ev := SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: []byte("A"),
SignatureA: make([]byte, bls.SignatureLen+1), // one byte too long
MessageB: []byte("B"),
SignatureB: make([]byte, bls.SignatureLen),
}
require.ErrorIs(t, ev.Verify(), errEmptySignature)
}
func TestSlashEvidenceVerifySignatures(t *testing.T) {
// Verify that the evidence round-trips through BLS correctly
sk, err := bls.NewSecretKey()
require.NoError(t, err)
pk := bls.PublicFromSecretKey(sk)
msgA := []byte("vote-for-block-A-at-height-100")
msgB := []byte("vote-for-block-B-at-height-100")
sigA := bls.Sign(sk, msgA)
sigB := bls.Sign(sk, msgB)
// Both signatures should verify against the public key
require.True(t, bls.Verify(pk, sigA, msgA))
require.True(t, bls.Verify(pk, sigB, msgB))
// Cross-verification should fail
require.False(t, bls.Verify(pk, sigA, msgB))
require.False(t, bls.Verify(pk, sigB, msgA))
// Evidence struct should pass structural verification
ev := SlashEvidence{
Height: 100,
Type: DoubleVoteEvidence,
MessageA: msgA,
SignatureA: bls.SignatureToBytes(sigA),
MessageB: msgB,
SignatureB: bls.SignatureToBytes(sigB),
}
require.NoError(t, ev.Verify())
// Verify messages are actually different
require.False(t, bytes.Equal(msgA, msgB))
}
-3
View File
@@ -528,6 +528,3 @@ func (v *visitor) RegisterL1ValidatorTx(*RegisterL1ValidatorTx) error
func (v *visitor) SetL1ValidatorWeightTx(*SetL1ValidatorWeightTx) error { return nil }
func (v *visitor) DisableL1ValidatorTx(*DisableL1ValidatorTx) error { return nil }
func (v *visitor) IncreaseL1ValidatorBalanceTx(*IncreaseL1ValidatorBalanceTx) error { return nil }
func (v *visitor) SlashValidatorTx(*SlashValidatorTx) error { return nil }
func (v *visitor) CreateAssetTx(*CreateAssetTx) error { return nil }
func (v *visitor) OperationTx(*OperationTx) error { return nil }
-7
View File
@@ -36,11 +36,4 @@ type Visitor interface {
SetL1ValidatorWeightTx(*SetL1ValidatorWeightTx) error
IncreaseL1ValidatorBalanceTx(*IncreaseL1ValidatorBalanceTx) error
DisableL1ValidatorTx(*DisableL1ValidatorTx) error
SlashValidatorTx(*SlashValidatorTx) error
// P-only primary network — historically X-Chain
// (kept on PlatformVM, opt-in for X-Chain when present):
CreateAssetTx(*CreateAssetTx) error
OperationTx(*OperationTx) error
}
-12
View File
@@ -191,18 +191,6 @@ func (v *backendVisitor) DisableL1ValidatorTx(tx *txs.DisableL1ValidatorTx) erro
return v.baseTx(&tx.BaseTx)
}
func (v *backendVisitor) SlashValidatorTx(tx *txs.SlashValidatorTx) error {
return v.baseTx(&tx.BaseTx)
}
func (v *backendVisitor) CreateAssetTx(tx *txs.CreateAssetTx) error {
return v.baseTx(&tx.BaseTx)
}
func (v *backendVisitor) OperationTx(tx *txs.OperationTx) error {
return v.baseTx(&tx.BaseTx)
}
func (v *backendVisitor) baseTx(tx *txs.BaseTx) error {
return v.b.removeUTXOs(v.ctx, constants.PlatformChainID, tx.InputIDs())
}
-89
View File
@@ -31,8 +31,6 @@ var (
ErrUnknownAuthType = errors.New("unknown auth type")
ErrUnknownOwnerType = errors.New("unknown owner type")
ErrUnknownCredentialType = errors.New("unknown credential type")
ErrUnknownOpType = errors.New("unknown op type")
ErrInvalidNumUTXOsInOp = errors.New("invalid number of UTXOs in operation")
emptySig [secp256k1.SignatureLen]byte
)
@@ -246,93 +244,6 @@ func (s *visitor) DisableL1ValidatorTx(tx *txs.DisableL1ValidatorTx) error {
return sign(s.tx, true, txSigners)
}
func (s *visitor) SlashValidatorTx(tx *txs.SlashValidatorTx) error {
txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins)
if err != nil {
return err
}
return sign(s.tx, true, txSigners)
}
// CreateAssetTx signs the BaseTx fee inputs that pay for asset creation.
// No additional auth required beyond the input credentials.
func (s *visitor) CreateAssetTx(tx *txs.CreateAssetTx) error {
txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins)
if err != nil {
return err
}
return sign(s.tx, false, txSigners)
}
// OperationTx signs BaseTx fee inputs plus, for each operation, the
// signature indices required by the consumed UTXO outputs. Mirrors
// `wallet/chain/x/signer/visitor.go::getOpsSigners` so the P-only
// secp256k1fx path produces credentials the semantic verifier will accept.
func (s *visitor) OperationTx(tx *txs.OperationTx) error {
txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins)
if err != nil {
return err
}
opsSigners, err := s.getOpsSigners(constants.PlatformChainID, tx.Ops)
if err != nil {
return err
}
txSigners = append(txSigners, opsSigners...)
return sign(s.tx, false, txSigners)
}
// getOpsSigners resolves the per-op credential signers from the consumed
// UTXOs. PlatformVM ships only secp256k1fx so the operation must carry a
// *secp256k1fx.MintOperation; the consumed UTXO output must be a
// *secp256k1fx.MintOutput (the asset's mint authority). Any other shape is
// rejected — the same gate the syntactic verifier and codec both apply.
func (s *visitor) getOpsSigners(
sourceChainID ids.ID,
ops []*txs.Operation,
) ([][]keychain.Signer, error) {
txSigners := make([][]keychain.Signer, len(ops))
for credIndex, op := range ops {
mintOp, ok := op.Op.(*secp256k1fx.MintOperation)
if !ok {
return nil, ErrUnknownOpType
}
input := &mintOp.MintInput
inputSigners := make([]keychain.Signer, len(input.SigIndices))
txSigners[credIndex] = inputSigners
if len(op.UTXOIDs) != 1 {
return nil, ErrInvalidNumUTXOsInOp
}
utxoID := op.UTXOIDs[0].InputID()
utxo, err := s.backend.GetUTXO(s.ctx, sourceChainID, utxoID)
if err == database.ErrNotFound {
// No access to the UTXO — partial sign and let the caller
// merge a populated credential later.
continue
}
if err != nil {
return nil, err
}
mintOut, ok := utxo.Out.(*secp256k1fx.MintOutput)
if !ok {
return nil, ErrUnknownOutputType
}
for sigIndex, addrIndex := range input.SigIndices {
if addrIndex >= uint32(len(mintOut.Addrs)) {
return nil, ErrInvalidUTXOSigIndex
}
addr := mintOut.Addrs[addrIndex]
key, ok := s.kc.Get(addr)
if !ok {
continue
}
inputSigners[sigIndex] = key
}
}
return txSigners, nil
}
func (s *visitor) getSigners(sourceChainID ids.ID, ins []*lux.TransferableInput) ([][]keychain.Signer, error) {
txSigners := make([][]keychain.Signer, len(ins))
for credIndex, transferInput := range ins {
-82
View File
@@ -31,8 +31,6 @@ var (
errUnknownOutputType = errors.New("unknown output type")
errUnknownChainAuthType = errors.New("unknown net auth type")
errInvalidUTXOSigIndex = errors.New("invalid UTXO signature index")
errUnknownOpType = errors.New("unknown op type")
errInvalidNumUTXOsInOp = errors.New("invalid number of UTXOs in operation")
emptySig [secp256k1.SignatureLen]byte
)
@@ -363,15 +361,6 @@ func (s *signerVisitor) DisableL1ValidatorTx(tx *txs.DisableL1ValidatorTx) error
return sign(s.tx, false, txSigners)
}
// SlashValidatorTx signs a SlashValidatorTx
func (s *signerVisitor) SlashValidatorTx(tx *txs.SlashValidatorTx) error {
txSigners, err := s.getSigners(constants.PrimaryNetworkID, tx.Ins)
if err != nil {
return err
}
return sign(s.tx, false, txSigners)
}
// IncreaseL1ValidatorBalanceTx signs an IncreaseL1ValidatorBalanceTx
func (s *signerVisitor) IncreaseL1ValidatorBalanceTx(tx *txs.IncreaseL1ValidatorBalanceTx) error {
txSigners, err := s.getSigners(constants.PrimaryNetworkID, tx.Ins)
@@ -420,74 +409,3 @@ func (s *signerVisitor) CreateSovereignL1Tx(tx *txs.CreateSovereignL1Tx) error {
}
return sign(s.tx, false, txSigners)
}
// CreateAssetTx signs a CreateAssetTx (BaseTx fee inputs only).
func (s *signerVisitor) CreateAssetTx(tx *txs.CreateAssetTx) error {
txSigners, err := s.getSigners(constants.PrimaryNetworkID, tx.Ins)
if err != nil {
return err
}
return sign(s.tx, false, txSigners)
}
// OperationTx signs an OperationTx (BaseTx fee inputs + per-operation slots).
// Mirrors XVM's `wallet/chain/x/signer/visitor.go::getOpsSigners`: each Op
// must be a *secp256k1fx.MintOperation, and the credential signs over the
// MintOutput owners of the consumed UTXO.
func (s *signerVisitor) OperationTx(tx *txs.OperationTx) error {
txSigners, err := s.getSigners(constants.PrimaryNetworkID, tx.Ins)
if err != nil {
return err
}
opsSigners, err := s.getOpsSigners(constants.PrimaryNetworkID, tx.Ops)
if err != nil {
return err
}
txSigners = append(txSigners, opsSigners...)
return sign(s.tx, false, txSigners)
}
func (s *signerVisitor) getOpsSigners(
sourceChainID ids.ID,
ops []*txs.Operation,
) ([][]keychain.Signer, error) {
txSigners := make([][]keychain.Signer, len(ops))
for credIndex, op := range ops {
mintOp, ok := op.Op.(*secp256k1fx.MintOperation)
if !ok {
return nil, errUnknownOpType
}
input := &mintOp.MintInput
inputSigners := make([]keychain.Signer, len(input.SigIndices))
txSigners[credIndex] = inputSigners
if len(op.UTXOIDs) != 1 {
return nil, errInvalidNumUTXOsInOp
}
utxoID := op.UTXOIDs[0].InputID()
utxo, err := s.backend.GetUTXO(s.ctx, sourceChainID, utxoID)
if err == database.ErrNotFound {
continue
}
if err != nil {
return nil, err
}
mintOut, ok := utxo.Out.(*secp256k1fx.MintOutput)
if !ok {
return nil, errUnknownOutputType
}
for sigIndex, addrIndex := range input.SigIndices {
if addrIndex >= uint32(len(mintOut.Addrs)) {
return nil, errInvalidUTXOSigIndex
}
addr := mintOut.Addrs[addrIndex]
key, ok := s.kc.Get(addr)
if !ok {
continue
}
inputSigners[sigIndex] = key
}
}
return txSigners, nil
}
-15
View File
@@ -179,21 +179,6 @@ func (b *backendVisitor) DisableL1ValidatorTx(tx *txs.DisableL1ValidatorTx) erro
return b.baseTx(&tx.BaseTx)
}
func (b *backendVisitor) SlashValidatorTx(tx *txs.SlashValidatorTx) error {
return b.baseTx(&tx.BaseTx)
}
func (b *backendVisitor) CreateAssetTx(tx *txs.CreateAssetTx) error {
// Fee inputs are consumed; minted asset outputs become locally tracked
// UTXOs owned by the backend so subsequent OperationTx can spend them.
return b.baseTx(&tx.BaseTx)
}
func (b *backendVisitor) OperationTx(tx *txs.OperationTx) error {
// Operation UTXOIDs are tracked through InputIDs() (see OperationTx).
return b.baseTx(&tx.BaseTx)
}
func (b *backendVisitor) baseTx(tx *txs.BaseTx) error {
return b.b.removeUTXOs(
b.ctx,