Files
maker/dexpoolid_test.go
zeekay c8714c22b5 fix(dex): seed canonical custody-backed LUX/LUSD+LUX/LETH at the native poolId
The maker seeded books that no other native rail could find and that the venue
could not fund. Three concrete defects, all fixed and proven against the live
venue wire contract (github.com/luxfi/dex/pkg/{venue,zapwire,dchain}).

DEFECT 1 — poolId convention mismatch.
SymbolPoolID derived poolId = keccak256(symbol), but the entire native venue
stack derives ASCII(symbol) left-aligned into 32 bytes with byte[31]=0xD0
(pkg/venue.SymbolToPoolID, pkg/dchain.poolIDForSymbol, cmd/fourpath-live). The
maker's books therefore landed at a poolId the FIX/WS/precompile rails never
address. SymbolPoolID now uses the venue convention (one derivation, correct by
default for every symbol — cleaner than per-pair BindPool overrides, and matches
the DEX's documented "re-declare the frozen frame locally" pattern). Proven
byte-identical to the real venue: poolId("LUX/LUSD") = 4c55582f4c555344..d0.

DEFECT 2 — no custody (asset binding + funding).
DEXClient only spoke the asset-LESS path (ensure_market + place); the venue
rejects an order it cannot fund on a custody market. Added the FROZEN custody
frames (clob_open_market 96B, clob_deposit 88B, balance resp 9B) and OpenMarket /
Deposit / PoolID methods, byte-identical to zapwire. The seeder now, before
placing: opens each market with its (base, quote) ids and FUNDS the maker. The
deposit is sized with lockUnits — the EXACT fixed-point math of dchain.orderLock
(sell locks floor(size) base; buy locks ceil(size*price) quote) — so every
resting order is fundable and the maker deposits no more than the books lock.
Funding is aggregated PER ASSET across all pairs: two markets that share the
native LUX base sum into ONE LUX deposit, because a deposit is keyed by
(user,asset,amount) and a content-derived idempotency ref — depositing per
market-leg would make the second leg's byte-identical deposit dedup to a no-op
and starve it (caught by the cross-pair seed test).

DEFECT 3 — pinned stable asset ids.
Asset ids are full 32-byte injective ids, never derived from a symbol. Pinned
canonical DEVNET ids: LUX = all-zero (native, ids.Empty); LUSD/LETH = the a32
label form (ASCII ticker in the low bytes) the live settlement rail
(cmd/fourpath-live, conservation_test) already uses on devnet. The mainnet
C-Chain ERC-20 addresses are deliberately NOT used (DEVNET ONLY; documented for
repin when a devnet ERC-20 is deployed).

Symbols stay exactly "LUX/LUSD" and "LUX/LETH" (no per-run tags).
cmd/maker wires the two canonical custody pairs.

Tests (go test ./... — 17/17 pass, clean under -race): byte-equality of
SymbolPoolID / encodeOpenMarket / encodeDeposit / depositRef vs the venue;
lockUnits == orderLock; a custody-aware fake venue (per-asset balance ledger that
rejects underfunded places) proves open_market+deposit precede place, every place
is accepted because funded, and an unfunded place is rejected.
2026-06-22 04:05:28 -07:00

81 lines
3.4 KiB
Go

package maker
import (
"encoding/hex"
"testing"
)
// dexpoolid_test.go proves DEFECT-1 is fixed: the maker derives a symbol's
// poolId by the SAME convention every native venue rail uses, so a seeded book
// lands at the SAME 32-byte id the FIX, WS, and precompile rails address.
//
// The venue convention has THREE byte-identical homes:
// - github.com/luxfi/dex/pkg/venue.SymbolToPoolID (venue.go:140-145, FIX/WS)
// - github.com/luxfi/dex/pkg/dchain.poolIDForSymbol (localnet_e2e_test.go:65-71)
// - github.com/luxfi/dex/cmd/fourpath-live.poolIDForSymbol (main.go:72-77)
//
// All three are: copy ASCII(symbol) left-aligned into [32]byte, then set
// byte[31]=0xD0. We do NOT import dex/pkg/venue (that drags the whole dex module
// — and a geth minor bump — in for a 4-line pure function; the DEX's own pattern
// is to re-declare the frozen frame locally and assert byte-equality, exactly as
// dex.go's header documents). So we (a) re-state the venue algorithm verbatim as
// a reference and assert SymbolPoolID == it, and (b) pin the captured ground-truth
// hex the venue functions emit, so a drift in EITHER fails the build.
// venueSymbolToPoolID is copied VERBATIM from dex/pkg/venue/venue.go:140-145
// (== dchain.poolIDForSymbol == fourpath-live.poolIDForSymbol). If the venue ever
// changes the convention, this reference and the pinned vectors below must change
// together — and this test is where that divergence surfaces.
func venueSymbolToPoolID(symbol string) [32]byte {
var p [32]byte
copy(p[:], symbol)
p[31] = 0xD0
return p
}
func TestSymbolPoolIDMatchesVenueConvention(t *testing.T) {
for _, sym := range []string{"LUX/LUSD", "LUX/LETH", "FOO/BAR", ""} {
got := SymbolPoolID(sym)
want := venueSymbolToPoolID(sym)
if got != want {
t.Errorf("SymbolPoolID(%q) = %x, venue convention = %x", sym, got, want)
}
// The 0xD0 high-byte tag must always be present (collision guard vs the
// all-zero native asset id).
if got[31] != 0xD0 {
t.Errorf("SymbolPoolID(%q) byte[31] = 0x%02x, want 0xD0", sym, got[31])
}
}
}
// TestSymbolPoolIDGroundTruthVectors pins the EXACT hex the canonical venue
// functions emit for the two canonical devnet symbols (captured from the venue
// algorithm). A maker that frames a different poolId would route to a different
// book than every other rail — these vectors make that a build failure.
func TestSymbolPoolIDGroundTruthVectors(t *testing.T) {
vectors := map[string]string{
// 4c55582f4c555344 = ASCII "LUX/LUSD", left-aligned, byte[31]=0xd0.
"LUX/LUSD": "4c55582f4c5553440000000000000000000000000000000000000000000000d0",
// 4c55582f4c455448 = ASCII "LUX/LETH".
"LUX/LETH": "4c55582f4c4554480000000000000000000000000000000000000000000000d0",
}
for sym, wantHex := range vectors {
got := SymbolPoolID(sym)
if h := hex.EncodeToString(got[:]); h != wantHex {
t.Errorf("SymbolPoolID(%q) = %s, want %s", sym, h, wantHex)
}
}
}
// TestNotKeccakPoolID guards against a regression to the OLD keccak256(symbol)
// derivation (DEFECT 1): the keccak of "LUX/LUSD" has no 0xD0 tail and differs
// from the venue id, so a keccak-derived id would never carry byte[31]=0xD0 with
// the ASCII prefix intact.
func TestNotKeccakPoolID(t *testing.T) {
id := SymbolPoolID("LUX/LUSD")
// ASCII prefix must be the literal symbol bytes (keccak would scramble them).
if string(id[:8]) != "LUX/LUSD" {
t.Errorf("poolId prefix = %q, want literal symbol bytes (keccak regression?)", id[:8])
}
}