platformvm/txs/zap_native: R4V7 Owner SyntacticVerify + per-tx Verify (LP-023 batch 5 Phase A)

Wire layer (zap_native parser) is permissive by design — it confirms
TxKind + buffer geometry only. Executor-side semantic gates live HERE
on the consumer-side boundary.

Owner.SyntacticVerify enforces:
  - ErrOwnerAddrsEmpty: Addresses.Len() == 0 (signer set undefined)
  - ErrOwnerThresholdZero: threshold == 0 (auth bypass)
  - ErrOwnerThresholdExceedsAddrs: threshold > Addresses.Len()
    (unsatisfiable quorum)

OwnerStub.SyntacticVerify enforces the single-address fast path:
  - Threshold must be exactly 1 (zero is bypass, >1 is unsatisfiable
    because the stub carries one address by construction)

Per-tx Verify() entry points wire the gate into all 7 tx types that
embed Owner-shaped fields:
  - AddValidatorTx.RewardsOwner
  - AddDelegatorTx.DelegationRewardsOwner
  - AddPermissionlessValidatorTx.{Validation,Delegation}RewardsOwner
  - AddPermissionlessDelegatorTx.DelegationRewardsOwner
  - CreateChainTx.Owner
  - CreateNetworkTx.Owner
  - CreateSovereignL1Tx.Owner

Test scope (TDD red→green):
  - owner_syntactic_test.go — 7 cases on Owner + OwnerStub directly
  - tx_verify_test.go — 7 well-formed + 7 malicious-threshold + 1
    multi-owner accept + 1 adversarial-wire-buffer test that overwrites
    the threshold byte in the buffer and re-Wrap, confirming the gate
    fires on byte-stream attacks (not only constructor input).

