mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
* 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>
157 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|