Files
zeekay 3b98c49e30 dexsession: scope fee-on-transfer id divergence to the IntentSubmitted topic + cross-repo id golden (swap-seam r2)
FEE-ON-TRANSFER id divergence (LOW/INFO #2c). On-chain DeriveIntentID binds the OBSERVED-DELTA
locked amount (the value the 0x9999 vault actually received), not the requested amount. For
native LUX + standard ERC-20s, locked == amountIn so this off-chain pre-derivation equals the
on-chain id exactly. For a FEE-ON-TRANSFER token, locked < amountIn so the pre-derived id
DIVERGES — but that is only a missed pre-registration optimization, not a safety or correlation
break: the on-chain IntentSubmitted event carries the REAL id as its INDEXED topic (and the real
locked amount), so a keeper correlates via that topic — the authoritative binding — and builds
the D order from the event, never a guessed id. Scoped the DeriveIntentID doc to state this
precisely (pre-registration valid iff locked == amountIn; otherwise correlate via the indexed
topic).

CROSS-REPO id golden (LOW/INFO #3b). Added TestIntentID_OffChainEqualsOnChain: the off-chain
DeriveIntentID over fixed inputs must hash to a shared golden the precompile side ALSO pins
(its TestIntentID_CrossRepoGolden, native_intentid_parity_test). dexsession cannot import
precompile/dex (EVM+geth+cgo + a forbidden compile edge to C Verify), so the two id
implementations are anchored to ONE golden — either drifting fails its half. Closes the missing
cross-repo equivalence test.

CGO_ENABLED=0 pure-Go (GOWORK=off); full dexsession suite green.
2026-06-21 22:43:29 -07:00

254 lines
9.4 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dexsession
import (
"bytes"
"encoding/hex"
"testing"
)
// wire_test.go round-trips every envelope through build->parse->read and asserts
// EVERY field survives. This catches field-slot overlap (a bytes32 mistakenly
// given an 8-byte slot clobbers the next field) — the exact class of bug that
// must never reach the value-boundary id derivation, since a corrupted intent id
// would name the wrong object.
// intentIDGolden is the cross-repo golden intent id: SHA-256 over the canonical
// DeriveIntentID preimage with the FIXED inputs in TestIntentID_OffChainEqualsOnChain.
// It is reproduced byte-for-byte by precompile/dex's TestIntentID_OffChainEqualsOnChain
// (the on-chain DeriveIntentID). dexsession cannot import precompile/dex (it would drag the
// EVM+geth+cgo graph into the transport module and create a compile edge to the C Verify
// path — both forbidden), so the two implementations are pinned to ONE shared golden: if
// either side's id derivation drifts, its half of this test fails. This is the value-boundary
// contract — the off-chain pointer MUST name the same object the chain mints.
const intentIDGolden = "5b0e8d3828def79d10b8ea811c422d702c58125508a6de78c5ae3a4b691ce016"
// TestIntentID_OffChainEqualsOnChain proves the off-chain (dexsession) DeriveIntentID equals
// the on-chain (precompile) DeriveIntentID for the SAME inputs, via the shared golden. The
// FIXED inputs here are IDENTICAL to the precompile-side twin; both must hash to
// intentIDGolden. (The watch-correlation contract for non-fee-on-transfer inputs, where
// locked == amountIn — see DeriveIntentID's fee-on-transfer caveat.)
func TestIntentID_OffChainEqualsOnChain(t *testing.T) {
var c, d ID
c[0], c[31] = 0xC0, 0x01
d[0], d[31] = 0xD0, 0x02
var acct Account
acct[19] = 0xAA // 0x00..00AA
var assetIn ID
for i := range assetIn {
assetIn[i] = byte(0x30 + i)
}
var market ID
for i := range market {
market[i] = byte(0x70 + i)
}
got := DeriveIntentID(96369, c, d, acct, assetIn, 1_000_000, market, 7)
if hex.EncodeToString(got[:]) != intentIDGolden {
t.Fatalf("off-chain DeriveIntentID diverged from the on-chain golden:\n got %s\nwant %s\n"+
"the off-chain pointer would name a different object than the chain mints — fix the "+
"derivation to match precompile/dex/native_wire.go DeriveIntentID.",
hex.EncodeToString(got[:]), intentIDGolden)
}
}
func TestWire_QuoteRequest_RoundTrip(t *testing.T) {
in := QuoteRequest{MarketID: fillID(0xA1), AmountIn: 123456789, ZeroForOne: true}
m, err := buildQuoteRequest(in)
if err != nil {
t.Fatal(err)
}
got := readQuoteRequest(m)
if got != in {
t.Fatalf("QuoteRequest round-trip:\n got %+v\nwant %+v", got, in)
}
}
func TestWire_QuoteResult_RoundTrip(t *testing.T) {
in := QuoteResult{MarketID: fillID(0xB2), AmountIn: 1000, AmountOut: 999, BestPricePx: 0xDEADBEEF, Liquid: true}
m, err := buildQuoteResult(in)
if err != nil {
t.Fatal(err)
}
got := readQuoteResult(m)
if got != in {
t.Fatalf("QuoteResult round-trip:\n got %+v\nwant %+v", got, in)
}
}
func TestWire_StateRequest_RoundTrip(t *testing.T) {
in := StateRequest{Kind: StateBalance, MarketID: fillID(0x11), Account: fillAcct(0x22), Asset: fillID(0x33), IntentID: fillID(0x44)}
m, err := buildStateRequest(in)
if err != nil {
t.Fatal(err)
}
got := readStateRequest(m)
if got != in {
t.Fatalf("StateRequest round-trip:\n got %+v\nwant %+v", got, in)
}
}
func TestWire_StateResult_RoundTrip(t *testing.T) {
in := StateResult{Kind: StateMarket, Exists: true, Known: true, Available: 42, BaseID: fillID(0x55), QuoteID: fillID(0x66)}
m, err := buildStateResult(in)
if err != nil {
t.Fatal(err)
}
got := readStateResult(m)
if got != in {
t.Fatalf("StateResult round-trip:\n got %+v\nwant %+v", got, in)
}
}
func TestWire_SwapIntentRequest_RoundTrip(t *testing.T) {
in := SwapIntentRequest{
NetworkID: 96369, Nonce: 7, AmountIn: 1_000_000, MinAmountOut: 990_000, Deadline: 1 << 40,
CChainID: fillID(0xC0), DChainID: fillID(0xD0), Account: fillAcct(0xAA),
AssetIn: fillID(0x01), AssetInAddr: fillAcct(0x02), MarketID: fillID(0x4D), Recipient: fillAcct(0xBB),
}
m, err := buildSwapIntentRequest(in)
if err != nil {
t.Fatal(err)
}
got := readSwapIntentRequest(m)
if got != in {
t.Fatalf("SwapIntentRequest round-trip:\n got %+v\nwant %+v", got, in)
}
}
func TestWire_PreparedIntent_RoundTrip(t *testing.T) {
in := PreparedIntent{
To: fillAcct(0x99), IntentID: fillID(0x4B), QuotedOut: 12345, AmountIn: 1_000_000,
DChainID: fillID(0xD0), CChainID: fillID(0xC0), Account: fillAcct(0xAA), Recipient: fillAcct(0xBB),
AssetIn: fillID(0x01), MarketID: fillID(0x4D),
Calldata: bytes.Repeat([]byte{0xCA}, 200), HookData: []byte{'D', 'I', '0', '1'},
}
m, err := buildPreparedIntent(in, MsgPrepare)
if err != nil {
t.Fatal(err)
}
if route := m.Flags() >> 8; route != MsgPrepare {
t.Fatalf("PreparedIntent route flag: got 0x%X want 0x%X", route, MsgPrepare)
}
got := readPreparedIntent(m)
// Compare field by field (slices need bytes.Equal).
if got.To != in.To || got.IntentID != in.IntentID || got.QuotedOut != in.QuotedOut || got.AmountIn != in.AmountIn ||
got.DChainID != in.DChainID || got.CChainID != in.CChainID || got.Account != in.Account || got.Recipient != in.Recipient ||
got.AssetIn != in.AssetIn || got.MarketID != in.MarketID {
t.Fatalf("PreparedIntent fixed-field round-trip:\n got %+v\nwant %+v", got, in)
}
if !bytes.Equal(got.Calldata, in.Calldata) {
t.Fatalf("PreparedIntent.Calldata: got %x want %x", got.Calldata, in.Calldata)
}
if !bytes.Equal(got.HookData, in.HookData) {
t.Fatalf("PreparedIntent.HookData: got %x want %x", got.HookData, in.HookData)
}
}
// TestWire_PreparedIntent_RouteFlags proves the same PreparedIntent payload,
// finalized under each route the dispatcher serves, carries the correct flag in
// Flags()>>8 and still round-trips every field. This replaces the old fixed-
// offset [6:8] byte-patch retag with the FinishWithFlags idiom: the flag must
// survive build->parse for both MsgPrepare (the prepare call) and MsgNotify
// (NotifyIntent), and the body must be untouched by the route choice.
func TestWire_PreparedIntent_RouteFlags(t *testing.T) {
in := PreparedIntent{
To: fillAcct(0x99), IntentID: fillID(0x4B), QuotedOut: 12345, AmountIn: 1_000_000,
DChainID: fillID(0xD0), CChainID: fillID(0xC0), Account: fillAcct(0xAA), Recipient: fillAcct(0xBB),
AssetIn: fillID(0x01), MarketID: fillID(0x4D),
Calldata: bytes.Repeat([]byte{0xCA}, 200), HookData: []byte{'D', 'I', '0', '1'},
}
for _, msgType := range []uint16{MsgPrepare, MsgNotify} {
m, err := buildPreparedIntent(in, msgType)
if err != nil {
t.Fatalf("buildPreparedIntent(%#x): %v", msgType, err)
}
if route := m.Flags() >> 8; route != msgType {
t.Fatalf("route flag for %#x: got 0x%X want 0x%X", msgType, route, msgType)
}
got := readPreparedIntent(m)
if got.IntentID != in.IntentID || got.MarketID != in.MarketID || got.AmountIn != in.AmountIn ||
!bytes.Equal(got.Calldata, in.Calldata) || !bytes.Equal(got.HookData, in.HookData) {
t.Fatalf("payload not preserved across route %#x:\n got %+v\nwant %+v", msgType, got, in)
}
}
}
func TestWire_DExportRef_RoundTrip(t *testing.T) {
in := DExportRef{SourceChainID: fillID(0xDD), SourceTxID: fillID(0xEE), OutputIndex: 9, IntentID: fillID(0x4B)}
m, err := buildDExportRef(in)
if err != nil {
t.Fatal(err)
}
got := readDExportRef(m)
if got != in {
t.Fatalf("DExportRef round-trip:\n got %+v\nwant %+v", got, in)
}
}
func TestWire_IntentStatus_RoundTrip(t *testing.T) {
in := IntentStatus{
IntentID: fillID(0x4B), Phase: PhaseCommitted,
Ref: DExportRef{SourceChainID: fillID(0xDD), SourceTxID: fillID(0xEE), OutputIndex: 3, IntentID: fillID(0x4B)},
Reason: "ok",
}
m, err := buildIntentStatus(in)
if err != nil {
t.Fatal(err)
}
got := readIntentStatus(m)
if got.IntentID != in.IntentID || got.Phase != in.Phase || got.Reason != in.Reason ||
got.Ref.SourceChainID != in.Ref.SourceChainID || got.Ref.SourceTxID != in.Ref.SourceTxID || got.Ref.OutputIndex != in.Ref.OutputIndex {
t.Fatalf("IntentStatus round-trip:\n got %+v\nwant %+v", got, in)
}
}
func TestWire_SettlementResult_RoundTrip(t *testing.T) {
in := SettlementSubmitResult{
Mode: SettleCalldata, To: fillAcct(0x99), ObjectKey: fillID(0x0B), CTxHash: fillID(0x0C),
Calldata: bytes.Repeat([]byte{0x5E}, 137),
}
m, err := buildSettlementResult(in)
if err != nil {
t.Fatal(err)
}
got := readSettlementResult(m)
if got.Mode != in.Mode || got.To != in.To || got.ObjectKey != in.ObjectKey || got.CTxHash != in.CTxHash {
t.Fatalf("SettlementResult fixed fields:\n got %+v\nwant %+v", got, in)
}
if !bytes.Equal(got.Calldata, in.Calldata) {
t.Fatalf("SettlementResult.Calldata: got %x want %x", got.Calldata, in.Calldata)
}
}
func TestWire_IntentWatchRef_RoundTrip(t *testing.T) {
in := IntentWatchRef{IntentID: fillID(0x4B)}
m, err := buildIntentWatchRef(in)
if err != nil {
t.Fatal(err)
}
got := readIntentWatchRef(m)
if got != in {
t.Fatalf("IntentWatchRef round-trip: got %x want %x", got.IntentID[:], in.IntentID[:])
}
}
// fillID returns an ID whose every byte is b (so any truncation/overlap shows).
func fillID(b byte) ID {
var id ID
for i := range id {
id[i] = b
}
return id
}
// fillAcct returns an Account whose every byte is b.
func fillAcct(b byte) Account {
var a Account
for i := range a {
a[i] = b
}
return a
}