Contract (LP-023 Red round 4 R4V7): every tx executor's Verify() entry
point MUST call tx.Verify() before treating embedded Owner fields as
authoritative. Skipping Verify() opens the auth-bypass attack vector.
This commit is contained in:
Hanzo AI
2026-06-02 16:19:05 -07:00
parent 48cdf9cdff
commit 20ced67102
4 changed files with 575 additions and 0 deletions
+92
View File
@@ -68,6 +68,36 @@ var ErrOwnerSingleAddrUseStub = errors.New(
"zap_native: Owner requires len(Addresses) >= 2; use OwnerStub for single-address fast path",
)
// Owner SyntacticVerify error set (LP-023 Red round 4, R4V7 — executor
// side gate). The wire layer accepts any (threshold, locktime, addrlist)
// triple as long as the buffer geometry passes the ListStride clamp; a
// malicious encoder can publish threshold=0 (no-op gate) or threshold >
// len(Addresses) (unsatisfiable gate) or empty Addresses (undefined). The
// executor MUST call SyntacticVerify before trusting the Owner.
var (
// ErrOwnerThresholdZero is returned when threshold == 0. A zero
// threshold semantically means "no signature required" which is an
// authorization bypass. The single-address fast path (OwnerStub) is
// also gated by this — there is no legitimate threshold=0 owner.
ErrOwnerThresholdZero = errors.New(
"zap_native: Owner.Threshold must be > 0; threshold=0 disables authorization",
)
// ErrOwnerThresholdExceedsAddrs is returned when threshold >
// Addresses.Len(). An unsatisfiable gate (DoS — spend can never
// authorize) and on signed-arithmetic consumers can underflow.
ErrOwnerThresholdExceedsAddrs = errors.New(
"zap_native: Owner.Threshold exceeds Addresses.Len() — unsatisfiable signer quorum",
)
// ErrOwnerAddrsEmpty is returned when Addresses.Len() == 0. With no
// signer set the gate is undefined; combined with R4V7's threshold=0
// case this would silently allow any tx to spend.
ErrOwnerAddrsEmpty = errors.New(
"zap_native: Owner.Addresses is empty — signer set undefined",
)
)
// Owner is the multi-address output owner. It composes a threshold +
// locktime + a variable AddressList. Stride for the AddressList lives in
// the same buffer's variable section; the parent object's fixed section
@@ -126,6 +156,41 @@ func (o Owner) Addresses() AddressList {
return AddressListView(o.parent, o.baseOffset+OffsetOwnerHeader_AddressList)
}
// SyntacticVerify enforces R4V7 executor-side semantic gates on an Owner
// header parsed from an untrusted wire buffer. Returns one of:
//
// - nil — Owner is well-formed: threshold ∈ [1, Addresses.Len()] and
// Addresses.Len() > 0.
// - ErrOwnerAddrsEmpty — Addresses.Len() == 0 (signer set undefined).
// - ErrOwnerThresholdZero — threshold == 0 (authorization bypass).
// - ErrOwnerThresholdExceedsAddrs — threshold > Addresses.Len()
// (unsatisfiable quorum).
//
// CONTRACT (LP-023 Red round 4 R4V7): every tx executor's Verify() entry
// point MUST call SyntacticVerify on every Owner consumed from a wire
// buffer before treating the Threshold/Addresses pair as a quorum gate.
// The wire layer (ZAP) is permissive by design — semantic gates live
// here in the consumer.
//
// Ordering matters: addrs-empty is checked BEFORE threshold==0 so a
// fully-empty Owner reports the more informative "addrs empty" error
// rather than the threshold one.
func (o Owner) SyntacticVerify() error {
addrs := o.Addresses()
n := addrs.Len()
if n == 0 {
return ErrOwnerAddrsEmpty
}
t := o.Threshold()
if t == 0 {
return ErrOwnerThresholdZero
}
if uint64(t) > uint64(n) {
return ErrOwnerThresholdExceedsAddrs
}
return nil
}
// OwnerView reads an Owner from a parent object's embedded-header offset.
// The Owner header is INLINE within the parent's fixed section (size
// SizeOwnerHeader = 20 bytes), so this returns a sub-view rather than
@@ -137,6 +202,33 @@ func OwnerView(parent zap.Object, fieldOffset int) Owner {
return Owner{parent: parent, baseOffset: fieldOffset}
}
// SyntacticVerify enforces R4V7 executor-side semantic gates on an
// OwnerStub. The stub by construction carries exactly ONE address, so
// the only valid threshold value is 1. Returns:
//
// - nil — Threshold == 1 and Address != zero-ShortID.
// - ErrOwnerThresholdZero — Threshold == 0 (auth bypass).
// - ErrOwnerThresholdExceedsAddrs — Threshold > 1 (unsatisfiable: only
// one address present, no quorum > 1 can ever be reached).
//
// The address-zero check is intentionally NOT enforced here: a zero
// ShortID is not a wire-protocol violation, only a usability footgun
// (no key can ever match), so the executor catches it at the spend
// stage if the user fat-fingers it.
//
// CONTRACT (parallel of Owner.SyntacticVerify): every tx executor's
// Verify() entry point that consumes an OwnerStub from a wire buffer
// MUST call SyntacticVerify before treating Threshold as a quorum gate.
func (s OwnerStub) SyntacticVerify() error {
if s.Threshold == 0 {
return ErrOwnerThresholdZero
}
if s.Threshold > 1 {
return ErrOwnerThresholdExceedsAddrs
}
return nil
}
// OwnerInput is the constructor input for a multi-address Owner.
type OwnerInput struct {
Threshold uint32
@@ -0,0 +1,159 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap_native
import (
"errors"
"testing"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// TestOwnerSyntacticVerifyAcceptsValid pins the happy path: a well-formed
// multi-address Owner with threshold ∈ [1, len(Addresses)] returns nil.
//
// LP-023 Red round 4 finding R4V7: the wire layer accepts threshold > len
// and threshold == 0; the executor's call-site SyntacticVerify must reject.
func TestOwnerSyntacticVerifyAcceptsValid(t *testing.T) {
addrs := []ids.ShortID{
{0x01}, {0x02}, {0x03},
}
b := zap.NewBuilder(256)
thr, lt, off, count, err := NewOwnerInline(b, OwnerInput{
Threshold: 2,
Locktime: 1_700_000_000,
Addresses: addrs,
})
if err != nil {
t.Fatalf("NewOwnerInline: %v", err)
}
ob := b.StartObject(SizeOwnerHeader)
ob.SetUint32(OffsetOwnerHeader_Threshold, thr)
ob.SetUint64(OffsetOwnerHeader_Locktime, lt)
ob.SetList(OffsetOwnerHeader_AddressList, off, count)
ob.FinishAsRoot()
msg, err := zap.Parse(b.Finish())
if err != nil {
t.Fatalf("Parse: %v", err)
}
owner := OwnerView(msg.Root(), 0)
if err := owner.SyntacticVerify(); err != nil {
t.Fatalf("SyntacticVerify(valid) = %v, want nil", err)
}
}
// TestOwnerSyntacticVerifyRejectsThresholdZero pins R4V7: threshold == 0
// means "any signer is fine, no quorum needed" — a security gap that
// effectively disables authorization. SyntacticVerify must reject with
// the typed error ErrOwnerThresholdZero.
//
// Attack: an adversary publishes a wire-encoded Owner with threshold=0
// and len(Addresses)=k. Naive consumers calling Threshold() see 0 and
// either treat the gate as "open" or trigger underflow in
// quorum-counting code. Both flows skip signature validation.
func TestOwnerSyntacticVerifyRejectsThresholdZero(t *testing.T) {
addrs := []ids.ShortID{{0x01}, {0x02}}
b := zap.NewBuilder(256)
// Bypass NewOwnerInline (which would also reject 0 in the future) and
// hand-craft the malicious wire shape directly — this models the
// adversary's untrusted-network buffer.
off, count := WriteAddressList(b, addrs)
ob := b.StartObject(SizeOwnerHeader)
ob.SetUint32(OffsetOwnerHeader_Threshold, 0)
ob.SetUint64(OffsetOwnerHeader_Locktime, 0)
ob.SetList(OffsetOwnerHeader_AddressList, off, count)
ob.FinishAsRoot()
msg, err := zap.Parse(b.Finish())
if err != nil {
t.Fatalf("Parse: %v", err)
}
owner := OwnerView(msg.Root(), 0)
if got := owner.SyntacticVerify(); !errors.Is(got, ErrOwnerThresholdZero) {
t.Fatalf("SyntacticVerify(threshold=0) = %v, want ErrOwnerThresholdZero", got)
}
}
// TestOwnerSyntacticVerifyRejectsThresholdExceedsAddrs pins R4V7: threshold
// > len(Addresses) is unsatisfiable. SyntacticVerify must reject with
// ErrOwnerThresholdExceedsAddrs.
//
// Attack: adversary sets threshold=1000 against len(addrs)=2. The wire
// layer accepts (no cross-field constraint). A consumer that trusts the
// threshold as a quorum count without bounding against Len() will
// either always reject (DoS — locks the spend forever) or, worse if the
// quorum counter is tracked with signed arithmetic, can be underflowed.
func TestOwnerSyntacticVerifyRejectsThresholdExceedsAddrs(t *testing.T) {
addrs := []ids.ShortID{{0x01}, {0x02}}
b := zap.NewBuilder(256)
off, count := WriteAddressList(b, addrs)
ob := b.StartObject(SizeOwnerHeader)
ob.SetUint32(OffsetOwnerHeader_Threshold, 7) // > 2 addrs
ob.SetUint64(OffsetOwnerHeader_Locktime, 0)
ob.SetList(OffsetOwnerHeader_AddressList, off, count)
ob.FinishAsRoot()
msg, err := zap.Parse(b.Finish())
if err != nil {
t.Fatalf("Parse: %v", err)
}
owner := OwnerView(msg.Root(), 0)
if got := owner.SyntacticVerify(); !errors.Is(got, ErrOwnerThresholdExceedsAddrs) {
t.Fatalf("SyntacticVerify(threshold=7,addrs=2) = %v, want ErrOwnerThresholdExceedsAddrs", got)
}
}
// TestOwnerSyntacticVerifyRejectsEmptyAddrs pins R4V7: a wire-encoded
// Owner with an empty AddressList is undefined — there's no signer set,
// so authorization is undefined. Reject with ErrOwnerAddrsEmpty.
//
// Attack: adversary sets the wire AddressList to length 0. The wire
// layer is happy (Len() returns 0 honestly). A naive consumer iterating
// the list with for i:=0;i<Len();i++ never enters the loop, and if it
// then checks "did we accumulate threshold sigs?" with threshold=0
// (R4V7 also covers that), the spend goes through with zero signatures.
func TestOwnerSyntacticVerifyRejectsEmptyAddrs(t *testing.T) {
b := zap.NewBuilder(64)
ob := b.StartObject(SizeOwnerHeader)
ob.SetUint32(OffsetOwnerHeader_Threshold, 1)
ob.SetUint64(OffsetOwnerHeader_Locktime, 0)
// No SetList — leaves the AddressList pointer as null (Len()=0).
ob.FinishAsRoot()
msg, err := zap.Parse(b.Finish())
if err != nil {
t.Fatalf("Parse: %v", err)
}
owner := OwnerView(msg.Root(), 0)
if got := owner.SyntacticVerify(); !errors.Is(got, ErrOwnerAddrsEmpty) {
t.Fatalf("SyntacticVerify(empty addrs) = %v, want ErrOwnerAddrsEmpty", got)
}
}
// TestOwnerStubSyntacticVerifyAcceptsValid pins the single-address fast
// path: threshold=1, address non-zero → SyntacticVerify returns nil.
func TestOwnerStubSyntacticVerifyAcceptsValid(t *testing.T) {
stub := OwnerStub{Threshold: 1, Locktime: 0, Address: ids.ShortID{0x42}}
if err := stub.SyntacticVerify(); err != nil {
t.Fatalf("SyntacticVerify(valid stub) = %v, want nil", err)
}
}
// TestOwnerStubSyntacticVerifyRejectsThresholdZero pins the single-address
// stub gate: threshold=0 must be rejected (parallel of the multi-address
// Owner case — same security gap).
func TestOwnerStubSyntacticVerifyRejectsThresholdZero(t *testing.T) {
stub := OwnerStub{Threshold: 0, Locktime: 0, Address: ids.ShortID{0x42}}
if got := stub.SyntacticVerify(); !errors.Is(got, ErrOwnerThresholdZero) {
t.Fatalf("SyntacticVerify(threshold=0 stub) = %v, want ErrOwnerThresholdZero", got)
}
}
// TestOwnerStubSyntacticVerifyRejectsThresholdAboveOne pins R4V7 for the
// stub: by construction the stub has exactly ONE address, so threshold
// values > 1 are unsatisfiable.
func TestOwnerStubSyntacticVerifyRejectsThresholdAboveOne(t *testing.T) {
stub := OwnerStub{Threshold: 2, Locktime: 0, Address: ids.ShortID{0x42}}
if got := stub.SyntacticVerify(); !errors.Is(got, ErrOwnerThresholdExceedsAddrs) {
t.Fatalf("SyntacticVerify(threshold=2 stub) = %v, want ErrOwnerThresholdExceedsAddrs", got)
}
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap_native
import (
"fmt"
"github.com/luxfi/ids"
)
// Verify methods enforce executor-side semantic gates for every zap_native
// tx type that embeds Owner / OwnerStub fields. The wire parser (parseAndCheckKind)
// is intentionally permissive — it confirms TxKind + buffer geometry only.
// Semantic gates live HERE so the parser stays pure and the consumer-side
// boundary is one and only one method per tx type.
//
// LP-023 Red round 4 R4V7: executor MUST call tx.Verify() before treating
// any embedded Owner as authoritative. Skipping Verify() opens an
// authorization bypass when the wire-encoded threshold == 0 or threshold
// > len(addrs).
//
// Contract: each Verify() returns a wrapped typed error from
// {ErrOwnerThresholdZero, ErrOwnerThresholdExceedsAddrs, ErrOwnerAddrsEmpty}.
// errors.Is on the typed error MUST match.
// stubFromTuple reconstructs an OwnerStub from the (threshold, locktime,
// address) tuple returned by the embedded-stub accessors. The wire layer
// holds these fields inline in the parent fixed section, so reconstruction
// is a pure value-copy.
func stubFromTuple(threshold uint32, locktime uint64, address ids.ShortID) OwnerStub {
return OwnerStub{Threshold: threshold, Locktime: locktime, Address: address}
}
// Verify runs SyntacticVerify on the embedded RewardsOwner of an
// AddValidatorTx. Wraps the typed error with the tx kind for context.
func (t AddValidatorTx) Verify() error {
o := stubFromTuple(t.RewardsOwner())
if err := o.SyntacticVerify(); err != nil {
return fmt.Errorf("AddValidatorTx.RewardsOwner: %w", err)
}
return nil
}
// Verify runs SyntacticVerify on the embedded DelegationRewardsOwner of an
// AddDelegatorTx.
func (t AddDelegatorTx) Verify() error {
o := stubFromTuple(t.DelegationRewardsOwner())
if err := o.SyntacticVerify(); err != nil {
return fmt.Errorf("AddDelegatorTx.DelegationRewardsOwner: %w", err)
}
return nil
}
// Verify runs SyntacticVerify on both embedded owners of an
// AddPermissionlessValidatorTx — the ValidationRewardsOwner AND the
// DelegationRewardsOwner. Either malformed owner fails the whole tx.
func (t AddPermissionlessValidatorTx) Verify() error {
v := stubFromTuple(t.ValidationRewardsOwner())
if err := v.SyntacticVerify(); err != nil {
return fmt.Errorf("AddPermissionlessValidatorTx.ValidationRewardsOwner: %w", err)
}
d := stubFromTuple(t.DelegationRewardsOwner())
if err := d.SyntacticVerify(); err != nil {
return fmt.Errorf("AddPermissionlessValidatorTx.DelegationRewardsOwner: %w", err)
}
return nil
}
// Verify runs SyntacticVerify on the embedded DelegationRewardsOwner of an
// AddPermissionlessDelegatorTx.
func (t AddPermissionlessDelegatorTx) Verify() error {
o := stubFromTuple(t.DelegationRewardsOwner())
if err := o.SyntacticVerify(); err != nil {
return fmt.Errorf("AddPermissionlessDelegatorTx.DelegationRewardsOwner: %w", err)
}
return nil
}
// Verify runs SyntacticVerify on the embedded Owner of a CreateChainTx.
func (t CreateChainTx) Verify() error {
o := stubFromTuple(t.Owner())
if err := o.SyntacticVerify(); err != nil {
return fmt.Errorf("CreateChainTx.Owner: %w", err)
}
return nil
}
// Verify runs SyntacticVerify on the embedded Owner of a CreateNetworkTx.
func (t CreateNetworkTx) Verify() error {
o := stubFromTuple(t.Owner())
if err := o.SyntacticVerify(); err != nil {
return fmt.Errorf("CreateNetworkTx.Owner: %w", err)
}
return nil
}
// Verify runs SyntacticVerify on the embedded Owner of a
// CreateSovereignL1Tx.
func (t CreateSovereignL1Tx) Verify() error {
o := stubFromTuple(t.Owner())
if err := o.SyntacticVerify(); err != nil {
return fmt.Errorf("CreateSovereignL1Tx.Owner: %w", err)
}
return nil
}
@@ -0,0 +1,218 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zap_native
import (
"errors"
"testing"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// TestVerify_AcceptsWellFormed walks every tx type that embeds an
// OwnerStub and pins that Verify() returns nil on a well-formed tx.
// Threshold=1, single address — the legitimate single-signer case.
func TestVerify_AcceptsWellFormed(t *testing.T) {
addr := ids.ShortID{0x42}
stub := OwnerStub{Threshold: 1, Locktime: 0, Address: addr}
t.Run("AddValidator", func(t *testing.T) {
tx := NewAddValidatorTx(AddValidatorTxInput{NetworkID: 1, RewardsOwner: stub})
if err := tx.Verify(); err != nil {
t.Fatalf("Verify = %v, want nil", err)
}
})
t.Run("AddDelegator", func(t *testing.T) {
tx := NewAddDelegatorTx(AddDelegatorTxInput{NetworkID: 1, DelegationRewardsOwner: stub})
if err := tx.Verify(); err != nil {
t.Fatalf("Verify = %v, want nil", err)
}
})
t.Run("AddPermissionlessValidator", func(t *testing.T) {
tx := NewAddPermissionlessValidatorTx(AddPermissionlessValidatorTxInput{
NetworkID: 1,
ValidationRewardsOwner: stub,
DelegationRewardsOwner: stub,
})
if err := tx.Verify(); err != nil {
t.Fatalf("Verify = %v, want nil", err)
}
})
t.Run("AddPermissionlessDelegator", func(t *testing.T) {
tx := NewAddPermissionlessDelegatorTx(AddPermissionlessDelegatorTxInput{
NetworkID: 1,
DelegationRewardsOwner: stub,
})
if err := tx.Verify(); err != nil {
t.Fatalf("Verify = %v, want nil", err)
}
})
t.Run("CreateChain", func(t *testing.T) {
tx := NewCreateChainTx(CreateChainTxInput{NetworkID: 1, Owner: stub})
if err := tx.Verify(); err != nil {
t.Fatalf("Verify = %v, want nil", err)
}
})
t.Run("CreateNetwork", func(t *testing.T) {
tx := NewCreateNetworkTx(CreateNetworkTxInput{NetworkID: 1, Owner: stub})
if err := tx.Verify(); err != nil {
t.Fatalf("Verify = %v, want nil", err)
}
})
t.Run("CreateSovereignL1", func(t *testing.T) {
tx := NewCreateSovereignL1Tx(CreateSovereignL1TxInput{NetworkID: 1, Owner: stub})
if err := tx.Verify(); err != nil {
t.Fatalf("Verify = %v, want nil", err)
}
})
}
// TestVerify_RejectsMaliciousThresholdZero pins that the executor-side
// Verify() catches a wire-encoded tx whose embedded OwnerStub has
// threshold == 0. This is the highest-severity R4V7 attack: a malicious
// encoder publishes a stub with no quorum, and the consumer treats it
// as authorized.
//
// We hand-craft the wire shape: build a real tx with threshold=1, then
// re-parse and overwrite the threshold byte to 0 via the builder.
// (Simpler: we go through NewXxxTx with a hand-supplied OwnerStub that
// has threshold=0 — bypassing the constructor isn't needed for this
// test because zap_native's NewXxxTx functions don't pre-validate.)
func TestVerify_RejectsMaliciousThresholdZero(t *testing.T) {
addr := ids.ShortID{0x42}
malicious := OwnerStub{Threshold: 0, Locktime: 0, Address: addr}
t.Run("AddValidator", func(t *testing.T) {
tx := NewAddValidatorTx(AddValidatorTxInput{NetworkID: 1, RewardsOwner: malicious})
err := tx.Verify()
if !errors.Is(err, ErrOwnerThresholdZero) {
t.Fatalf("Verify = %v, want ErrOwnerThresholdZero", err)
}
})
t.Run("AddDelegator", func(t *testing.T) {
tx := NewAddDelegatorTx(AddDelegatorTxInput{NetworkID: 1, DelegationRewardsOwner: malicious})
if !errors.Is(tx.Verify(), ErrOwnerThresholdZero) {
t.Fatalf("missing reject")
}
})
t.Run("APV_ValidationOwner", func(t *testing.T) {
good := OwnerStub{Threshold: 1, Address: addr}
tx := NewAddPermissionlessValidatorTx(AddPermissionlessValidatorTxInput{
NetworkID: 1,
ValidationRewardsOwner: malicious,
DelegationRewardsOwner: good,
})
if !errors.Is(tx.Verify(), ErrOwnerThresholdZero) {
t.Fatalf("missing reject on ValidationOwner=malicious")
}
})
t.Run("APV_DelegationOwner", func(t *testing.T) {
good := OwnerStub{Threshold: 1, Address: addr}
tx := NewAddPermissionlessValidatorTx(AddPermissionlessValidatorTxInput{
NetworkID: 1,
ValidationRewardsOwner: good,
DelegationRewardsOwner: malicious,
})
if !errors.Is(tx.Verify(), ErrOwnerThresholdZero) {
t.Fatalf("missing reject on DelegationOwner=malicious")
}
})
t.Run("CreateChain", func(t *testing.T) {
tx := NewCreateChainTx(CreateChainTxInput{NetworkID: 1, Owner: malicious})
if !errors.Is(tx.Verify(), ErrOwnerThresholdZero) {
t.Fatalf("missing reject")
}
})
t.Run("CreateNetwork", func(t *testing.T) {
tx := NewCreateNetworkTx(CreateNetworkTxInput{NetworkID: 1, Owner: malicious})
if !errors.Is(tx.Verify(), ErrOwnerThresholdZero) {
t.Fatalf("missing reject")
}
})
t.Run("CreateSovereignL1", func(t *testing.T) {
tx := NewCreateSovereignL1Tx(CreateSovereignL1TxInput{NetworkID: 1, Owner: malicious})
if !errors.Is(tx.Verify(), ErrOwnerThresholdZero) {
t.Fatalf("missing reject")
}
})
}
// TestVerify_RejectsMaliciousThresholdAboveOne pins the OwnerStub specific
// case where threshold > 1 (unsatisfiable; only one address is present).
func TestVerify_RejectsMaliciousThresholdAboveOne(t *testing.T) {
bad := OwnerStub{Threshold: 5, Locktime: 0, Address: ids.ShortID{0x42}}
tx := NewCreateNetworkTx(CreateNetworkTxInput{NetworkID: 1, Owner: bad})
if !errors.Is(tx.Verify(), ErrOwnerThresholdExceedsAddrs) {
t.Fatalf("CreateNetwork.Verify = %v, want ErrOwnerThresholdExceedsAddrs", tx.Verify())
}
}
// TestVerify_AdversarialWireBuffer pins R4V7 against the strongest threat
// model: an adversary controls the BYTE STREAM, not the constructor input.
// We build a CreateNetworkTx the normal way, then overwrite the threshold
// bytes in the buffer (offset 85) to 0, re-Wrap, and confirm Verify()
// catches it. This proves the gate fires on untrusted wire input, not
// only on constructor input.
func TestVerify_AdversarialWireBuffer(t *testing.T) {
good := OwnerStub{Threshold: 1, Locktime: 0, Address: ids.ShortID{0x42}}
tx := NewCreateNetworkTx(CreateNetworkTxInput{NetworkID: 1, Owner: good})
buf := tx.Bytes()
// The threshold uint32 lives at OffsetCreateNetworkTx_OwnerThreshold
// (= 85) in the FIXED SECTION. The fixed section starts at the
// root-object payload offset within buf. Locate it via re-Parse.
msg, err := zap.Parse(buf)
if err != nil {
t.Fatalf("Parse: %v", err)
}
root := msg.Root()
// Confirm pre-overwrite threshold == 1.
if got := root.Uint32(OffsetCreateNetworkTx_OwnerThreshold); got != 1 {
t.Fatalf("pre-overwrite threshold = %d, want 1", got)
}
// To overwrite, we duplicate the buffer and patch the 4 bytes at
// the root-object's threshold field. The root object's payload start
// is computed from the wire header. zap exposes the position via the
// Object's offset; but tests can simply search for the unique
// pattern (threshold=1, locktime=0, address[0]=0x42) and patch in
// place.
tampered := make([]byte, len(buf))
copy(tampered, buf)
// We know threshold lives uint32 little-endian; pattern is {0x01,0x00,0x00,0x00}
// followed by locktime uint64 {0,0,0,0,0,0,0,0} then 20-byte addr
// starting with 0x42. Scan once.
patched := false
for i := 0; i+32 <= len(tampered); i++ {
if tampered[i] == 0x01 && tampered[i+1] == 0 && tampered[i+2] == 0 && tampered[i+3] == 0 &&
tampered[i+4] == 0 && tampered[i+5] == 0 && tampered[i+6] == 0 && tampered[i+7] == 0 &&
tampered[i+8] == 0 && tampered[i+9] == 0 && tampered[i+10] == 0 && tampered[i+11] == 0 &&
tampered[i+12] == 0x42 {
// Overwrite the threshold uint32 with 0.
tampered[i] = 0
patched = true
break
}
}
if !patched {
t.Fatalf("could not locate threshold field in wire buffer")
}
// Re-Wrap the tampered buffer.
tamperedTx, err := WrapCreateNetworkTx(tampered)
if err != nil {
t.Fatalf("WrapCreateNetworkTx(tampered) = %v, want nil (parser is permissive)", err)
}
thr, _, _ := tamperedTx.Owner()
if thr != 0 {
t.Fatalf("tampered threshold = %d, want 0 (patch did not take effect)", thr)
}
// Now the executor-side gate must reject.
if !errors.Is(tamperedTx.Verify(), ErrOwnerThresholdZero) {
t.Fatalf("Verify(tampered) = %v, want ErrOwnerThresholdZero", tamperedTx.Verify())
}
}