mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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%%")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -33,9 +33,6 @@ const (
|
||||
kindSetL1ValidatorWeight
|
||||
kindIncreaseL1ValidatorBalance
|
||||
kindDisableL1Validator
|
||||
kindSlashValidator
|
||||
kindCreateAsset
|
||||
kindOperation
|
||||
)
|
||||
|
||||
// offKind is the fixed wire position of the discriminator (object offset 0).
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user