mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
NEW pkg/zapwire: pure-Go leaf holding the frozen CLOB ZAP frame (Submit=66/Place=65/ Cancel=40/EnsureMarket=32/Ack=24, FillWireSize=17, float64 codec) so the public precompile imports the wire format without the cgo/GPU engine. SubmitMarketable (IOC taker): removed time.Now()+atomic.AddUint64 from the consensus path; trade ts derives from block-derived order ts; fills carry maker+taker identity. zap_server.go docstring demoted authority-> gateway. nocgo: verifyOrdersCPU returns nil for empty. Verified pkg/zapwire 6/6, full ./... green.
218 lines
7.2 KiB
Go
218 lines
7.2 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//go:build cgo
|
|
|
|
// three_mode_parity_test.go — proves the DEX produces IDENTICAL match outcomes
|
|
// across the three execution engines for the same order sequence:
|
|
//
|
|
// 1. pure-Go : lx.OrderBook (the canonical Go/CPU reference matcher that
|
|
// lives in luxfi/dex — the d-chain's matcher)
|
|
// 2. CPU/C++ : clobMatchStepCPU — the Go port of the canonical C++ oracle
|
|
// (dex_cpu_oracle.hpp), uint256, byte-equal to the C++ backend
|
|
// 3. GPU : CLOBMatch via the loaded plugin (Metal locally), uint256,
|
|
// device-resident BookArena
|
|
//
|
|
// The three engines have DIFFERENT data models (lx.OrderBook is float/uint64
|
|
// and price-level aggregated; the GPU/oracle are uint256 and per-level arenas),
|
|
// so parity is asserted at the shared, model-independent abstraction every CLOB
|
|
// must agree on: for each incoming order, the TOTAL FILLED QUANTITY and the
|
|
// volume-weighted average execution price (VWAP).
|
|
//
|
|
// Modes 2 and 3 are already proven byte-identical to each other by
|
|
// TestCLOBGPUvsCPUByteParity. This test adds the third leg — the lx.OrderBook
|
|
// pure-Go matcher — and ties all three together.
|
|
//
|
|
// Run (Metal, this Mac):
|
|
//
|
|
// export SDKROOT=$(xcrun --show-sdk-path)
|
|
// export LUX_GPU_PLUGIN_DIR=/Users/z/work/luxcpp/gpu/build/metal_backend
|
|
// GOWORK=off go test ./pkg/lxgpu/ -run TestThreeModeMatchParity -v
|
|
|
|
package lxgpu
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/dex/pkg/lx"
|
|
)
|
|
|
|
// u256ToU64 collapses a uint256 to uint64, asserting the high 192 bits are
|
|
// zero. Every value in the parity scenarios fits in uint64 (that is the pure-Go
|
|
// book's native width), so this is lossless for the test inputs and lets us
|
|
// compare GPU/oracle output against the uint64 engine directly.
|
|
func u256ToU64(t *testing.T, v u256) uint64 {
|
|
t.Helper()
|
|
for i := 0; i < 24; i++ {
|
|
if v[i] != 0 {
|
|
t.Fatalf("u256 value exceeds uint64: %x", v)
|
|
}
|
|
}
|
|
var r uint64
|
|
for i := 24; i < 32; i++ {
|
|
r = (r << 8) | uint64(v[i])
|
|
}
|
|
return r
|
|
}
|
|
|
|
// goOrderbookStep feeds one order into the canonical lx.OrderBook and returns
|
|
// the (filledQty, vwap) outcome for that order, computed from the trades it
|
|
// produced — the same (filled, vwap) abstraction the GPU/oracle report.
|
|
//
|
|
// userID is derived from the side so a taker never self-trades the resting
|
|
// makers (lx.OrderBook skips self-trades; the GPU/oracle have no owner
|
|
// concept). Two stable users — one per side — keep the models comparable. The
|
|
// order ID and timestamp are set explicitly (never auto-assigned) so the run is
|
|
// deterministic and independent of wall-clock time or process-local counters.
|
|
func goOrderbookStep(ob *lx.OrderBook, s clobStep, seq int64) (filled, vwap uint64) {
|
|
side := lx.Side(s.side) // 0 = bid/buy, 1 = ask/sell
|
|
user := "bid" // bid maker/taker
|
|
if side == lx.Sell {
|
|
user = "ask" // ask user — distinct so crosses never self-trade
|
|
}
|
|
|
|
order := &lx.Order{
|
|
ID: uint64(seq), // explicit, distinct id per step
|
|
Symbol: "PARITY",
|
|
Side: side,
|
|
Type: lx.Limit,
|
|
Price: float64(s.price),
|
|
Size: float64(s.qty),
|
|
User: user,
|
|
UserID: user,
|
|
TimeInForce: "GTC",
|
|
Timestamp: time.Unix(0, seq), // deterministic price-time priority
|
|
}
|
|
|
|
// Capture the trade-log tail BEFORE matching so we slice exactly the trades
|
|
// this order produced. AddOrder matches on insert (continuous CLOB) and
|
|
// appends each fill to ob.Trades.
|
|
start := len(ob.Trades)
|
|
if ob.AddOrder(order) == 0 && order.Status == lx.Rejected {
|
|
return 0, 0
|
|
}
|
|
|
|
var notional uint64
|
|
for _, tr := range ob.Trades[start:] {
|
|
q := uint64(tr.Size)
|
|
filled += q
|
|
notional += q * uint64(tr.Price)
|
|
}
|
|
if filled > 0 {
|
|
vwap = notional / filled // floor division — matches the oracle's u256VWAP
|
|
}
|
|
return filled, vwap
|
|
}
|
|
|
|
// TestThreeModeMatchParity drives the fixed parity sequence through all three
|
|
// engines and asserts the (filled, vwap) outcome of every order is identical
|
|
// across pure-Go, the CPU oracle, and the GPU.
|
|
func TestThreeModeMatchParity(t *testing.T) {
|
|
backend := AutoBackend()
|
|
gpuActive := backend != GPUBackendNone
|
|
if !gpuActive {
|
|
t.Logf("no GPU plugin loaded (probe order %v) — running pure-Go vs CPU "+
|
|
"oracle only; set LUX_GPU_PLUGIN_DIR for the GPU leg", probeOrder())
|
|
} else {
|
|
t.Logf("GPU backend=%s plugin=%s", backend, GPUPluginPath())
|
|
}
|
|
|
|
seq := paritySequence()
|
|
|
|
// --- pure-Go engine (the canonical lx.OrderBook matcher) ---
|
|
// EnableImmediateMatching makes AddOrder cross the incoming order against the
|
|
// book and rest the residual — the same "match-on-insert, residual rests"
|
|
// semantic the CPU oracle (clobMatchStepCPU) and the GPU kernel implement.
|
|
ob := lx.NewOrderBook("PARITY")
|
|
ob.EnableImmediateMatching = true
|
|
goOut := make([][2]uint64, len(seq))
|
|
for i, s := range seq {
|
|
f, v := goOrderbookStep(ob, s, int64(i+1))
|
|
goOut[i] = [2]uint64{f, v}
|
|
}
|
|
|
|
// --- CPU oracle engine ---
|
|
var cpu bookArena
|
|
cpuOut := make([][2]uint64, len(seq))
|
|
for i, s := range seq {
|
|
out := clobMatchStepCPU(&cpu, s.side, u256FromU64(s.price), u256FromU64(s.qty))
|
|
cpuOut[i] = [2]uint64{u256ToU64(t, out.filled), u256ToU64(t, out.avgPrice)}
|
|
}
|
|
|
|
// --- GPU engine (optional) ---
|
|
var gpuOut [][2]uint64
|
|
if gpuActive {
|
|
arena, err := ArenaCreate()
|
|
if err != nil {
|
|
t.Fatalf("ArenaCreate: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if derr := ArenaDestroy(arena); derr != nil {
|
|
t.Errorf("ArenaDestroy: %v", derr)
|
|
}
|
|
})
|
|
var user [20]byte
|
|
var bookID [32]byte
|
|
bookID[31] = 7
|
|
gpuOut = make([][2]uint64, len(seq))
|
|
for i, s := range seq {
|
|
cd := encodeCLOBCalldata(s.side, u256FromU64(s.price), u256FromU64(s.qty), user, bookID)
|
|
out, _, merr := CLOBMatch(arena, cd)
|
|
if merr != nil {
|
|
t.Fatalf("CLOBMatch[%s]: %v", s.name, merr)
|
|
}
|
|
var filled, avg u256
|
|
copy(filled[:], out[0:32])
|
|
copy(avg[:], out[32:64])
|
|
gpuOut[i] = [2]uint64{u256ToU64(t, filled), u256ToU64(t, avg)}
|
|
}
|
|
}
|
|
|
|
// --- assert identical (filled, vwap) across all active engines ---
|
|
for i, s := range seq {
|
|
if goOut[i] != cpuOut[i] {
|
|
t.Errorf("pure-Go vs CPU mismatch at %q: go=(filled=%d,vwap=%d) cpu=(filled=%d,vwap=%d)",
|
|
s.name, goOut[i][0], goOut[i][1], cpuOut[i][0], cpuOut[i][1])
|
|
}
|
|
if gpuActive {
|
|
if gpuOut[i] != cpuOut[i] {
|
|
t.Errorf("GPU vs CPU mismatch at %q: gpu=(filled=%d,vwap=%d) cpu=(filled=%d,vwap=%d)",
|
|
s.name, gpuOut[i][0], gpuOut[i][1], cpuOut[i][0], cpuOut[i][1])
|
|
}
|
|
if gpuOut[i] != goOut[i] {
|
|
t.Errorf("GPU vs pure-Go mismatch at %q: gpu=(filled=%d,vwap=%d) go=(filled=%d,vwap=%d)",
|
|
s.name, gpuOut[i][0], gpuOut[i][1], goOut[i][0], goOut[i][1])
|
|
}
|
|
}
|
|
t.Logf("%-22s filled/vwap go=%d/%d cpu=%d/%d%s", s.name,
|
|
goOut[i][0], goOut[i][1], cpuOut[i][0], cpuOut[i][1],
|
|
gpuLogSuffix(gpuActive, gpuOut, i))
|
|
}
|
|
}
|
|
|
|
func gpuLogSuffix(active bool, gpuOut [][2]uint64, i int) string {
|
|
if !active {
|
|
return " gpu=SKIP"
|
|
}
|
|
return sprintGPU(gpuOut[i])
|
|
}
|
|
|
|
func sprintGPU(v [2]uint64) string {
|
|
return " gpu=" + itoa(v[0]) + "/" + itoa(v[1])
|
|
}
|
|
|
|
func itoa(v uint64) string {
|
|
if v == 0 {
|
|
return "0"
|
|
}
|
|
var b [20]byte
|
|
i := len(b)
|
|
for v > 0 {
|
|
i--
|
|
b[i] = byte('0' + v%10)
|
|
v /= 10
|
|
}
|
|
return string(b[i:])
|
|
}
|