Files
75c3c56144 Thinking Chains: precompile (#7)
* dex/0x9999: per-intent swap escrow record — per-taker cap (MEDIUM) + deadline reclaim (HIGH)

The swap rail lacked the LP rail's per-taker bound and any reclaim path: a swap's
locked tokenIn was stranded forever if D never settled (Deadline hardcoded 0, no
reclaim selector, SubmitCancel dead), and ImportSettlement bounded a credit only by
the shared seamReserve — no per-taker cap, so an over-export for one taker could
draw on others' pooled tokenIn.

ONE new per-intent escrow record (native_state.go swapIntentRecord: owner, assetIn,
remaining principal, deadline, status) drives BOTH fixes, created at SubmitSwapIntent
— the swap-rail analog of the LP RestingOrder:

MEDIUM (per-taker cap): ImportSettlement binds each settlement to the taker's OWN
named Open intent (owner==recorded owner, not past deadline). The SAME-ASSET refund
leg (recAsset==intent.AssetIn) is capped by + decrements the remaining locked
principal (ErrSettleExceedsIntent) — the exact same-asset analog of
ErrLPCollectExceedsPosition (the LP collects the asset it committed). The proceeds
leg (opposite asset) is NOT input-unit-capped (output units differ from input at any
price != 1 — an input-unit cap would wrongly refuse honest fills); its no-mint guard
is seamReserve backing + one-time consumption + the intent binding.

HIGH (reclaim liveness): a real non-zero Deadline is plumbed from an explicit DI01
intent-body word (V4 SwapParams tuple UNCHANGED) into the record. New reclaimIntent
selector refunds the remaining locked principal from seamReserve to the locker once
the deadline passes (one-time, deadline-gated; reclaim+late-settle are mutually
exclusive via the shared remaining counter + terminal Reclaimed status; a
compensating C->D Remove prevents D double-funding). So locked swap input can always
exit, like deposit/withdraw.

INFO: corrected the swap-rail-safety docstrings (settle9999.go,
native_dchain_client.go, native_events.go) to state per-rail what C enforces vs
relies on D for; fixed the false "reclaims via the cancel path" comment. Deleted the
dead SubmitCancel/SubmitCollect client methods (the cancel/collect keeper events are
emitted at their lifecycle sites; the swap reclaim is reclaimIntent).

Regression: native_swap_intent_redteam_test.go (8 tests) — RefundBoundToOwnIntent
Principal (MEDIUM cap), ProceedsNotCappedByInputPrincipal (dimension correctness),
ReclaimRefundsStrandedPrincipal + ReclaimGuards + ReclaimAndSettleAreMutually
Exclusive (HIGH), SettleRefusedPastDeadline, DeadlinePlumbedFromIntentBody,
SettleMustNameAnOpenIntent. Full dex suite passes.

Gated on red re-verify; NOT promoted to testnet/mainnet.

* aivmbridge: C->A inference bridge precompile (0x0300..04). Pattern A submit-intent / Pattern B verify committed receipt (keccak-merkle). LP-5301.

* aivmbridge: build-tag crossmodule parity test (CI green)

The cross-module byte-equality test is the only file importing chains/aivm
(resolvable only via go.work). Tag it `crossmodule` so default go test ./...
(CI, no workspace) skips it; run with `go test -tags crossmodule` + workspace.
Production aivmbridge stays decoupled (wire-format only).

---------

Co-authored-by: zeekay <z@zeekay.io>
2026-06-21 20:53:33 -07:00

157 lines
5.0 KiB
Go

// Copyright (C) 2026, Lux Partners Limited. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// decode_test.go — wire decode hardening for receipts, proofs, and the verify
// calldata frame. A malformed object must be REJECTED (never mis-decoded into a value
// that could pass verification). Also covers the merkle edge cases (single-leaf tree,
// over-wide index, over-deep path).
package aivmbridge
import (
"testing"
"github.com/luxfi/geth/common"
)
func TestDecodeReceipt_RejectsBadLength(t *testing.T) {
good := EncodeReceipt(fixtureReceipt())
if _, err := DecodeReceipt(good[:len(good)-1]); err != ErrReceiptDecode {
t.Fatalf("short: err = %v, want ErrReceiptDecode", err)
}
if _, err := DecodeReceipt(append(good, 0x00)); err != ErrReceiptDecode {
t.Fatalf("long: err = %v, want ErrReceiptDecode", err)
}
if _, err := DecodeReceipt(nil); err != ErrReceiptDecode {
t.Fatalf("nil: err = %v, want ErrReceiptDecode", err)
}
}
func TestDecodeReceipt_RejectsBadVersion(t *testing.T) {
r := fixtureReceipt()
r.Version = 999
enc := EncodeReceipt(r)
if _, err := DecodeReceipt(enc); err != ErrReceiptDecode {
t.Fatalf("err = %v, want ErrReceiptDecode (bad version)", err)
}
}
func TestDecodeProof_RejectsTruncated(t *testing.T) {
if _, err := DecodeProof(make([]byte, 41)); err != ErrProofDecode {
t.Fatalf("err = %v, want ErrProofDecode (below header)", err)
}
}
func TestDecodeProof_RejectsLengthMismatch(t *testing.T) {
// Declare pathLen=2 but supply only 1 node worth of trailing bytes.
b := make([]byte, 42+32)
b[41] = 2 // pathLen = 2
if _, err := DecodeProof(b); err != ErrProofDecode {
t.Fatalf("err = %v, want ErrProofDecode (truncated path)", err)
}
// Trailing junk past a correct path.
b2 := make([]byte, 42+32+1)
b2[41] = 1
if _, err := DecodeProof(b2); err != ErrProofDecode {
t.Fatalf("err = %v, want ErrProofDecode (trailing junk)", err)
}
}
func TestDecodeProof_RejectsOverDeep(t *testing.T) {
// pathLen > MaxProofDepth.
depth := MaxProofDepth + 1
b := make([]byte, 42+depth*32)
b[40] = byte(depth >> 8)
b[41] = byte(depth)
if _, err := DecodeProof(b); err != ErrProofDecode {
t.Fatalf("err = %v, want ErrProofDecode (over-deep path)", err)
}
}
func TestMerkle_SingleLeaf(t *testing.T) {
leaf := h32(0xAB)
// A depth-0 proof: root == leaf, index 0, empty path.
p := AInferenceProof{ReceiptRoot: leaf, Path: nil, Index: 0}
if !VerifyMerkle(leaf, p) {
t.Fatal("single-leaf proof should verify")
}
// Non-zero index on a depth-0 tree must fail.
p.Index = 1
if VerifyMerkle(leaf, p) {
t.Fatal("depth-0 tree with index!=0 must fail")
}
}
func TestMerkle_OverWideIndex(t *testing.T) {
leaves := [][32]byte{h32(1), h32(2), h32(3), h32(4)}
tree := buildMerkleTree(leaves)
p := tree.proof(1) // depth 2, valid index 1
// buildMerkleTree leaf-hashes internally (production tree), so VerifyMerkle — a pure
// path-folder — is given the leaf-hashed leaf, exactly as verifyInferenceReceipt does.
leaf := leafHash(leaves[1])
if !VerifyMerkle(leaf, p) {
t.Fatal("valid proof should verify")
}
// Index with a high bit beyond the tree depth (depth=2 -> valid 0..3); set bit 2.
bad := p
bad.Index = 1 | (1 << 2)
if VerifyMerkle(leaf, bad) {
t.Fatal("over-wide index must fail (high-bit aliasing)")
}
}
func TestVerifyFrame_RejectsBadFraming(t *testing.T) {
f := newVerifyFixture(t)
receipt := EncodeReceipt(f.receipt)
proof := encodeProof(f.proof)
good := encodeVerify(receipt, proof)
cases := []struct {
name string
in []byte
want error
}{
// Truncate the whole frame below the 4-byte selector+header.
{"below header", good[:6], ErrInputTooShort},
// Corrupt the declared receiptLen so total != actual.
{"len mismatch", corruptVerifyLen(good), ErrInputOversized},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gas := GasVerifyInferenceReceiptBase + 100_000
if _, _, err := BridgePrecompile.Run(f.st, f.caller, ContractAddress, tc.in, gas, false); err != tc.want {
t.Fatalf("err = %v, want %v", err, tc.want)
}
})
}
}
// corruptVerifyLen bumps the declared receiptLen by 1 so the framed total no longer
// matches the actual byte length.
func corruptVerifyLen(good []byte) []byte {
cp := append([]byte(nil), good...)
// header is at args offset 0 (after the 4-byte selector): args[0:2] = receiptLen.
// So in the full input, that's bytes [4:6].
rl := uint16(cp[4])<<8 | uint16(cp[5])
rl++
cp[4] = byte(rl >> 8)
cp[5] = byte(rl)
return cp
}
func TestVerifyFrame_UnknownSelector(t *testing.T) {
st := newMockState(common.HexToHash("0x01"))
in := make([]byte, 4)
putU32(in, 0xDEADBEEF)
if _, _, err := BridgePrecompile.Run(st, common.Address{0x1}, ContractAddress, in, 100_000, false); err == nil {
t.Fatal("unknown selector should error")
}
}
func TestRun_EmptyInput(t *testing.T) {
st := newMockState(common.HexToHash("0x01"))
if _, _, err := BridgePrecompile.Run(st, common.Address{0x1}, ContractAddress, nil, 100_000, false); err != ErrInputTooShort {
t.Fatalf("err = %v, want ErrInputTooShort", err)
}
}