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.
121 lines
4.5 KiB
Go
121 lines
4.5 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
//go:build cgo
|
|
|
|
// dexvm_gpu_test.go — round-trip test for the GPU plugin bridge.
|
|
//
|
|
// Tests the dlopen→dlsym→call→result path against a real plugin
|
|
// dylib. The plugin path is supplied via LUX_GPU_PLUGIN_DIR; when
|
|
// unset (or no plugin matches), the test skips cleanly so the
|
|
// package builds + tests on CPU-only CI hosts.
|
|
//
|
|
// Fixture: a single-pool AMM swap with hand-chosen reserves whose
|
|
// constant-product result is small enough to write by hand and
|
|
// exact under integer arithmetic. The same fixture is what the
|
|
// canonical CPU oracle (~/work/lux/dex/pkg/lx::ConstantProductOut)
|
|
// returns, so a mismatch means the plugin produced wrong bytes.
|
|
|
|
package lxgpu
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// constantProductOutCPU is the in-test CPU oracle, kept self-contained
|
|
// so we don't introduce a test-time dependency on ~/work/lux/dex.
|
|
// Computes (a * b) / d using two uint64 multiplies and a uint128
|
|
// shift-divide. For our fixture a*b fits in 64 bits — keep the path
|
|
// uint64-only and assert numerically.
|
|
func constantProductOutCPU(rx, ry, amount uint64) uint64 {
|
|
denom := rx + amount
|
|
if denom == 0 {
|
|
return 0
|
|
}
|
|
// Numerator a*b: 1_000 * 2_000_000 = 2_000_000_000 — fits in uint64.
|
|
// Fixture is sized so the multiply stays in 64 bits; the GPU does
|
|
// it in 128 bits internally regardless.
|
|
return (amount * ry) / denom
|
|
}
|
|
|
|
// TestGPUAMMSwapRoundTrip dlopen's a backend plugin from
|
|
// LUX_GPU_PLUGIN_DIR, runs a single-pool xy=k swap on it, and
|
|
// compares the kernel output to the CPU oracle.
|
|
//
|
|
// To run: build the plugin (e.g. via cmake at
|
|
// the GPU plugin build tree ) and set
|
|
//
|
|
// export LUX_GPU_PLUGIN_DIR=/path/to/dir/containing/libluxgpu_backend_metal.dylib
|
|
// go test -tags cgo ./dexvm/ -run TestGPUAMMSwapRoundTrip -v
|
|
func TestGPUAMMSwapRoundTrip(t *testing.T) {
|
|
backend := AutoBackend()
|
|
if backend == GPUBackendNone {
|
|
t.Skipf("no GPU plugin loaded — set LUX_GPU_PLUGIN_DIR to a directory "+
|
|
"containing libluxgpu_backend_*.{dylib,so} (probe order: cuda → hip → "+
|
|
"metal → vulkan → webgpu). dlopen tried: %v", probeOrder())
|
|
}
|
|
t.Logf("loaded GPU plugin: backend=%s path=%s", backend, GPUPluginPath())
|
|
|
|
// Fixture: one pool, reserve_x=1_000_000, reserve_y=2_000_000,
|
|
// amount_in=1_000. Expected receive_y = 1_998 (verified by hand
|
|
// AND the CPU oracle below). The numbers are small on purpose:
|
|
// (a) the multiply 1_000 * 2_000_000 fits in 64 bits so the
|
|
// test is independent of the kernel's 128-bit multiply; (b) the
|
|
// quotient 2_000_000_000 / 1_001_000 = 1_998 + r/1_001_000 with
|
|
// r = 2_000_000_000 - 1_998 * 1_001_000 = 2_002_000 — non-zero
|
|
// so we exercise the floor-division path.
|
|
reserves := []LuxAmmReservePair{
|
|
{ReserveX: 1_000_000, ReserveY: 2_000_000},
|
|
}
|
|
amounts := []uint64{1_000}
|
|
wantOut := constantProductOutCPU(1_000_000, 2_000_000, 1_000)
|
|
if wantOut != 1_998 {
|
|
t.Fatalf("CPU oracle drift: got %d, want 1_998", wantOut)
|
|
}
|
|
|
|
outs, err := AMMSwap(reserves, amounts)
|
|
if err != nil {
|
|
t.Fatalf("AMMSwap: %v", err)
|
|
}
|
|
if got, want := len(outs), len(amounts); got != want {
|
|
t.Fatalf("AMMSwap returned wrong length: got %d, want %d", got, want)
|
|
}
|
|
if outs[0] != wantOut {
|
|
t.Errorf("AMMSwap[0]: got %d, want %d (kernel returned wrong bytes — "+
|
|
"plugin uint128 multiply may be byte-swapped or the input layout drifted)",
|
|
outs[0], wantOut)
|
|
}
|
|
|
|
// Sanity bounds: the receive amount MUST be > 0 (the pool has
|
|
// reserves and the swap input is non-zero) AND MUST be strictly
|
|
// less than reserve_y (xy=k is a strict bijection on positive
|
|
// inputs — full drain is impossible).
|
|
if outs[0] == 0 {
|
|
t.Error("AMMSwap[0] = 0; non-degenerate input must produce a positive output")
|
|
}
|
|
if outs[0] >= reserves[0].ReserveY {
|
|
t.Errorf("AMMSwap[0] = %d >= reserve_y = %d; xy=k invariant violated",
|
|
outs[0], reserves[0].ReserveY)
|
|
}
|
|
}
|
|
|
|
// TestGPUAMMSwapEmpty verifies the n=0 fast path — no plugin call,
|
|
// just a zero-length slice. Runs regardless of whether a plugin is
|
|
// loaded so the bookkeeping path stays covered on CPU-only hosts.
|
|
func TestGPUAMMSwapEmpty(t *testing.T) {
|
|
outs, err := AMMSwap(nil, nil)
|
|
if AutoBackend() == GPUBackendNone {
|
|
if err != ErrGPUNotAvailable {
|
|
t.Fatalf("AMMSwap on n=0 with no plugin: got err=%v, want ErrGPUNotAvailable", err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("AMMSwap(nil, nil): %v", err)
|
|
}
|
|
if len(outs) != 0 {
|
|
t.Fatalf("AMMSwap(nil, nil) returned len=%d, want 0", len(outs))
|
|
}
|
|
}
|
|
|