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>
177 lines
6.6 KiB
Go
177 lines
6.6 KiB
Go
// Copyright (C) 2026, Lux Partners Limited. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// wire_test.go — the SHARED WIRE SPEC cross-check. It pins intent_id and receipt_hash
|
|
// to GOLDEN VECTORS computed from a fixed fixture. Blue-B's chains/aivm importer MUST
|
|
// reproduce these EXACT bytes; a divergence here is a cross-chain wire break (an A
|
|
// receipt C cannot verify, or an intent A imports under the wrong id). These vectors
|
|
// ARE the contract between the C precompile and the A importer.
|
|
|
|
package aivmbridge
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"testing"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
)
|
|
|
|
// fixtureIntent is the canonical wire-spec fixture (every field set to a distinct,
|
|
// recognizable pattern so a mis-ordered preimage is obvious).
|
|
func fixtureIntent() InferenceIntent {
|
|
return InferenceIntent{
|
|
CChainID: h32(0xC0),
|
|
AChainID: h32(0xA0),
|
|
CTxHash: h32(0x77),
|
|
CallIndex: 0x01020304,
|
|
Caller: common.HexToAddress("0x1111111111111111111111111111111111111111"),
|
|
ModelSpecHash: h32(0x5E),
|
|
ModelPromptHash: h32(0x9D),
|
|
N: 3,
|
|
Threshold: 2,
|
|
Fee: feeWord(1_000_000),
|
|
}
|
|
}
|
|
|
|
// fixtureReceipt is the canonical receipt wire-spec fixture (bound to fixtureIntent).
|
|
func fixtureReceipt() AInferenceReceipt {
|
|
intentID := DeriveIntentID(fixtureIntent())
|
|
return AInferenceReceipt{
|
|
Version: ReceiptVersion,
|
|
IntentID: intentID,
|
|
TaskID: h32(0x7A),
|
|
CChainID: h32(0xC0),
|
|
AChainID: h32(0xA0),
|
|
Requester: common.HexToAddress("0x1111111111111111111111111111111111111111"),
|
|
ModelSpecHash: h32(0x5E),
|
|
PromptHash: h32(0x9D),
|
|
CanonicalOutputHash: h32(0x0F),
|
|
Status: StatusCompleted,
|
|
N: 3,
|
|
Threshold: 2,
|
|
WinnersRoot: h32(0x44),
|
|
OperatorsRoot: h32(0x55),
|
|
FeePaid: feeWord(1_000_000),
|
|
SettledAtHeight: 0x0102030405060708,
|
|
}
|
|
}
|
|
|
|
// GOLDEN VECTORS — these are the pinned wire-spec outputs. Blue-B's chains/aivm MUST
|
|
// match them. (Computed by this package's own DeriveIntentID / ReceiptHash; locked in
|
|
// so a future refactor that changes the byte layout fails loudly.)
|
|
const (
|
|
goldenIntentID = "13346f5fe5f8feda7fec68968366fb397cf3854096a07a1528ada9a0c910d758"
|
|
goldenReceiptHash = "c6e07d2b28f8cafa0ccf12a540379eb37afe50cf48567e59d96e386a73ca5b5b"
|
|
)
|
|
|
|
// TestWireSpecGoldenIntentID asserts intent_id matches the pinned golden vector AND
|
|
// the documented keccak preimage layout exactly.
|
|
func TestWireSpecGoldenIntentID(t *testing.T) {
|
|
got := DeriveIntentID(fixtureIntent())
|
|
gotHex := hex.EncodeToString(got[:])
|
|
|
|
// Reconstruct the preimage by hand per the spec and assert DeriveIntentID equals
|
|
// keccak of it (proves the function follows the documented layout, not just itself).
|
|
want := manualIntentID(fixtureIntent())
|
|
wantHex := hex.EncodeToString(want[:])
|
|
if gotHex != wantHex {
|
|
t.Fatalf("intent_id != manual preimage keccak:\n got %s\n want %s", gotHex, wantHex)
|
|
}
|
|
t.Logf("GOLDEN intent_id = %s", gotHex)
|
|
|
|
if goldenIntentID != "" && goldenIntentID != "PLACEHOLDER_INTENT_ID" && gotHex != goldenIntentID {
|
|
t.Fatalf("intent_id golden vector drift:\n got %s\n golden %s", gotHex, goldenIntentID)
|
|
}
|
|
}
|
|
|
|
// TestWireSpecGoldenReceiptHash asserts receipt_hash matches the documented domain +
|
|
// canonical encoding, and round-trips encode/decode.
|
|
func TestWireSpecGoldenReceiptHash(t *testing.T) {
|
|
r := fixtureReceipt()
|
|
|
|
// Round-trip the canonical encoding.
|
|
enc := EncodeReceipt(r)
|
|
if len(enc) != receiptEncodedLen {
|
|
t.Fatalf("receipt encoding length = %d, want %d", len(enc), receiptEncodedLen)
|
|
}
|
|
dec, err := DecodeReceipt(enc)
|
|
if err != nil {
|
|
t.Fatalf("DecodeReceipt: %v", err)
|
|
}
|
|
if dec != r {
|
|
t.Fatalf("receipt round-trip mismatch:\n got %+v\n want %+v", dec, r)
|
|
}
|
|
|
|
got := ReceiptHash(r)
|
|
gotHex := hex.EncodeToString(got[:])
|
|
t.Logf("GOLDEN receipt_hash = %s", gotHex)
|
|
|
|
// Manual recompute: keccak(DOMAIN_RECEIPT || canonical_encoding).
|
|
want := manualReceiptHash(r)
|
|
if got != want {
|
|
t.Fatalf("receipt_hash != manual domain||encoding keccak:\n got %s\n want %s",
|
|
gotHex, hex.EncodeToString(want[:]))
|
|
}
|
|
|
|
if goldenReceiptHash != "" && goldenReceiptHash != "PLACEHOLDER_RECEIPT_HASH" && gotHex != goldenReceiptHash {
|
|
t.Fatalf("receipt_hash golden vector drift:\n got %s\n golden %s", gotHex, goldenReceiptHash)
|
|
}
|
|
}
|
|
|
|
// TestDomainSeparators pins the exact domain strings (a typo here is a wire break).
|
|
func TestDomainSeparators(t *testing.T) {
|
|
if DomainIntent != "lux/aivmbridge/intent/v1" {
|
|
t.Fatalf("DomainIntent = %q", DomainIntent)
|
|
}
|
|
if DomainReceipt != "lux/aivmbridge/receipt/v1" {
|
|
t.Fatalf("DomainReceipt = %q", DomainReceipt)
|
|
}
|
|
}
|
|
|
|
// manualIntentID is an INDEPENDENT re-implementation of the intent_id preimage from
|
|
// the spec text, used to prove DeriveIntentID follows the documented layout (a tautology
|
|
// is avoided: this assembles the bytes by hand in spec order).
|
|
func manualIntentID(in InferenceIntent) [32]byte {
|
|
var buf []byte
|
|
buf = append(buf, []byte("lux/aivmbridge/intent/v1")...)
|
|
buf = append(buf, in.CChainID[:]...)
|
|
buf = append(buf, in.AChainID[:]...)
|
|
buf = append(buf, in.CTxHash[:]...)
|
|
buf = append(buf, byte(in.CallIndex>>24), byte(in.CallIndex>>16), byte(in.CallIndex>>8), byte(in.CallIndex))
|
|
buf = append(buf, in.Caller.Bytes()...)
|
|
buf = append(buf, in.ModelSpecHash[:]...)
|
|
buf = append(buf, in.ModelPromptHash[:]...)
|
|
buf = append(buf, byte(in.N>>8), byte(in.N))
|
|
buf = append(buf, byte(in.Threshold>>8), byte(in.Threshold))
|
|
buf = append(buf, in.Fee[:]...)
|
|
return keccak(buf)
|
|
}
|
|
|
|
// manualReceiptHash independently recomputes keccak(DOMAIN_RECEIPT || canonical) by
|
|
// assembling the canonical encoding by hand in spec order.
|
|
func manualReceiptHash(r AInferenceReceipt) [32]byte {
|
|
var e []byte
|
|
e = append(e, byte(r.Version>>8), byte(r.Version))
|
|
e = append(e, r.IntentID[:]...)
|
|
e = append(e, r.TaskID[:]...)
|
|
e = append(e, r.CChainID[:]...)
|
|
e = append(e, r.AChainID[:]...)
|
|
e = append(e, r.Requester.Bytes()...)
|
|
e = append(e, r.ModelSpecHash[:]...)
|
|
e = append(e, r.PromptHash[:]...)
|
|
e = append(e, r.CanonicalOutputHash[:]...)
|
|
e = append(e, byte(r.Status))
|
|
e = append(e, byte(r.N>>8), byte(r.N))
|
|
e = append(e, byte(r.Threshold>>8), byte(r.Threshold))
|
|
e = append(e, r.WinnersRoot[:]...)
|
|
e = append(e, r.OperatorsRoot[:]...)
|
|
e = append(e, r.FeePaid[:]...)
|
|
h := r.SettledAtHeight
|
|
e = append(e, byte(h>>56), byte(h>>48), byte(h>>40), byte(h>>32), byte(h>>24), byte(h>>16), byte(h>>8), byte(h))
|
|
|
|
var buf []byte
|
|
buf = append(buf, []byte("lux/aivmbridge/receipt/v1")...)
|
|
buf = append(buf, e...)
|
|
return keccak(buf)
|
|
}
|