Files
node/vms/platformvm/network/zap_native_admission_test.go
T

249 lines
9.2 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package network
import (
"errors"
"testing"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/proto/zap_native"
)
// LP-023 Red round 7 R7V5 — admission-gate tests.
//
// Threat model: an adversary submits a tx whose kind has no executor
// body yet. Without the gate, the executor would return
// "not yet implemented" mid-block — by which time the tx has already
// been gossiped, sat in the mempool, and consumed RAM. The gate
// REFUSES at admission so the tx never enters the mempool at all.
// errSentinel is the inner-verifier sentinel used to confirm the gate
// delegates to the wrapped verifier when admission is allowed.
var errSentinel = errors.New("inner verifier hit")
// passThroughVerifier returns errSentinel for every tx — used to assert
// that the gate ALWAYS calls into the inner verifier for non-gated kinds.
type passThroughVerifier struct{}
func (passThroughVerifier) VerifyTx(*txs.Tx) error { return errSentinel }
// blockEverythingVerifier returns errSentinel for every tx — used as a
// canary to confirm gated kinds never reach the inner verifier.
type blockEverythingVerifier struct{}
func (blockEverythingVerifier) VerifyTx(*txs.Tx) error { return errSentinel }
// TestZapNativeAdmissionGate_RejectsCreateSovereignL1Tx pins R7V5: the
// gate must refuse a legacy *txs.CreateSovereignL1Tx with
// ErrZapNativeNotYetExecutable, BEFORE the inner verifier sees the tx.
// The inner verifier here returns errSentinel for everything — if it
// fires we'd see errSentinel, not ErrZapNativeNotYetExecutable.
func TestZapNativeAdmissionGate_RejectsCreateSovereignL1Tx(t *testing.T) {
gate := NewZapNativeAdmissionGate(blockEverythingVerifier{})
tx := &txs.Tx{Unsigned: &txs.CreateSovereignL1Tx{}}
err := gate.VerifyTx(tx)
if !errors.Is(err, ErrZapNativeNotYetExecutable) {
t.Fatalf(
"gate.VerifyTx(CreateSovereignL1Tx) = %v, want ErrZapNativeNotYetExecutable",
err,
)
}
if errors.Is(err, errSentinel) {
t.Fatalf("gate let CreateSovereignL1Tx reach the inner verifier: %v", err)
}
}
// TestZapNativeAdmissionGate_PassThroughLegacyTypes pins the brief:
// "Legacy txs.ConvertNetworkToL1Tx / txs.RegisterL1ValidatorTx (the working
// ones) still pass through". For txs.BaseTx (working executor) and
// txs.RegisterL1ValidatorTx + txs.ConvertNetworkToL1Tx (legacy
// executors at line 639/761), the gate must NOT fire; the inner
// verifier MUST see the tx (we assert via errSentinel).
func TestZapNativeAdmissionGate_PassThroughLegacyTypes(t *testing.T) {
gate := NewZapNativeAdmissionGate(passThroughVerifier{})
cases := []struct {
name string
tx *txs.Tx
}{
{"BaseTx", &txs.Tx{Unsigned: &txs.BaseTx{}}},
{"RegisterL1ValidatorTx", &txs.Tx{Unsigned: &txs.RegisterL1ValidatorTx{}}},
{"ConvertNetworkToL1Tx", &txs.Tx{Unsigned: &txs.ConvertNetworkToL1Tx{}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := gate.VerifyTx(tc.tx)
if !errors.Is(err, errSentinel) {
t.Fatalf(
"gate.VerifyTx(%s) = %v, want errSentinel (gate must pass to inner)",
tc.name, err,
)
}
if errors.Is(err, ErrZapNativeNotYetExecutable) {
t.Fatalf(
"gate FALSE-POSITIVE on %s: returned ErrZapNativeNotYetExecutable for a kind with a live executor",
tc.name,
)
}
})
}
}
// TestZapNativeAdmissionGate_RejectsZapWireCreateSovereignL1 exercises
// the future-proof half: a wire buffer that PARSES as zap_native.
// CreateSovereignL1Tx (TxKind=23) must be refused even if the legacy
// Unsigned struct field is something innocuous. The gate inspects the
// signed bytes via zap_native.IsZAPBytes + Wrap*Tx; the kind discriminator
// is the truth source.
func TestZapNativeAdmissionGate_RejectsZapWireCreateSovereignL1(t *testing.T) {
stub := zap_native.OwnerStub{Threshold: 1, Locktime: 0, Address: ids.ShortID{0x42}}
zapTx := zap_native.NewCreateSovereignL1Tx(zap_native.CreateSovereignL1TxInput{
NetworkID: 1,
Owner: stub,
Validators: []zap_native.ValidatorsListEntry{newWireFriendlyValidator(t)},
Chains: []zap_native.ChainsListEntry{
{Name: []byte("evm"), VMID: ids.ID{0xed, 0xed}, FxIDs: []ids.ID{{0x01}}, GenesisData: []byte("g")},
},
})
wireBytes := zapTx.Bytes()
if !zap_native.IsZAPBytes(wireBytes) {
t.Fatalf("baseline: expected wire bytes to be ZAP-magic, got prefix=%q", string(wireBytes[:4]))
}
tx := &txs.Tx{Unsigned: &txs.BaseTx{}} // innocuous legacy struct
tx.SetBytes(wireBytes, wireBytes)
gate := NewZapNativeAdmissionGate(blockEverythingVerifier{})
err := gate.VerifyTx(tx)
if !errors.Is(err, ErrZapNativeNotYetExecutable) {
t.Fatalf(
"gate.VerifyTx(zap-wire CreateSovereignL1) = %v, want ErrZapNativeNotYetExecutable",
err,
)
}
}
// TestZapNativeAdmissionGate_RejectsZapWireRegisterL1Validator exercises
// the wire-kind=7 (RegisterL1Validator) gate path. Even though the
// legacy struct executor is live, the zap_native wire path is still
// gated until the wire-aware admission flow lands.
func TestZapNativeAdmissionGate_RejectsZapWireRegisterL1Validator(t *testing.T) {
zapTx := zap_native.NewRegisterL1ValidatorTx(
ids.ID{0xAA},
[zap_native.BLSPubKeySize]byte{},
[zap_native.BLSPoPSize]byte{},
1_900_000_000,
ids.ID{0xBB},
)
wireBytes := zapTx.Bytes()
if !zap_native.IsZAPBytes(wireBytes) {
t.Fatalf("baseline: expected wire bytes to be ZAP-magic, got prefix=%q", string(wireBytes[:4]))
}
tx := &txs.Tx{Unsigned: &txs.BaseTx{}}
tx.SetBytes(wireBytes, wireBytes)
gate := NewZapNativeAdmissionGate(blockEverythingVerifier{})
err := gate.VerifyTx(tx)
if !errors.Is(err, ErrZapNativeNotYetExecutable) {
t.Fatalf(
"gate.VerifyTx(zap-wire RegisterL1Validator) = %v, want ErrZapNativeNotYetExecutable",
err,
)
}
}
// TestZapNativeAdmissionGate_RejectsZapWireConvertNetworkToL1 exercises
// the wire-kind=22 (ConvertNetworkToL1) gate path.
func TestZapNativeAdmissionGate_RejectsZapWireConvertNetworkToL1(t *testing.T) {
zapTx := zap_native.NewConvertNetworkToL1Tx(zap_native.ConvertNetworkToL1TxInput{
NetworkID: 1,
BlockchainID: ids.ID{0x11},
Chain: ids.ID{0x22},
ManagerChainID: ids.ID{0x33},
Address: []byte("0x" + "00"),
Validators: []zap_native.ValidatorsListEntry{newWireFriendlyValidator(t)},
})
wireBytes := zapTx.Bytes()
if !zap_native.IsZAPBytes(wireBytes) {
t.Fatalf("baseline: expected wire bytes to be ZAP-magic, got prefix=%q", string(wireBytes[:4]))
}
tx := &txs.Tx{Unsigned: &txs.BaseTx{}}
tx.SetBytes(wireBytes, wireBytes)
gate := NewZapNativeAdmissionGate(blockEverythingVerifier{})
err := gate.VerifyTx(tx)
if !errors.Is(err, ErrZapNativeNotYetExecutable) {
t.Fatalf(
"gate.VerifyTx(zap-wire ConvertNetworkToL1) = %v, want ErrZapNativeNotYetExecutable",
err,
)
}
}
// TestZapNativeAdmissionGate_PassThroughNonZapBytes pins the gate's
// non-interference contract: a tx whose Bytes() are NOT ZAP-magic
// (e.g. legacy linearcodec-encoded) and whose Unsigned is a working
// executor type MUST be delegated to the inner verifier unmodified.
func TestZapNativeAdmissionGate_PassThroughNonZapBytes(t *testing.T) {
tx := &txs.Tx{Unsigned: &txs.BaseTx{}}
// Non-ZAP wire bytes — anything that doesn't start with "ZAP\x00".
tx.SetBytes([]byte{0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF}, []byte{0x00, 0x00, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF})
gate := NewZapNativeAdmissionGate(passThroughVerifier{})
err := gate.VerifyTx(tx)
if !errors.Is(err, errSentinel) {
t.Fatalf("gate.VerifyTx(non-ZAP) = %v, want errSentinel (gate must pass to inner)", err)
}
}
// TestZapNativeAdmissionGate_NilSafety confirms the gate handles
// edge-case inputs (nil tx, nil Unsigned, empty Bytes) without
// panicking. Defense-in-depth: the gate is on the hot path, so
// it MUST be panic-free on every input.
func TestZapNativeAdmissionGate_NilSafety(t *testing.T) {
gate := NewZapNativeAdmissionGate(passThroughVerifier{})
t.Run("nil tx", func(t *testing.T) {
// Inner verifier (passThroughVerifier) returns errSentinel on
// any input including nil; the gate must NOT panic on the way.
err := gate.VerifyTx(nil)
if !errors.Is(err, errSentinel) {
t.Fatalf("gate.VerifyTx(nil) = %v, want errSentinel", err)
}
})
t.Run("nil Unsigned", func(t *testing.T) {
err := gate.VerifyTx(&txs.Tx{Unsigned: nil})
if !errors.Is(err, errSentinel) {
t.Fatalf("gate.VerifyTx(nilUnsigned) = %v, want errSentinel", err)
}
})
t.Run("empty bytes", func(t *testing.T) {
tx := &txs.Tx{Unsigned: &txs.BaseTx{}}
tx.SetBytes(nil, nil)
err := gate.VerifyTx(tx)
if !errors.Is(err, errSentinel) {
t.Fatalf("gate.VerifyTx(emptyBytes) = %v, want errSentinel", err)
}
})
}
// newWireFriendlyValidator returns a ValidatorsListEntry whose
// constructor-time BLS fields are present (even if zeroed). The R7V5
// admission gate does NOT verify BLS PoP — that's R7V7 / Verify()'s
// job — so this minimal entry is sufficient for the gate test.
func newWireFriendlyValidator(t *testing.T) zap_native.ValidatorsListEntry {
t.Helper()
return zap_native.ValidatorsListEntry{
NodeID: ids.NodeID{0x77},
Weight: 1_000_000,
BLSPubKey: [zap_native.BLSPubKeySize]byte{},
BLSPoP: [zap_native.BLSPoPSize]byte{},
RegistrationExpiry: 1_900_000_000,
}
}