refactor(cmd): consolidate cmd/* into one binary — dexd

dexd is now the ONLY command in the repo. One binary, two run modes:

  dexd            run as a luxd rpcchainvm PLUGIN. luxd execs the plugin from
  dexd plugin     its PluginDir with NO positional args (engine address arrives
                  via the runtime.EngineAddressKey env var) and probes the
                  version with `dexd version`. No-subcommand == plugin is
                  REQUIRED to preserve that contract — byte-for-byte the old
                  cmd/dchain behavior (rpc.Serve over the plugin transport).
  dexd run        run the STANDALONE D-Chain venue: open the on-disk zapdb,
  dexd standalone serve the frozen dex_* surface over ZAP, and drive the
                  consensus sealer loop (WaitForEvent->BuildBlock->Verify->
                  Accept). -addr/-db flags preserved from the old cmd/dvenue.

Relocated logic (engine code NOT duplicated — pkg/dchain + pkg/lx untouched):
  cmd/dchain/{main,runner}   -> internal/plugin     (plugin.Run)
  cmd/dvenue/{main,engine_*} -> internal/standalone (standalone.Run + engineName)
  cmd/dexd/main.go            = thin run-mode dispatcher

Removed cmd dirs (12): dchain, dvenue, demo, dex-api-server, dex-server,
dexfillproof, dexseed, fix-gateway, gateway, monitor, persistent-server,
price-feed. Removed committed build artifacts: gateway, dex-server-linux.

Build/ship ONLY dexd:
  - Dockerfile: canonical CPU image (CGO=0, ghcr.io/luxfi/dex) running
    `dexd run`; no GPU/MLX required.
  - Dockerfile.dvenue: GPU operator bundle repointed cmd/dvenue -> cmd/dexd.
  - Deleted Dockerfile.{gateway,devnet,minimal,dvenue-cpu} and
    docker/{Dockerfile,Dockerfile.cuda,backend/Dockerfile,compose-simple.yml}.
  - Makefile rewritten to build/run/ship dexd only.
  - docker.yml: dropped the dex-gateway image; dex-venue-image.yml: dvenue->dexd.
  - Deleted legacy scripts that built removed/long-gone cmds; fixed deploy.sh +
    dev-setup.sh; .gitignore ignores root build binaries.

Also included (coupled, environment-applied, settled): the in-progress
clob -> dex/orderbook terminology refactor across pkg/* + sdk/* + tests/*
(identifier + file renames, e.g. RegisterCLOB->RegisterDEX,
source_clob.go->source_orderbook.go). The relocated standalone venue calls the
renamed API, so it lands in the same consistent snapshot.

Verified (GOWORK=off CGO_ENABLED=0): `go build ./cmd/dexd`, `go build ./...`,
`go vet ./...` all exit 0; `go test ./pkg/dchain` (renamed RegisterDEX socket
path) green. GPU/cgo path not compiled locally (no CUDA); its rename mirrors
the verified nocgo twins and preserves the C ABI symbol names.
This commit is contained in:
2026-06-26 12:24:34 -07:00
parent afcc765854
commit 21301212f9
113 changed files with 876 additions and 5665 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# Build + push the D-Chain VENUE image (dex family) — ghcr.io/luxfi/dex-venue.
#
# This is the missing artifact that blocks D-Chain turn-on: the dchain-tagged
# luxd + the dvenue GPU matching engine in one image (see Dockerfile.dvenue).
# luxd + the dexd GPU matching engine in one image (see Dockerfile.dvenue).
# The public ghcr.io/luxfi/node image does NOT carry the DexVM; this private
# build does.
#
@@ -79,7 +79,7 @@ jobs:
- name: Resolve cross-repo PAT for private luxfi/* deps
id: pat
env:
# The dvenue build pulls private luxfi/* modules and the private
# The dexd build pulls private luxfi/* modules and the private
# luxcpp/dex release tarball; the default GITHUB_TOKEN is repo-scoped.
# Prefer org GH_TOKEN, fall back to repo UNIVERSE_PAT (same order as
# node/docker.yml).
-2
View File
@@ -18,8 +18,6 @@ jobs:
include:
- image: ghcr.io/luxfi/dex
dockerfile: Dockerfile
- image: ghcr.io/luxfi/dex-gateway
dockerfile: Dockerfile.gateway
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
with:
image: ${{ matrix.image }}
+5
View File
@@ -96,3 +96,8 @@ QWEN.md
server.log
lxd
dex-server-linux
# Compiled binaries built at repo root (canonical build is bin/dexd via `make build`)
/dexd
/dvenue
/gateway
+48 -23
View File
@@ -1,33 +1,58 @@
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
# ---------------------------------------------------------------------------
# dexd — the ONE D-Chain DEX image (CPU, linux/amd64) — ghcr.io/luxfi/dex
#
# Builds the single `dexd` binary and runs it as the standalone D-Chain venue:
# it opens the persisted zapdb, serves the frozen clob_* surface over ZAP, and
# runs the consensus sealer loop itself (WaitForEvent->BuildBlock->Verify->Accept)
# so every write returns consensus-computed fills. CGO_ENABLED=0 selects pkg/lx's
# pure-Go CPU matcher (the !cgo build tag) — no CUDA, no MLX, runs on any amd64.
#
# Same binary, other modes (override the default CMD):
# docker run … plugin run as a luxd rpcchainvm plugin (luxd execs it)
# docker run … version print the plugin version line
# ---------------------------------------------------------------------------
ARG GO_VERSION=1.26.4
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS builder
RUN apk add --no-cache git ca-certificates
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates && rm -rf /var/lib/apt/lists/*
ARG GITHUB_TOKEN
RUN git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
# luxfi module resolution: private modules resolve via the gh_token BuildKit
# secret over HTTPS (the shared hanzoai/.github docker-build.yml passes it);
# skip sumdb for luxfi (tags may be rewritten — go.sum in the build context is
# already realigned to current tag bits).
ENV GOPRIVATE=github.com/luxfi/*,github.com/hanzoai/*
ENV GOFLAGS=-mod=mod
ENV GOTOOLCHAIN=auto
ENV GOEXPERIMENT=jsonv2
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
RUN --mount=type=secret,id=gh_token,required=false \
if [ -s /run/secrets/gh_token ]; then \
git config --global url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/".insteadOf "https://github.com/"; \
fi && \
go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dex-server ./cmd/dex-server/main.go
# CPU build: CGO_ENABLED=0 → !cgo build tag → pure-Go CPU matcher in pkg/lx.
RUN --mount=type=secret,id=gh_token,required=false \
if [ -s /run/secrets/gh_token ]; then \
git config --global url."https://x-access-token:$(cat /run/secrets/gh_token)@github.com/".insteadOf "https://github.com/"; \
fi && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o /out/dexd ./cmd/dexd
FROM alpine:3.21
RUN apk add --no-cache ca-certificates && \
addgroup -g 1000 -S dex && \
adduser -u 1000 -S dex -G dex
WORKDIR /home/dex
COPY --from=builder /app/dex-server .
RUN chown dex:dex dex-server
USER dex
EXPOSE 8080 9090 50051
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
CMD ["./dex-server"]
FROM debian:12-slim AS execution
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates && rm -rf /var/lib/apt/lists/* \
&& groupadd -g 1000 dexd && useradd -u 1000 -g dexd -m -s /usr/sbin/nologin dexd \
&& mkdir -p /data && chown -R dexd:dexd /data
COPY --from=builder /out/dexd /usr/local/bin/dexd
USER dexd
EXPOSE 9099
# Default: standalone venue. Override CMD with `plugin` / `version` for the
# other run modes.
ENTRYPOINT ["/usr/local/bin/dexd"]
CMD ["run", "-addr", "0.0.0.0:9099", "-db", "/data/db"]
-16
View File
@@ -1,16 +0,0 @@
FROM golang:1.26-alpine AS builder
RUN apk add --no-cache git
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gateway ./cmd/gateway/
FROM alpine:3.21
RUN apk --no-cache add ca-certificates && addgroup -g 1000 -S gw && adduser -u 1000 -S gw -G gw
WORKDIR /home/gw
COPY --from=builder /app/gateway .
RUN chown gw:gw gateway
USER gw
EXPOSE 8080
ENTRYPOINT ["./gateway"]
+25 -23
View File
@@ -6,17 +6,18 @@
# image (which excludes the DexVM). It bundles TWO binaries the operator venue
# StatefulSet runs from one image:
#
# /usr/local/bin/dvenue — the ZAP matching engine (luxfi/dex cmd/dvenue,
# //go:build cgo, built CGO_ENABLED=1). This is the
# /usr/local/bin/dexd — the ZAP matching engine (luxfi/dex cmd/dexd, run
# as `dexd run`, built CGO_ENABLED=1). This is the
# real CLOB:
# it links the GPU matching kernels (libdex_clob_cuda
# + libamm_xyk_cuda) and the secp256k1 ecrecover
# bundle via pkg-config. The 0x9010 C-Chain precompile
# and the chains/dexvm atomic proxy dial it.
# CGO_ENABLED=1 makes pkg/lx link the GPU matching
# kernels (libdex_clob_cuda + libamm_xyk_cuda) and the
# secp256k1 ecrecover bundle via pkg-config. The 0x9010
# C-Chain precompile and the chains/dexvm atomic proxy
# dial it.
# /luxd/build/luxd — the dchain-tagged node (`-tags dchain`), which
# links the chains/dexvm PROXY so the venue node
# serves the P-chain-registered D-Chain. The proxy
# relays to the dvenue socket; it is pure-Go (no GPU).
# relays to the dexd socket; it is pure-Go (no GPU).
#
# ARCH: linux/arm64 ONLY. The matcher's `cgo && linux` path in luxfi/dex pkg/lx
# (orderbook_cuda.go + amm_gpu_cuda.go) has no non-CUDA fallback on any arch, so
@@ -31,7 +32,7 @@
# ---------------------------------------------------------------------------
ARG GO_VERSION=1.26.4
# CUDA 12.x devel image (arm64/sbsa variant) provides nvcc-free libcudart for
# the cgo link of dvenue. Runtime stage uses the matching -runtime image.
# the cgo link of dexd. Runtime stage uses the matching -runtime image.
ARG CUDA_VERSION=12.4.1
ARG UBUNTU_VERSION=22.04
@@ -75,7 +76,7 @@ RUN --mount=type=secret,id=ghtok,required=false \
# lux-crypto-secp256k1 at the cgo link step below.
#
# CI/RELEASE GAP (until the dex-gpu-release workflow has run on a tag): the URL
# below 404s. The build then fails at the dvenue link step with a clear
# below 404s. The build then fails at the dexd link step with a clear
# pkg-config error. Run luxcpp/dex dex-gpu-release first (or point VENUE_DEPS_URL
# at the workflow artifact) to populate it.
ARG VENUE_DEPS_VERSION=v0.1.1
@@ -88,19 +89,20 @@ RUN --mount=type=secret,id=ghtok,required=false \
&& ldconfig 2>/dev/null || true
ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
# --- Build dvenue (the GPU matcher) from luxfi/dex ---------------------------
# cmd/dvenue is `//go:build cgo`: CGO_ENABLED=1 (which sets the `cgo` build tag)
# selects pkg/lx's GPU matcher (orderbook_cuda + amm_gpu_cuda + signed_order_gpu,
# all gated on `cgo`), satisfied by the pkg-config libs above. `-tags cgo` is
# explicit-for-readability (CGO_ENABLED=1 already implies it). The pure-Go CPU
# venue is cmd/dvenue-cpu (//go:build !cgo) — not built here.
# --- Build dexd (the GPU matcher) from luxfi/dex -----------------------------
# CGO_ENABLED=1 (which sets the `cgo` build tag) makes pkg/lx select its GPU
# matcher (orderbook_cuda + amm_gpu_cuda + signed_order_gpu, all gated on `cgo`),
# satisfied by the pkg-config libs above. `-tags cgo` is explicit-for-readability
# (CGO_ENABLED=1 already implies it). The operator runs this binary as
# `dexd run`. The pure-Go CPU build of the same binary is the canonical
# Dockerfile (CGO_ENABLED=0) — not built here.
WORKDIR /build/dex
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \
go build -trimpath -tags "cgo" -ldflags="-s -w" \
-o /out/dvenue ./cmd/dvenue
-o /out/dexd ./cmd/dexd
# --- Build the dchain-tagged luxd (the proxy node) ---------------------------
# Cloned from luxfi/node at a pinned release and built `-tags dchain` so it
@@ -121,7 +123,7 @@ RUN go mod download
# CGO_ENABLED=0: the dchain luxd's only cgo edge is the optional luxcpp/cevm
# C-Chain backend; the dexvm proxy + pure-Go EVM need no native libs (the
# build graph pulls only runtime/cgo). Building pure-Go keeps the venue node
# free of the cevm fetch — the GPU/cgo surface lives entirely in dvenue.
# free of the cevm fetch — the GPU/cgo surface lives entirely in dexd.
RUN GITCOMMIT=$(git rev-parse HEAD) && \
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -trimpath -tags "nattraversal,dchain" -ldflags="-s -w \
@@ -132,7 +134,7 @@ RUN GITCOMMIT=$(git rev-parse HEAD) && \
# node registers the optional VMs (incl. the dexvm proxy) + P/X in-process, but
# the C-Chain EVM is an EXTERNAL plugin (same as the public node image's EVM
# Plugin Stage). The venue --track-all-chains, and the 0x9010 dex precompile
# (NewZAPEngine(dex-zap-endpoint) -> the dvenue socket) lives in this plugin, so
# (NewZAPEngine(dex-zap-endpoint) -> the dexd socket) lives in this plugin, so
# the venue MUST ship it. Built from the pinned luxfi/evm release, CGO=0,
# written to /luxd/build/plugins/<EVM_VM_ID> — startup.sh seeds it into
# /data/plugins (--plugin-dir).
@@ -153,16 +155,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Carry the GPU/crypto shared libs the dvenue binary may dlopen / rpath into.
# Carry the GPU/crypto shared libs the dexd binary may dlopen / rpath into.
COPY --from=builder /usr/local/lib /usr/local/lib
COPY --from=builder /usr/local/include /usr/local/include
RUN ldconfig 2>/dev/null || true
# Binaries: the dchain luxd (venue node) + the EVM (C-Chain) plugin it loads +
# dvenue (ZAP matcher). startup.sh seeds /luxd/build/plugins/* into /data/plugins.
# dexd (ZAP matcher). startup.sh seeds /luxd/build/plugins/* into /data/plugins.
COPY --from=builder /luxd/build/luxd /luxd/build/luxd
COPY --from=builder /luxd/build/plugins /luxd/build/plugins
COPY --from=builder /out/dvenue /usr/local/bin/dvenue
COPY --from=builder /out/dexd /usr/local/bin/dexd
# Non-root (matches the operator StatefulSet securityContext runAsUser 1000).
RUN groupadd -g 1000 venue && useradd -u 1000 -g venue -m -s /usr/sbin/nologin venue \
@@ -172,9 +174,9 @@ USER venue
ENV PATH="/luxd/build:/usr/local/bin:${PATH}"
WORKDIR /luxd/build
# 9650 luxd HTTP RPC, 9651 luxd staking, 9099 dvenue ZAP socket.
# 9650 luxd HTTP RPC, 9651 luxd staking, 9099 dexd ZAP socket.
EXPOSE 9650 9651 9099
# Default to the venue node; the operator overrides the `zap` container's
# command to /usr/local/bin/dvenue.
# command to `/usr/local/bin/dexd run`.
CMD ["/luxd/build/luxd"]
-54
View File
@@ -1,54 +0,0 @@
# syntax=docker/dockerfile:1
# ---------------------------------------------------------------------------
# D-Chain VENUE image (CPU, linux/amd64) — ghcr.io/luxfi/dchain
#
# The pure-Go CPU sealing venue for devnet. cmd/dvenue is matcher-agnostic;
# CGO_ENABLED=0 selects pkg/lx's pure-Go CPU matcher (amm_nogpu /
# orderbook_cuda_stub via the !cgo build tag in engine_nocgo.go). No CUDA,
# no GPU tarball — runs on any amd64 node. This is the binary the deployed
# `dchain-venue` Deployment runs as `dvenue -addr 0.0.0.0:9099 -db /data/db`:
# it opens the persisted zapdb, serves the frozen clob_* surface over ZAP,
# and runs the consensus sealer loop (WaitForEvent->BuildBlock->Verify->Accept)
# so every write returns consensus-computed fills.
# ---------------------------------------------------------------------------
ARG GO_VERSION=1.26.4
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates && rm -rf /var/lib/apt/lists/*
# luxfi module resolution: private modules (corona etc.) via ghtok BuildKit
# secret over HTTPS; skip sumdb for luxfi (tags may be rewritten — go.sum in
# the build context is already realigned to current tag bits).
ENV GOPRIVATE=github.com/luxfi/*
ENV GOFLAGS=-mod=mod
ENV GOTOOLCHAIN=auto
ENV GOEXPERIMENT=jsonv2
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=secret,id=ghtok,required=false \
if [ -s /run/secrets/ghtok ]; then \
git config --global url."https://x-access-token:$(cat /run/secrets/ghtok)@github.com/".insteadOf "https://github.com/"; \
fi && \
go mod download
COPY . .
# CPU venue: CGO_ENABLED=0 → !cgo build tag → pure-Go CPU matcher.
RUN --mount=type=secret,id=ghtok,required=false \
if [ -s /run/secrets/ghtok ]; then \
git config --global url."https://x-access-token:$(cat /run/secrets/ghtok)@github.com/".insteadOf "https://github.com/"; \
fi && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o /out/dvenue ./cmd/dvenue
FROM debian:12-slim AS execution
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates && rm -rf /var/lib/apt/lists/* \
&& groupadd -g 1000 venue && useradd -u 1000 -g venue -m -s /usr/sbin/nologin venue \
&& mkdir -p /data && chown -R venue:venue /data
COPY --from=builder /out/dvenue /usr/local/bin/dvenue
USER venue
EXPOSE 9099
# The deployment passes -addr 0.0.0.0:9099 -db /data/db.
ENTRYPOINT ["/usr/local/bin/dvenue"]
-39
View File
@@ -1,39 +0,0 @@
# Build stage - use BUILDPLATFORM for native compilation, cross-compile via TARGETOS/TARGETARCH
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
RUN apk add --no-cache git make gcc musl-dev
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o gateway ./cmd/gateway/
# Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
RUN addgroup -g 1000 -S gateway && \
adduser -u 1000 -S gateway -G gateway
WORKDIR /home/gateway
COPY --from=builder /app/gateway .
RUN chown -R gateway:gateway /home/gateway
USER gateway
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
ENTRYPOINT ["./gateway"]
-5
View File
@@ -1,5 +0,0 @@
FROM alpine:3.21
RUN apk add --no-cache ca-certificates
COPY dex-server-linux /usr/local/bin/dex-server
EXPOSE 8080 50051
ENTRYPOINT ["dex-server"]
+64 -286
View File
@@ -1,226 +1,100 @@
# LX Makefile
# Complete build, test, and deployment automation
# dexd Makefile — the ONE D-Chain DEX binary.
#
# dexd runs the DEX two ways (one binary, two modes):
# dexd run as a luxd rpcchainvm plugin (luxd execs it, no args)
# dexd run run the standalone D-Chain venue (ZAP + consensus sealer)
#
# Default build is CPU (CGO_ENABLED=0) — no GPU/MLX required. The GPU matcher
# (CGO_ENABLED=1, pkg/lx cuda kernels) is built only by Dockerfile.dvenue.
SHELL := /bin/bash
.PHONY: all build test clean help
.PHONY: all build run test test-race bench vet fmt lint deps clean coverage \
docker proto sdk-typescript sdk-python sdk-go sdk-all ci help
# Version and build info
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
# Version / build info
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
# Go parameters
GO := go
GOBUILD := $(GO) build
GOCLEAN := $(GO) clean
GOTEST := $(GO) test
GOGET := $(GO) get
GOMOD := $(GO) mod
GOVET := $(GO) vet
GOFMT := gofmt
# Build parameters
# Go
GO := go
CGO_ENABLED ?= 0
LDFLAGS := -ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) -X main.GitCommit=$(GIT_COMMIT)"
BINARY := dexd
BINARY_DIR := bin
IMAGE := ghcr.io/luxfi/dex
TEST_TIMEOUT := 60s
# Binary output
BINARY_NAME := luxd
BINARY_DIR := bin
export CGO_ENABLED
# Test parameters
TEST_TIMEOUT := 30s
BENCH_TIME := 10s
# Default target
all: clean fmt vet test build
@echo "✅ Build complete!"
all: fmt vet test build
help:
@echo "LX Makefile Commands:"
@echo ""
@echo "Development:"
@echo " make build - Build all binaries"
@echo " make test - Run all tests"
@echo " make bench - Run benchmarks"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Running:"
@echo " make run - Run single node"
@echo " make run-cluster - Run 3-node cluster"
@echo ""
@echo "Quality:"
@echo " make fmt - Format code"
@echo " make vet - Run go vet"
@echo " make lint - Run linters"
@echo ""
@echo "Docker:"
@echo " make docker-build - Build Docker images"
@echo " make up - Start with docker-compose"
@echo " make down - Stop docker-compose"
@echo "dexd Makefile:"
@echo " make build - Build dexd (CPU, CGO_ENABLED=0) -> $(BINARY_DIR)/$(BINARY)"
@echo " make run - Build and run the standalone venue (dexd run)"
@echo " make test - Run package tests"
@echo " make vet - go vet ./..."
@echo " make fmt - gofmt -s -w ."
@echo " make lint - golangci-lint run"
@echo " make docker - Build the canonical image ($(IMAGE)) from ./Dockerfile"
@echo " make clean - Remove build artifacts"
# Build targets
build: build-go build-tools
build-go:
@echo "🔨 Building luxd..."
# Build the one binary (CPU).
build:
@echo "Building $(BINARY) (CGO_ENABLED=$(CGO_ENABLED))..."
@mkdir -p $(BINARY_DIR)
@CGO_ENABLED=$(CGO_ENABLED) $(GOBUILD) $(LDFLAGS) -o $(BINARY_DIR)/$(BINARY_NAME) ./cmd/luxd
@echo "✅ luxd built successfully!"
@$(GO) build -trimpath -ldflags "-s -w -X main.Version=$(VERSION) -X main.GitCommit=$(GIT_COMMIT)" \
-o $(BINARY_DIR)/$(BINARY) ./cmd/dexd
@echo "Built $(BINARY_DIR)/$(BINARY)"
build-cpp:
@echo "🔨 Building with C++ optimizations..."
@mkdir -p $(BINARY_DIR)
@CGO_ENABLED=1 $(GOBUILD) $(LDFLAGS) -tags cpp -o $(BINARY_DIR)/$(BINARY_NAME)-cpp ./cmd/luxd
# Run the standalone venue.
run: build
@$(BINARY_DIR)/$(BINARY) run
build-gpu:
@echo "🔨 Building with GPU support..."
@mkdir -p $(BINARY_DIR)
@CGO_ENABLED=1 $(GOBUILD) $(LDFLAGS) -tags "cpp gpu mlx" -o $(BINARY_DIR)/$(BINARY_NAME)-gpu ./cmd/luxd
# FPGA build targets
build-fpga:
@echo "🔨 Building with FPGA support..."
@mkdir -p $(BINARY_DIR)
@CGO_ENABLED=1 $(GOBUILD) $(LDFLAGS) -tags "cgo fpga" -o $(BINARY_DIR)/$(BINARY_NAME)-fpga ./cmd/luxd
build-fpga-versal:
@echo "🔨 Building for AMD Versal FPGA..."
@mkdir -p $(BINARY_DIR)
@$(GOBUILD) $(LDFLAGS) -tags "fpga versal" -o $(BINARY_DIR)/$(BINARY_NAME)-versal ./cmd/luxd
build-fpga-f2:
@echo "🔨 Building for AWS F2 FPGA..."
@mkdir -p $(BINARY_DIR)
@$(GOBUILD) $(LDFLAGS) -tags "fpga awsf2" -o $(BINARY_DIR)/$(BINARY_NAME)-f2 ./cmd/luxd
build-tools:
@echo "🔨 Building tools..."
@$(GOBUILD) -o $(BINARY_DIR)/price-feed ./cmd/price-feed
@$(GOBUILD) -o $(BINARY_DIR)/monitor ./cmd/monitor
# Test targets
# Tests
test:
@echo "🧪 Running tests..."
@$(GOTEST) -v -timeout $(TEST_TIMEOUT) ./pkg/lx/...
@$(GOTEST) -v -timeout $(TEST_TIMEOUT) ./pkg/api/...
@echo "✅ Tests passed!"
test-all:
@./scripts/test-all.sh
@$(GO) test -timeout $(TEST_TIMEOUT) ./pkg/...
test-race:
@$(GOTEST) -race -timeout 2m ./pkg/...
@$(GO) test -race -timeout 2m ./pkg/...
# Benchmark targets
bench:
@echo "🏁 Running benchmarks..."
@$(GOTEST) -bench=. -benchmem -benchtime=$(BENCH_TIME) -run=^$$ ./pkg/lx/...
bench-all:
@./scripts/run-comprehensive-benchmark.sh
# Run targets
run:
@$(BINARY_DIR)/$(BINARY_NAME) --data-dir ~/.lxd --http-port 8080
run-cluster:
@./scripts/run-lx-cluster.sh
run-dev:
@air -c .air.toml 2>/dev/null || $(BINARY_DIR)/$(BINARY_NAME)
# Code quality
fmt:
@$(GOFMT) -s -w .
@$(GO) test -bench=. -benchmem -benchtime=10s -run=^$$ ./pkg/lx/...
# Quality
vet:
@$(GOVET) ./...
@$(GO) vet ./...
fmt:
@gofmt -s -w .
lint:
@golangci-lint run --timeout 5m 2>/dev/null || echo "Install golangci-lint for linting"
# Docker targets
docker-build:
@docker build -f docker/backend/Dockerfile -t lxdex:$(VERSION) .
coverage:
@$(GO) test -coverprofile=coverage.out -covermode=atomic ./pkg/...
@$(GO) tool cover -html=coverage.out -o coverage.html
@echo "Coverage report: coverage.html"
up:
@docker-compose -f docker/compose.yml up -d
down:
@docker-compose -f docker/compose.yml down
logs:
@docker-compose -f docker/compose.yml logs -f
# Utility targets
# Modules
deps:
@$(GOMOD) download
@$(GOMOD) tidy
@$(GO) mod download
@$(GO) mod tidy
clean:
@$(GOCLEAN)
@rm -rf $(BINARY_DIR)
@rm -f coverage.out coverage.html
@$(GO) clean
@rm -rf $(BINARY_DIR) coverage.out coverage.html
version:
@echo "Version: $(VERSION)"
@echo "Commit: $(GIT_COMMIT)"
@echo "Built: $(BUILD_TIME)"
# Canonical image (CPU dexd).
docker:
@docker build -f Dockerfile -t $(IMAGE):$(VERSION) .
# CI target
ci: clean fmt vet test build
@echo "✅ CI pipeline complete!"
# 3-node benchmark (legacy compatibility)
3node-bench:
@echo "🌐 Starting 3-node FPC network benchmark..."
@./scripts/run-3node-bench.sh
@echo "✅ 3-node benchmark complete!"
# Run demo
demo:
@./scripts/demo.sh
# Test MLX acceleration
test-mlx:
@./scripts/test-mlx.sh
# Build with MLX support
build-mlx: build-gpu
# Test CUDA acceleration
test-cuda:
@./scripts/test-cuda.sh
# Docker CUDA build
docker-cuda:
@docker build -f docker/backend/Dockerfile.cuda -t lxdex-cuda:$(VERSION) .
# Ensure 100% test passing
test-100:
@./scripts/ensure-100-pass.sh
# Run performance tuning
perf-tune:
@sudo ./scripts/perf-tune.sh
# Development setup
setup:
@./scripts/setup.sh
# Install development tools
tools:
@go install github.com/cosmtrek/air@latest
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
@go install github.com/securego/gosec/v2/cmd/gosec@latest
# Generate protobuf code
# Protobuf
proto:
@protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/*.proto
# SDK targets
# SDKs
sdk-typescript:
@cd sdk/typescript && npm install && npm run build
@@ -232,102 +106,6 @@ sdk-go:
sdk-all: sdk-typescript sdk-python sdk-go
# Deploy targets
deploy-staging:
@./scripts/deploy.sh staging deploy
deploy-production:
@./scripts/deploy.sh production deploy
rollback:
@./scripts/deploy.sh production rollback
# Monitoring
metrics:
@open http://localhost:9090
grafana:
@open http://localhost:3001
# Performance benchmarks
bench-10gbps:
@./scripts/benchmark-10gbps.sh
bench-zmq:
@./scripts/run-zmq-benchmark.sh
bench-comprehensive:
@./scripts/run-comprehensive-benchmark.sh
# Network operations
run-testnet:
@./scripts/run-lux-testnet.sh
run-local:
@./scripts/run-local-network.sh
run-fpc:
@./scripts/run-fpc-network.sh
run-qzmq:
@./scripts/run-qzmq-network.sh
# Database operations
db-migrate:
@migrate -path migrations -database "postgres://lxdex:lxdex123@localhost:5432/lxdex?sslmode=disable" up
db-rollback:
@migrate -path migrations -database "postgres://lxdex:lxdex123@localhost:5432/lxdex?sslmode=disable" down 1
db-reset:
@migrate -path migrations -database "postgres://lxdex:lxdex123@localhost:5432/lxdex?sslmode=disable" drop -f
@make db-migrate
# Complete verification
verify-all: deps fmt vet lint test-100 bench
@echo "✅ Complete verification passed!"
# Install everything for development
install: deps tools
@echo "✅ Development environment ready!"
# Security scan
security:
@gosec -fmt json -out security-report.json ./...
# Coverage report
coverage:
@go test -v -race -coverprofile=coverage.out -covermode=atomic ./pkg/...
@go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report: coverage.html"
# Production build
prod: clean verify-all build docker-build
@echo "✅ Production build complete!"
# Quick development cycle
dev: fmt test-fast build-go run-dev
# Fast tests only
test-fast:
@go test -short -timeout 30s ./pkg/...
# Integration tests
test-integration:
@go test -tags integration -timeout 5m ./test/integration/...
# E2E tests
test-e2e:
@./scripts/test-all.sh e2e
# Load testing
load-test:
@k6 run test/load/scenario.js
# Initialize project
init:
@chmod +x scripts/*.sh
@mkdir -p bin
@echo "✅ Project initialized!"
.PHONY: all build test bench clean help run docker deploy sdk db verify install init demo test-mlx test-cuda docker-cuda
# CI pipeline
ci: fmt vet test build
@echo "CI complete"
-30
View File
@@ -1,30 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command dchain is the D-Chain DEX VM plugin binary — the native consensus
// matcher VM the Lux node ("D" chain) loads from its PluginDir, exactly like
// the EVM plugin. It runs the lx.OrderBook matcher INSIDE luxd consensus
// (Block.Verify) and persists the authoritative order book to its own zapdb;
// the trade is the D-Chain state transition, sequenced by luxd's multi-validator
// engine — there is no standalone venue and no ZAP relay in the trading path.
//
// The VM (pkg/dchain) is pure-Go (CGO=0): the matcher, settlement, and block
// lifecycle carry no native dependency, so this plugin builds and ships CGO=0
// for every org+arch — the same harness as chains/*/cmd/plugin. The optional
// GPU AMM accelerator in pkg/lx is a SEPARATE concern gated by its own
// cuda/metal tags and is NOT linked by this consensus plugin.
package main
import (
"fmt"
"github.com/luxfi/dex/cmd/dchain/runner"
"github.com/luxfi/dex/pkg/dchain"
"github.com/luxfi/version"
)
func main() {
versionString := fmt.Sprintf("Lux-DChain/%s [node=%s, rpcchainvm=%d]",
dchain.Version, version.Current, version.RPCChainVMProtocol)
runner.Run(versionString)
}
-48
View File
@@ -1,48 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package runner boots the native D-Chain DEX VM as a ZAP plugin process,
// mirroring github.com/luxfi/evm/plugin/runner. rpc.Serve hands the VM the
// chain's own zapdb and serves block requests to luxd's consensus engine, so
// BuildBlock/Verify/Accept run inside luxd's multi-validator consensus. The VM
// is pure-Go (CGO=0); this entrypoint links no native code.
package runner
import (
"context"
"fmt"
"os"
"github.com/luxfi/dex/pkg/dchain"
log "github.com/luxfi/log"
"github.com/luxfi/vm/rpc"
)
// Run boots the d-chain VM over the ZAP plugin transport. rpc.Serve connects to
// the node runtime named in the runtime.EngineAddressKey env var, opens this
// chain's own badgerdb/zapdb at ChainDataDir/db, hands it to VM.Initialize as
// init.DB, and serves block requests — the exact harness lux/evm boots through.
// It blocks until the runtime shuts the VM down. versionStr, when non-empty and
// the process was invoked with the version flag, is printed and the process
// exits 0 (the node probes plugin versions this way).
func Run(versionStr string) {
if printVersion() && versionStr != "" {
fmt.Println(versionStr)
os.Exit(0)
}
if err := rpc.Serve(context.Background(), log.Root(), &dchain.VM{}); err != nil {
fmt.Printf("dchain rpc.Serve error: %s\n", err)
os.Exit(1)
}
}
// printVersion reports whether the process was asked to just print its version
// and exit. The node passes a single "version" arg when probing a plugin.
func printVersion() bool {
for _, a := range os.Args[1:] {
if a == "version" || a == "--version" || a == "-version" {
return true
}
}
return false
}
-159
View File
@@ -1,159 +0,0 @@
package main
import (
"fmt"
"time"
"github.com/luxfi/dex/pkg/lx"
)
func main() {
fmt.Println("================================================")
fmt.Println(" LX - Order Book Demo")
fmt.Println("================================================")
fmt.Println()
// Create order book
book := lx.NewOrderBook("BTC-USD")
fmt.Println("📚 Created BTC-USD order book")
fmt.Println()
// Add buy orders (bids)
fmt.Println("➕ Adding buy orders...")
book.AddOrder(&lx.Order{
ID: 1,
Type: lx.Limit,
Side: lx.Buy,
Price: 49900,
Size: 1.0,
Timestamp: time.Now(),
})
fmt.Printf(" Buy 1.00 BTC @ $49,900\n")
book.AddOrder(&lx.Order{
ID: 2,
Type: lx.Limit,
Side: lx.Buy,
Price: 49950,
Size: 0.5,
Timestamp: time.Now(),
})
fmt.Printf(" Buy 0.50 BTC @ $49,950\n")
book.AddOrder(&lx.Order{
ID: 3,
Type: lx.Limit,
Side: lx.Buy,
Price: 50000,
Size: 2.0,
Timestamp: time.Now(),
})
fmt.Printf(" Buy 2.00 BTC @ $50,000\n")
fmt.Println()
// Add sell orders (asks)
fmt.Println("➕ Adding sell orders...")
book.AddOrder(&lx.Order{
ID: 4,
Type: lx.Limit,
Side: lx.Sell,
Price: 50100,
Size: 1.5,
Timestamp: time.Now(),
})
fmt.Printf(" Sell 1.50 BTC @ $50,100\n")
book.AddOrder(&lx.Order{
ID: 5,
Type: lx.Limit,
Side: lx.Sell,
Price: 50050,
Size: 1.0,
Timestamp: time.Now(),
})
fmt.Printf(" Sell 1.00 BTC @ $50,050\n")
fmt.Println()
// Show book state
fmt.Println("📊 Order Book State:")
fmt.Println("━━━━━━━━━━━━━━━━━━━━")
// Get best prices from the book
bestBid := book.GetBestBid()
bestAsk := book.GetBestAsk()
spread := bestAsk - bestBid
fmt.Printf(" Best Bid: $%.2f\n", bestBid)
fmt.Printf(" Best Ask: $%.2f\n", bestAsk)
if bestAsk > 0 {
fmt.Printf(" Spread: $%.2f (%.3f%%)\n", spread, (spread/bestAsk)*100)
}
fmt.Println()
// Execute a market order
fmt.Println("🚀 Executing market buy order for 1.5 BTC...")
initialTradeCount := uint64(len(book.Trades))
tradesExecuted := book.AddOrder(&lx.Order{
ID: 6,
Type: lx.Market,
Side: lx.Buy,
Size: 1.5,
Timestamp: time.Now(),
})
if tradesExecuted > 0 {
fmt.Println()
fmt.Println("💰 Trades Executed:")
fmt.Println("━━━━━━━━━━━━━━━━━━")
// Get the new trades
newTrades := book.Trades[initialTradeCount:]
totalSize := 0.0
totalValue := 0.0
for i, trade := range newTrades {
fmt.Printf(" Trade %d: %.2f BTC @ $%.2f = $%.2f\n",
i+1, trade.Size, trade.Price, trade.Size*trade.Price)
totalSize += trade.Size
totalValue += trade.Size * trade.Price
}
if totalSize > 0 {
avgPrice := totalValue / totalSize
fmt.Println()
fmt.Printf(" Total: %.2f BTC\n", totalSize)
fmt.Printf(" Value: $%.2f\n", totalValue)
fmt.Printf(" Avg Price: $%.2f\n", avgPrice)
}
}
fmt.Println()
// Final book state
fmt.Println("📊 Updated Book State:")
fmt.Println("━━━━━━━━━━━━━━━━━━━━━")
// Get updated best prices
bestBid = book.GetBestBid()
bestAsk = book.GetBestAsk()
spread = bestAsk - bestBid
fmt.Printf(" Best Bid: $%.2f\n", bestBid)
fmt.Printf(" Best Ask: $%.2f\n", bestAsk)
if bestAsk > 0 {
fmt.Printf(" Spread: $%.2f\n", spread)
}
fmt.Println()
fmt.Println("📈 Order Book Summary:")
fmt.Printf(" Total Trades: %d\n", len(book.Trades))
fmt.Printf(" Active Orders: %d\n", len(book.Orders))
fmt.Println()
fmt.Println("✅ Demo complete!")
fmt.Println()
fmt.Println("The LX successfully:")
fmt.Println(" • Matched orders by price-time priority")
fmt.Println(" • Executed trades at best available prices")
fmt.Println(" • Updated order book state in real-time")
fmt.Println(" • Maintained consistent bid-ask spread")
}
-65
View File
@@ -1,65 +0,0 @@
# dex-api-server — Standalone DEX HTTP API
`dex-api-server` is a single-process daemon that serves a `pkg/lx.OrderBook`
over REST and WebSocket. It is the simplest deployment shape of the LX
matching engine and is what most local-dev workflows reach for first.
## Topology
There is exactly one operating mode: **standalone**.
| Aspect | Behavior |
|--------|----------|
| Orderbook | In-process `pkg/lx.OrderBook`. No replication. |
| Consensus | None. The daemon "mines" orders by calling `AddOrder` synchronously. |
| Persistence | None — the book is rebuilt from scratch on every start. |
| Auth | None (CORS open). Do not expose this binary to the public internet. |
Multi-node consensus deployments use a different binary,
[`cmd/dag-network`](../dag-network/main.go) (build tag `zmqtest`), which
wires `pkg/consensus.LuxDAGOrderBook` and broadcasts vertices over ZeroMQ.
That code path has its own multi-node test in `pkg/consensus/multinode_test.go`.
## Standalone start
```sh
dex-api-server -port 8080
```
Endpoints:
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/` | HTML index (debug) |
| GET | `/api/orderbook` | Snapshot of bids / asks |
| GET | `/api/trades` | Recent trades |
| GET | `/api/stats` | Market stats |
| POST | `/api/order` | Place a limit or market order |
| GET (Upgrade) | `/ws` | WebSocket for live updates |
## Why no built-in single-node consensus mode
Adding a Quasar / LuxDAG self-loop here would be redundant: the orderbook
inside `dex-api-server` already finalises orders synchronously inside
`OrderBook.AddOrder`, so a single-validator consensus loop on top would only
add latency without altering semantics. The consensus engine
(`pkg/consensus`) is meaningful when there are real peers to vote with —
hence `cmd/dag-network` for that case and `dex-api-server` for the local
dev / single-process case.
If a future use-case requires a single binary that does both, the right
refactor is to extract a `Daemon` struct with a `--peers` flag where
`len(peers) == 0` selects the in-process orderbook and `len(peers) > 0`
selects the consensus-bound one. That work is **not** in this commit —
it would require defining clean interfaces between the two orderbook
implementations and is a separate change.
## Smoke test
```sh
go test -tags=integration ./cmd/dex-api-server/... -count=1 -timeout 60s
```
`standalone_test.go` builds the daemon, starts it on a free port, places a
limit order via `/api/order`, and confirms the orderbook is queryable. It
exercises the HTTP plumbing end-to-end.
-351
View File
@@ -1,351 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
"github.com/luxfi/dex/pkg/lx"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins for testing
},
}
type Server struct {
orderBook *lx.OrderBook
trades []Trade
orderID uint64
}
type Trade struct {
ID uint64 `json:"id"`
Price float64 `json:"price"`
Size float64 `json:"size"`
Side string `json:"side"`
Timestamp time.Time `json:"timestamp"`
}
type OrderRequest struct {
Type string `json:"type"` // "limit" or "market"
Side string `json:"side"` // "buy" or "sell"
Price float64 `json:"price"`
Size float64 `json:"size"`
}
type Response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
}
func main() {
port := flag.Int("port", 8080, "Server port")
flag.Parse()
server := &Server{
orderBook: lx.NewOrderBook("BTC-USD"),
trades: make([]Trade, 0),
}
// Seed the order book with some initial orders
server.seedOrderBook()
// HTTP Routes
http.HandleFunc("/", server.handleHome)
http.HandleFunc("/api/orderbook", server.handleOrderBook)
http.HandleFunc("/api/order", server.handleOrder)
http.HandleFunc("/api/trades", server.handleTrades)
http.HandleFunc("/api/stats", server.handleStats)
http.HandleFunc("/ws", server.handleWebSocket)
addr := fmt.Sprintf(":%d", *port)
log.Printf("🚀 LX API Server starting on http://localhost%s", addr)
log.Println("📊 REST API Endpoints:")
log.Println(" GET /api/orderbook - Get order book")
log.Println(" GET /api/trades - Get recent trades")
log.Println(" GET /api/stats - Get market stats")
log.Println(" POST /api/order - Place order")
log.Println(" WS /ws - WebSocket connection")
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatal(err)
}
}
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
html := `
<!DOCTYPE html>
<html>
<head>
<title>LX API</title>
<style>
body { font-family: monospace; padding: 20px; background: #1a1a1a; color: #0f0; }
h1 { color: #0f0; }
pre { background: #000; padding: 10px; border: 1px solid #0f0; }
.endpoint { margin: 10px 0; }
.method { color: #ff0; }
</style>
</head>
<body>
<h1>🚀 LX API Server</h1>
<h2>Performance: 13M+ orders/sec</h2>
<h3>REST API Endpoints:</h3>
<div class="endpoint"><span class="method">GET</span> /api/orderbook - Get current order book</div>
<div class="endpoint"><span class="method">GET</span> /api/trades - Get recent trades</div>
<div class="endpoint"><span class="method">GET</span> /api/stats - Get market statistics</div>
<div class="endpoint"><span class="method">POST</span> /api/order - Place new order</div>
<h3>WebSocket:</h3>
<div class="endpoint"><span class="method">WS</span> /ws - Real-time market data</div>
<h3>Example curl commands:</h3>
<pre>
# Get order book
curl http://localhost:8080/api/orderbook
# Place a buy order
curl -X POST http://localhost:8080/api/order \
-H "Content-Type: application/json" \
-d '{"type":"limit","side":"buy","price":50000,"size":0.1}'
# Get recent trades
curl http://localhost:8080/api/trades
# Get market stats
curl http://localhost:8080/api/stats
</pre>
</body>
</html>
`
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, html)
}
func (s *Server) handleOrderBook(w http.ResponseWriter, r *http.Request) {
snapshot := s.orderBook.GetOrderBookSnapshot()
bids := snapshot.Bids
asks := snapshot.Asks
data := map[string]interface{}{
"symbol": "BTC-USD",
"bids": bids,
"asks": asks,
"timestamp": time.Now(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{
Success: true,
Data: data,
})
}
func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req OrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{
Success: false,
Message: "Invalid request format",
})
return
}
// Create order
s.orderID++
order := &lx.Order{
ID: s.orderID,
Type: lx.Limit,
Side: lx.Buy,
Price: req.Price,
Size: req.Size,
User: "api-user",
Timestamp: time.Now(),
}
if req.Type == "market" {
order.Type = lx.Market
}
if req.Side == "sell" {
order.Side = lx.Sell
}
// Add to order book
orderID := s.orderBook.AddOrder(order)
if orderID == 0 {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{
Success: false,
Message: "Failed to add order",
})
return
}
// Simulate trade execution
if req.Type == "market" || (s.orderID%3 == 0) {
s.trades = append(s.trades, Trade{
ID: uint64(len(s.trades) + 1),
Price: req.Price,
Size: req.Size,
Side: req.Side,
Timestamp: time.Now(),
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{
Success: true,
Message: "Order placed successfully",
Data: map[string]interface{}{
"order_id": s.orderID,
"status": "accepted",
},
})
}
func (s *Server) handleTrades(w http.ResponseWriter, r *http.Request) {
// Return last 20 trades
start := 0
if len(s.trades) > 20 {
start = len(s.trades) - 20
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{
Success: true,
Data: s.trades[start:],
})
}
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
snapshot := s.orderBook.GetOrderBookSnapshot()
var bestBidPrice, bestAskPrice float64
if len(snapshot.Bids) > 0 {
bestBidPrice = snapshot.Bids[0].Price
}
if len(snapshot.Asks) > 0 {
bestAskPrice = snapshot.Asks[0].Price
}
stats := map[string]interface{}{
"symbol": "BTC-USD",
"best_bid": bestBidPrice,
"best_ask": bestAskPrice,
"total_orders": s.orderID,
"total_trades": len(s.trades),
"last_trade": nil,
"engine_status": "operational",
"performance": "13M+ orders/sec",
}
if len(s.trades) > 0 {
stats["last_trade"] = s.trades[len(s.trades)-1]
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Response{
Success: true,
Data: stats,
})
}
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("WebSocket upgrade failed:", err)
return
}
defer conn.Close()
log.Println("WebSocket client connected")
// Send initial orderbook
snapshot := s.orderBook.GetOrderBookSnapshot()
conn.WriteJSON(map[string]interface{}{
"type": "snapshot",
"data": map[string]interface{}{
"bids": snapshot.Bids,
"asks": snapshot.Asks,
},
})
// Send updates every second
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Send market update
snapshot := s.orderBook.GetOrderBookSnapshot()
var bestBidPrice, bestAskPrice float64
if len(snapshot.Bids) > 0 {
bestBidPrice = snapshot.Bids[0].Price
}
if len(snapshot.Asks) > 0 {
bestAskPrice = snapshot.Asks[0].Price
}
update := map[string]interface{}{
"type": "update",
"data": map[string]interface{}{
"best_bid": bestBidPrice,
"best_ask": bestAskPrice,
"timestamp": time.Now(),
},
}
if err := conn.WriteJSON(update); err != nil {
log.Println("WebSocket write error:", err)
return
}
}
}
}
func (s *Server) seedOrderBook() {
// Add some initial buy orders
buyPrices := []float64{49900, 49800, 49700, 49600, 49500}
for i, price := range buyPrices {
s.orderID++
s.orderBook.AddOrder(&lx.Order{
ID: s.orderID,
Type: lx.Limit,
Side: lx.Buy,
Price: price,
Size: 0.1 * float64(i+1),
User: "seed",
Timestamp: time.Now(),
})
}
// Add some initial sell orders
sellPrices := []float64{50100, 50200, 50300, 50400, 50500}
for i, price := range sellPrices {
s.orderID++
s.orderBook.AddOrder(&lx.Order{
ID: s.orderID,
Type: lx.Limit,
Side: lx.Sell,
Price: price,
Size: 0.1 * float64(i+1),
User: "seed",
Timestamp: time.Now(),
})
}
log.Printf("📊 Seeded order book with %d initial orders", s.orderID)
}
-149
View File
@@ -1,149 +0,0 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build integration
// Package main, file standalone_test.go.
//
// Exercises dex-api-server in standalone mode. dex-api-server is the
// in-process orderbook daemon — it owns a local pkg/lx.OrderBook and
// services REST + WebSocket from it. There is no consensus binding,
// no Lux mainnet handshake, no peer discovery. Standalone is the only
// mode this daemon supports.
//
// Smoke flow:
// 1. build dex-api-server
// 2. spawn it with a free port
// 3. POST /api/order to place a limit-buy order
// 4. GET /api/orderbook and confirm the order appears on the bid side
// 5. SIGTERM and confirm graceful exit
//
// Multi-node consensus is provided by an entirely different binary
// (cmd/dag-network, build-tagged zmqtest) that wires pkg/consensus
// directly. Wiring Quasar / LuxDAGOrderBook into dex-api-server would
// be a non-trivial refactor — see the README for the architectural
// rationale.
//
// Run: `go test -tags=integration ./cmd/dex-api-server/... -count=1 -timeout 60s`.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"syscall"
"testing"
"time"
)
func freePort(t *testing.T) int {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port
}
func waitReady(t *testing.T, addr string) {
t.Helper()
deadline := time.Now().Add(30 * time.Second)
url := "http://" + addr + "/api/orderbook"
for time.Now().Before(deadline) {
resp, err := http.Get(url)
if err == nil {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode == 200 {
return
}
}
time.Sleep(200 * time.Millisecond)
}
t.Fatalf("dex-api-server not ready on %s", addr)
}
func TestStandalonePlaceOrderRoundTrip(t *testing.T) {
tmpDir := t.TempDir()
binPath := filepath.Join(tmpDir, "dex-api-server")
build := exec.Command("go", "build", "-o", binPath, ".")
build.Dir = "."
build.Stdout, build.Stderr = os.Stdout, os.Stderr
if err := build.Run(); err != nil {
t.Fatalf("go build dex-api-server: %v", err)
}
port := freePort(t)
addr := fmt.Sprintf("127.0.0.1:%d", port)
cmd := exec.Command(binPath, "-port", fmt.Sprint(port))
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Start(); err != nil {
t.Fatalf("start: %v", err)
}
t.Cleanup(func() {
_ = cmd.Process.Signal(syscall.SIGTERM)
done := make(chan struct{})
go func() { _ = cmd.Wait(); close(done) }()
select {
case <-done:
case <-time.After(5 * time.Second):
_ = cmd.Process.Kill()
<-done
}
})
waitReady(t, addr)
// Place a buy order at a price well above existing bids so it sits
// on the book without immediately matching against the seeded asks.
order, _ := json.Marshal(OrderRequest{
Type: "limit",
Side: "buy",
Price: 99000.0,
Size: 0.5,
})
resp, err := http.Post("http://"+addr+"/api/order", "application/json", bytes.NewReader(order))
if err != nil {
t.Fatalf("place order: %v", err)
}
defer resp.Body.Close()
var placed Response
if err := json.NewDecoder(resp.Body).Decode(&placed); err != nil {
t.Fatalf("decode place response: %v", err)
}
if !placed.Success {
t.Fatalf("place order failed: %s", placed.Message)
}
// The seeded book contains buy orders down to the 99000 area; our
// new order may have matched. The contract under test is simply
// that the daemon accepted the order and the book is still queryable.
resp2, err := http.Get("http://" + addr + "/api/orderbook")
if err != nil {
t.Fatalf("get orderbook: %v", err)
}
defer resp2.Body.Close()
var book Response
if err := json.NewDecoder(resp2.Body).Decode(&book); err != nil {
t.Fatalf("decode orderbook: %v", err)
}
if !book.Success {
t.Fatalf("orderbook query failed: %s", book.Message)
}
data, ok := book.Data.(map[string]interface{})
if !ok {
t.Fatalf("unexpected orderbook payload: %T", book.Data)
}
if _, ok := data["bids"]; !ok {
t.Fatalf("orderbook missing bids field: %v", data)
}
}
-111
View File
@@ -1,111 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net"
"os"
"sync/atomic"
"time"
"github.com/nats-io/nats.go"
)
type ServerAnnouncement struct {
ID string `json:"id"`
Addr string `json:"addr"`
Type string `json:"type"`
Started time.Time `json:"started"`
}
type DexServer struct {
nc *nats.Conn
id string
addr string
orders int64
trades int64
}
func main() {
port := flag.Int("port", 8080, "Server port")
natsURL := flag.String("nats", nats.DefaultURL, "NATS URL")
flag.Parse()
hostname, _ := os.Hostname()
id := fmt.Sprintf("%s-%d", hostname, os.Getpid())
addr := fmt.Sprintf("%s:%d", getLocalIP(), *port)
log.Printf("🏦 DEX Server starting")
log.Printf("📍 ID: %s", id)
log.Printf("🌐 Address: %s", addr)
// Connect to NATS
nc, err := nats.Connect(*natsURL)
if err != nil {
log.Fatalf("Failed to connect to NATS: %v", err)
}
defer nc.Close()
server := &DexServer{
nc: nc,
id: id,
addr: addr,
}
// Announce ourselves
go server.announcer()
// Subscribe to orders
nc.QueueSubscribe("dex.orders", "dex-servers", func(m *nats.Msg) {
atomic.AddInt64(&server.orders, 1)
if atomic.LoadInt64(&server.orders)%2 == 0 {
atomic.AddInt64(&server.trades, 1)
}
m.Respond([]byte(`{"status":"accepted"}`))
})
// Stats printer
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
orders := atomic.LoadInt64(&server.orders)
trades := atomic.LoadInt64(&server.trades)
log.Printf("📊 Stats: Orders=%d, Trades=%d", orders, trades)
}
}()
log.Println("✅ DEX Server ready!")
select {}
}
func (s *DexServer) announcer() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
announcement := ServerAnnouncement{
ID: s.id,
Addr: s.addr,
Type: "dex-server",
Started: time.Now(),
}
for range ticker.C {
data, _ := json.Marshal(announcement)
s.nc.Publish("dex.announce", data)
}
}
func getLocalIP() string {
addrs, _ := net.InterfaceAddrs()
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return "127.0.0.1"
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command dexd is the ONE D-Chain DEX binary. It runs the DEX two ways:
//
// dexd run as a luxd VM plugin (rpcchainvm). luxd execs the plugin
// dexd plugin from its PluginDir with NO args and drives
// BuildBlock/Verify/Accept inside multi-validator consensus.
// NO-args == plugin is REQUIRED, not a convenience: it is
// exactly how luxd launches a plugin (the engine address
// arrives via the runtime.EngineAddressKey env var, never argv),
// and `dexd version` is the node's version probe.
//
// dexd run run the standalone D-Chain venue: open the on-disk zapdb,
// dexd standalone serve the frozen dex_* surface over ZAP, and drive the
// consensus sealer loop itself (WaitForEvent -> BuildBlock ->
// Verify -> Accept).
//
// The matcher, VM, and engine all live in pkg/dchain + pkg/lx; this binary only
// selects a run mode and wires it — it duplicates no engine code. CGO_ENABLED
// picks the matcher in pkg/lx (GPU under cgo, pure-Go CPU without); the default
// build is CPU (CGO=0) and links no native code.
package main
import (
"os"
"github.com/luxfi/dex/internal/plugin"
"github.com/luxfi/dex/internal/standalone"
)
func main() {
// The first arg selects the run mode. Anything that is NOT an explicit
// standalone/plugin subcommand falls through to the plugin entrypoint —
// this preserves the luxd plugin contract byte-for-byte: luxd execs the
// binary with NO args (serve the handshake) or with "version" (probe the
// version), and both must reach plugin.Run.
if len(os.Args) > 1 {
switch os.Args[1] {
case "run", "standalone":
standalone.Run(os.Args[2:])
return
case "plugin":
plugin.Run()
return
}
}
plugin.Run()
}
-267
View File
@@ -1,267 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command dexfillproof is a one-shot, REAL native-CLOB fill proof for the live
// in-luxd D-Chain DEX. It proves the consensus liveness fix end-to-end: a CLOB
// write reaches the mempool, the D-Chain matches at Verify and FINALIZES the
// block (height advances), and a maker ask crossed by a taker bid produces a
// real fill with CONSERVED balances.
//
// It needs NO EVM key and NO deployed ERC-20: every asset is a canonical
// dex.DeriveAssetID over a REAL on-chain reference (EVM_NATIVE LUX on the
// C-Chain + the real UTXO LUX assetID on the X-Chain). Neither is an
// ascii-of-symbol id, so there is zero synthetic poison — the same structural
// safety as cmd/dexseed.
//
// Sequence (each write blocks until its block is accepted):
//
// 1. dex_open_market(pid, baseID, quoteID) bind the REAL (base,quote)
// 2. dex_deposit(maker, baseID, qty) fund maker base (to sell)
// 3. dex_deposit(taker, quoteID, notional) fund taker quote (to buy)
// 4. dex_place(pid, SELL, price, qty, maker) maker rests a limit ask
// 5. dex_submit(pid, BUY, IOC@price, qty, taker) taker crosses -> FILL
// 6. dex_get_markets / dex_get_book read back, assert the fill
// 7. assert height advanced 0 -> >0 (FINALIZATION proof)
package main
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/luxfi/dex/pkg/dex"
"github.com/luxfi/dex/pkg/zapwire"
"github.com/luxfi/ids"
)
// poolIDDomain + poolIDFor mirror cmd/dexseed/seed.go EXACTLY so the poolID this
// proof opens is byte-identical to the canonical seeder's (and to what the
// indexer keys on). NON-ascii, domain-separated, length-prefixed.
var poolIDDomain = []byte("lux:dex:dexseed:poolid:v1")
func writeLP(h interface{ Write([]byte) (int, error) }, b []byte) {
var lp [8]byte
binary.BigEndian.PutUint64(lp[:], uint64(len(b)))
_, _ = h.Write(lp[:])
_, _ = h.Write(b)
}
func poolIDFor(baseID, quoteID ids.ID) ids.ID {
h := sha256.New()
writeLP(h, poolIDDomain)
writeLP(h, baseID[:])
writeLP(h, quoteID[:])
var p [32]byte
copy(p[:], h.Sum(nil))
return ids.ID(p)
}
var httpClient = &http.Client{Timeout: 45 * time.Second}
type chainHead struct {
Height uint64 `json:"height"`
LastAccepted string `json:"lastAccepted"`
Root string `json:"root"`
}
type marketJSON struct {
PoolID string `json:"poolId"`
ID string `json:"id"`
Symbol string `json:"symbol"`
AssetsBound bool `json:"assetsBound"`
Orders int `json:"orders"`
OpenOrders int `json:"openOrders"`
Remaining float64 `json:"remaining"`
BestBid float64 `json:"bestBid"`
BestAsk float64 `json:"bestAsk"`
}
type getMarketsResp struct {
chainHead
Count int `json:"count"`
Markets []marketJSON `json:"markets"`
}
func die(f string, a ...any) { fmt.Fprintf(os.Stderr, "FAIL: "+f+"\n", a...); os.Exit(1) }
func ok(f string, a ...any) { fmt.Printf("OK "+f+"\n", a...) }
// postFrame POSTs a frozen zapwire frame (octet-stream); the write blocks in the
// VM until the block is accepted. Returns the raw ack/fills frame.
func postFrame(base, method string, payload []byte) ([]byte, error) {
req, err := http.NewRequest(http.MethodPost, base+"/ext/bc/D/dex/"+method, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s -> HTTP %d: %s", method, resp.StatusCode, string(body))
}
return body, nil
}
// getJSON GETs a read route (reads are GET; writes are POST octet-stream).
func getJSON(base, method string) ([]byte, error) {
resp, err := httpClient.Get(base + "/ext/bc/D/dex/" + method)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s -> HTTP %d: %s", method, resp.StatusCode, string(body))
}
return body, nil
}
func getMarkets(base string) getMarketsResp {
b, err := getJSON(base, "dex_get_markets")
if err != nil {
die("dex_get_markets: %v", err)
}
var r getMarketsResp
if err := json.Unmarshal(b, &r); err != nil {
die("dex_get_markets decode: %v (body=%s)", err, string(b))
}
return r
}
func mustDerive(networkID uint32, chain ids.ID, kind dex.AssetKind, ref []byte, what string) ids.ID {
id, err := dex.DeriveAssetID(networkID, chain, kind, ref)
if err != nil {
die("derive %s: %v", what, err)
}
return id
}
func main() {
var (
node = flag.String("node", "http://127.0.0.1:9650", "validator base URL")
networkID = flag.Uint("network-id", 3, "primary network id")
cChainStr = flag.String("cchain-id", "", "C-Chain blockchain id (EVM_NATIVE source) REQUIRED")
xChainStr = flag.String("xchain-id", "", "X-Chain blockchain id (UTXO source) REQUIRED")
luxAssetID = flag.String("lux-asset-id", "", "real X-Chain LUX assetID (UTXO ref) REQUIRED")
maker = flag.String("maker", "fillproof-maker", "maker handle")
taker = flag.String("taker", "fillproof-taker", "taker handle")
price = flag.Float64("price", 2.0, "ask price (quote per base)")
qty = flag.Float64("qty", 10.0, "base quantity")
)
flag.Parse()
if *cChainStr == "" || *xChainStr == "" || *luxAssetID == "" {
die("required: -cchain-id -xchain-id -lux-asset-id")
}
cChain, err := ids.FromString(*cChainStr)
if err != nil {
die("cchain-id: %v", err)
}
xChain, err := ids.FromString(*xChainStr)
if err != nil {
die("xchain-id: %v", err)
}
luxAsset, err := ids.FromString(*luxAssetID)
if err != nil {
die("lux-asset-id: %v", err)
}
// REAL assets, canonical derivation, zero synthetic content.
baseID := mustDerive(uint32(*networkID), cChain, dex.AssetKindEVMNative, dex.EVMNativeMarker, "base EVM_NATIVE LUX")
quoteID := mustDerive(uint32(*networkID), xChain, dex.AssetKindUTXO, luxAsset[:], "quote UTXO LUX")
if baseID == quoteID {
die("base==quote (not a market)")
}
pid := poolIDFor(baseID, quoteID)
fmt.Printf("base(EVM_NATIVE LUX)=%s\nquote(UTXO LUX)=%s\npoolID=%s\n\n", baseID, quoteID, pid)
before := getMarkets(*node)
fmt.Printf("D-Chain BEFORE: height=%d lastAccepted=%s markets=%d\n\n", before.Height, before.LastAccepted, before.Count)
// 1. open market (lightest write — proves FINALIZATION on its own).
if _, err := postFrame(*node, "dex_open_market", zapwire.EncodeOpenMarket([32]byte(pid), [32]byte(baseID), [32]byte(quoteID))); err != nil {
die("dex_open_market: %v", err)
}
afterOpen := getMarkets(*node)
ok("dex_open_market accepted: height %d -> %d", before.Height, afterOpen.Height)
if afterOpen.Height <= before.Height {
die("D-Chain did NOT finalize (height did not advance %d -> %d) — consensus still wedged", before.Height, afterOpen.Height)
}
ok("FINALIZATION PROVEN: D-Chain height advanced past 0")
// 2-3. fund both sides (creditDeposit credits the handle; needs non-zero ref).
depRef := func(tag string) [32]byte { var r [32]byte; copy(r[:], []byte(tag)); r[31] = 0x01; return r }
makerQty := uint64(*qty * 1e9) // maker sells qty base
takerNotional := uint64(*price**qty*1e9 + 1e9) // taker holds enough quote to buy
if _, err := postFrame(*node, "dex_deposit", zapwire.EncodeDeposit(*maker, [32]byte(baseID), makerQty, depRef("maker-base"))); err != nil {
die("dex_deposit maker base: %v", err)
}
ok("dex_deposit maker base accepted (%.4f base)", *qty)
if _, err := postFrame(*node, "dex_deposit", zapwire.EncodeDeposit(*taker, [32]byte(quoteID), takerNotional, depRef("taker-quote"))); err != nil {
die("dex_deposit taker quote: %v", err)
}
ok("dex_deposit taker quote accepted (%.4f quote)", float64(takerNotional)/1e9)
// 4. maker rests a limit ask.
if _, err := postFrame(*node, "dex_place", zapwire.EncodePlace([32]byte(pid), zapwire.SideSell, *price, *qty, *maker)); err != nil {
die("dex_place maker ask: %v", err)
}
ok("dex_place maker SELL %.4f @ %.4f accepted", *qty, *price)
// 5. taker crosses with an IOC buy bounded at the ask price -> FILL.
fillsFrame, err := postFrame(*node, "dex_submit", zapwire.EncodeSubmit([32]byte(pid), zapwire.SideBuy, false, *price, *qty, *taker))
if err != nil {
die("dex_submit taker buy: %v", err)
}
fills, derr := zapwire.DecodeFills(fillsFrame)
if derr != nil {
die("decode fills: %v (frame=%x)", derr, fillsFrame)
}
if len(fills) == 0 {
die("taker submit produced NO fills — no cross (book=%+v)", getMarkets(*node))
}
var filled float64
for _, f := range fills {
filled += f.Size
ok("FILL: price=%.6f size=%.6f", f.Price, f.Size)
}
// 6-7. read back: market shows the trade, height advanced again.
after := getMarkets(*node)
fmt.Printf("\nD-Chain AFTER: height=%d lastAccepted=%s markets=%d\n", after.Height, after.LastAccepted, after.Count)
// The read surface (pkg/dchain/read.go) encodes poolId as lowercase hex of the
// 32-byte id; ids.ID.String() is cb58. Match on hex, the canonical wire form.
pidHex := hex.EncodeToString(pid[:])
var mkt *marketJSON
for i := range after.Markets {
if after.Markets[i].PoolID == pidHex || after.Markets[i].ID == pidHex {
mkt = &after.Markets[i]
}
}
if mkt == nil {
die("seeded market not present in dex_get_markets (poolId=%s)", pidHex)
}
ok("market present: assetsBound=%v bestBid=%.6f bestAsk=%.6f remaining=%.6f", mkt.AssetsBound, mkt.BestBid, mkt.BestAsk, mkt.Remaining)
// Conservation: the taker filled exactly what the maker offered; the ask fully
// consumed (remaining 0 on that price level) means base moved maker->taker and
// quote moved taker->maker for the same notional. The matcher enforces value
// conservation per fill at Verify; here we assert the externally-observable
// invariant: filled size == placed size, market crossed and cleared.
if filled+1e-9 < *qty {
die("CONSERVATION/partial: filled %.6f < placed %.6f", filled, *qty)
}
ok("CONSERVATION OK: taker filled %.6f == maker placed %.6f; notional %.6f quote", filled, *qty, filled**price)
fmt.Printf("\n=== NATIVE FILL PROOF PASS ===\n")
fmt.Printf("poolID=%s\nbase=EVM_NATIVE LUX (%s)\nquote=UTXO LUX (%s)\nfill=%.6f base @ %.6f -> finalized height %d\n",
pid, baseID, quoteID, filled, *price, after.Height)
}
-338
View File
@@ -1,338 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command dexseed is the CANONICAL real-market seeder for the live native D-Chain
// DEX. It seeds REAL markets onto the in-luxd CLOB ingestion seam (POST
// /ext/bc/D/dex/dex_* with FROZEN zapwire frames) using REAL, consensus-native asset
// identities — dex.DeriveAssetID(networkID, C-chainID, kind, canonicalRef) — for
// every side of every market.
//
// SAFE AGAINST A SHARED CLUSTER BY CONSTRUCTION (no guard, no loopback fence): dexseed
// NEVER builds an ascii-of-symbol id. Every asset it opens is admitted ONLY if its
// (kind, ref) derives a well-formed canonical AssetID via dex.DeriveAssetID — the
// SAME identity the chain's resolver computes — and the chain's own permissionless
// resolver additionally proves each side is backed by live on-chain code (the
// EXTCODESIZE reality gate) before binding the market. A synthetic/malformed ref is
// refused HERE, at frame-build time, before a single frame is sent. There is no
// synthetic seeder to fence off any more: the footgun (ascii-of-symbol markets with no
// settlement identity) was removed, not guarded — dexseed's safety is structural.
//
// END-TO-END SMOKE PATH (one real market):
//
// 1. dex_open_market(poolID, baseID, quoteID) bind the REAL (base,quote)
// 2. dex_deposit(maker, baseID, amount, ref) fund the maker's base balance
// 3. dex_place(poolID, SELL, askPrice, askSize, maker) rest a maker limit ask
// 4. dex_get_markets / dex_get_book?market=<poolID> read back & confirm
//
// EXAMPLE (devnet, one real LUX/<ERC20> market against a single validator):
//
// dexseed \
// -node http://localhost:9650 \
// -network-id 3 \
// -cchain-id Bm8X7THQtS2txLrQWTDXN8a4JDuhP4KGybUE754LLSiVFLQ7v \
// -quote-erc20 0x<20-byte-token-address> \
// -maker maker
//
// The base is EVM_NATIVE LUX (the EVM zero-address native marker). The quote is the
// real ERC-20 at -quote-erc20 (it MUST be a token actually deployed on the C-Chain;
// the chain's reality gate rejects a non-deployed address and the open is refused with
// HTTP 502). Omitting -quote-erc20 prints what to pass and exits — there is no
// synthetic fallback by design.
package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/luxfi/dex/pkg/zapwire"
"github.com/luxfi/ids"
)
// Read endpoint sub-path keys. These mirror pkg/dchain.MethodGetMarkets / MethodGetBook
// — the external HTTP routes the native VM registers under /ext/bc/D/dex/. They are
// declared here (not imported) so the seeder reads the JSON schema exactly as an
// external consumer (the indexer) would, never coupling to the VM package.
const (
methodGetMarkets = "dex_get_markets"
methodGetBook = "dex_get_book"
)
// httpClient is a short-timeout client. A write blocks in the VM's submitTx until the
// order's block is accepted, so the timeout must comfortably exceed one consensus
// round (30s).
var httpClient = &http.Client{Timeout: 30 * time.Second}
// chainHead mirrors pkg/dchain.chainHead — the accepted-block anchor every read body
// carries.
type chainHead struct {
Height uint64 `json:"height"`
LastAccepted string `json:"lastAccepted"`
Root string `json:"root"`
}
// marketJSON mirrors pkg/dchain.marketJSON (one row of dex_get_markets).
type marketJSON struct {
PoolID string `json:"poolId"`
Symbol string `json:"symbol"`
AssetsBound bool `json:"assetsBound"`
Orders int `json:"orders"`
Remaining float64 `json:"remaining"`
BestBid float64 `json:"bestBid"`
BestAsk float64 `json:"bestAsk"`
}
// getMarketsResp mirrors pkg/dchain.getMarketsResp (dex_get_markets body).
type getMarketsResp struct {
chainHead
Count int `json:"count"`
Markets []marketJSON `json:"markets"`
}
// levelJSON mirrors pkg/dchain.levelJSON (one aggregated book level).
type levelJSON struct {
Price float64 `json:"price"`
Size float64 `json:"size"`
Count int `json:"count"`
}
// getBookResp mirrors pkg/dchain.getBookResp (dex_get_book body).
type getBookResp struct {
chainHead
Market string `json:"market"`
Orders int `json:"orders"`
Remaining float64 `json:"remaining"`
BestBid float64 `json:"bestBid"`
BestAsk float64 `json:"bestAsk"`
Bids []levelJSON `json:"bids"`
Asks []levelJSON `json:"asks"`
}
func die(format string, args ...any) {
fmt.Fprintf(os.Stderr, "FAIL: "+format+"\n", args...)
os.Exit(1)
}
// postFrame POSTs a FROZEN zapwire frame to <base>/ext/bc/D/dex/<method> as
// application/octet-stream and returns the raw response bytes (the zapwire ack / fills
// / balance frame). base is http://host:port (no trailing slash), pointed at the
// in-luxd CLOB ingestion seam.
func postFrame(base, method string, payload []byte) ([]byte, error) {
url := base + "/ext/bc/D/dex/" + method
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: HTTP %d: %s", method, resp.StatusCode, strings.TrimSpace(string(body)))
}
return body, nil
}
// getJSON GETs <base>/ext/bc/D/dex/<method>[?query] and decodes the JSON body.
func getJSON(base, method, query string, out any) error {
url := base + "/ext/bc/D/dex/" + method
if query != "" {
url += "?" + query
}
resp, err := httpClient.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%s: HTTP %d: %s", method, resp.StatusCode, strings.TrimSpace(string(body)))
}
return json.Unmarshal(body, out)
}
// parseERC20Addr parses a 0x-prefixed (or bare) 20-byte hex token address. A
// non-20-byte or non-hex value is an error — there is NO synthetic fallback.
func parseERC20Addr(s string) ([]byte, error) {
s = strings.TrimSpace(s)
if s == "" {
return nil, fmt.Errorf("empty token address")
}
if len(s) >= 2 && (s[0:2] == "0x" || s[0:2] == "0X") {
s = s[2:]
}
b, err := hex.DecodeString(s)
if err != nil {
return nil, fmt.Errorf("not hex: %w", err)
}
if len(b) != 20 {
return nil, fmt.Errorf("ERC-20 address must be 20 bytes, got %d", len(b))
}
return b, nil
}
func main() {
var (
nodeURL = flag.String("node", "", "single validator base URL, e.g. http://localhost:9650 (REQUIRED)")
networkID = flag.Uint("network-id", 3, "Lux network id the assets live under (3=devnet; mainnet=1, testnet=2)")
cChainID = flag.String("cchain-id", "", "C-Chain blockchain id (the sourceChainID for EVM_NATIVE/ERC20 asset derivation) (REQUIRED)")
quoteERC20 = flag.String("quote-erc20", "", "REAL ERC-20 token contract address (0x...20 bytes) to use as the market quote; the base is EVM_NATIVE LUX")
maker = flag.String("maker", "maker", "maker CLOB user handle (the 16-byte on-wire user, NOT an EVM key)")
askPrice = flag.Float64("ask-price", 1.0, "resting maker ask price (quote per base)")
askSize = flag.Float64("ask-size", 1.0, "resting maker ask size (base whole units)")
settle = flag.Duration("settle", 2*time.Second, "wait before reading back so the order's block is accepted")
)
flag.Parse()
node := strings.TrimRight(strings.TrimSpace(*nodeURL), "/")
if node == "" {
die("-node is required (single validator base URL, e.g. http://localhost:9650)")
}
if *cChainID == "" {
die("-cchain-id is required (the C-Chain blockchain id; asset ids are derived under it)")
}
cid, err := ids.FromString(*cChainID)
if err != nil {
die("-cchain-id %q: %v", *cChainID, err)
}
if *networkID == 0 || *networkID > 0xffffffff {
die("-network-id %d out of range for uint32", *networkID)
}
netID := uint32(*networkID)
if *quoteERC20 == "" {
die("-quote-erc20 is required: a REAL ERC-20 token address (0x...) to quote LUX against.\n" +
" dexseed NEVER fabricates a synthetic quote (that bricks the chain). Pass a token\n" +
" actually deployed on the C-Chain, e.g. -quote-erc20 0x<20-byte-token-address>.")
}
quoteAddr, err := parseERC20Addr(*quoteERC20)
if err != nil {
die("-quote-erc20 %q: %v", *quoteERC20, err)
}
base := nativeSpec() // EVM_NATIVE LUX (the chain's own coin)
quote := erc20Spec(quoteAddr) // REAL ERC-20 quote
// Build the frozen frames + validated plan. This step REFUSES any synthetic /
// malformed asset ref (the only ones it would accept derive a well-formed canonical
// AssetID), so we never send a poison frame.
depositAmount := depositAmountFor(*askSize)
// The deposit ref is derived after we know the poolID/baseID, so derive a provisional
// plan first to learn the ids, then thread the real ref. realMarketFrames is pure and
// cheap, so we call it twice: once to learn baseID, once with the bound ref.
_, _, _, plan0, err := realMarketFrames(netID, cid, base, quote, *maker, *askPrice, *askSize, depositAmount, [32]byte{})
if err != nil {
die("derive real market (asset admission): %v", err)
}
depRef := depositRefFor(plan0.PoolID, plan0.BaseID, depositAmount)
openMarket, deposit, place, plan, err := realMarketFrames(netID, cid, base, quote, *maker, *askPrice, *askSize, depositAmount, depRef)
if err != nil {
die("derive real market (asset admission): %v", err)
}
fmt.Printf("=== dexseed: REAL-market seeder ===\n")
fmt.Printf(" node : %s\n", node)
fmt.Printf(" network-id : %d\n", netID)
fmt.Printf(" C-Chain id : %s\n", cid)
fmt.Printf(" base (LUX) : kind=%s id=%s\n", base.Kind, plan.BaseID)
fmt.Printf(" quote(ERC20): kind=%s addr=0x%s id=%s\n", quote.Kind, hex.EncodeToString(quoteAddr), plan.QuoteID)
fmt.Printf(" poolID : 0x%s (canonical, NON-ascii)\n", hex.EncodeToString(plan.PoolID[:]))
fmt.Printf(" maker : %q ask %.6f x %.6f deposit base=%d\n", *maker, *askPrice, *askSize, depositAmount)
mustPost := func(step, method string, payload []byte) []byte {
b, err := postFrame(node, method, payload)
if err != nil {
die("%s (%s): %v", step, method, err)
}
return b
}
// ---- 1/3 open the REAL market (bind base+quote canonical ids) ----
mustPost("[1/3] open-market", zapwire.MethodOpenMarket, openMarket)
fmt.Printf("[1/3] dex_open_market: bound base+quote canonical ids (real-asset market)\n")
// ---- 2/3 fund the maker's base balance ----
depResp := mustPost("[2/3] deposit", zapwire.MethodDeposit, deposit)
depStatus, credited, derr := zapwire.DecodeBalanceResp(depResp)
if derr != nil {
die("[2/3] deposit: decode balance resp: %v (resp=%x)", derr, depResp)
}
fmt.Printf("[2/3] dex_deposit: maker base credited=%d (status=%d, ref=0x%s…)\n",
credited, depStatus, hex.EncodeToString(depRef[:4]))
// ---- 3/3 rest the maker limit ask ----
askResp := mustPost("[3/3] place", zapwire.MethodPlace, place)
orderID, ackStatus, aerr := zapwire.DecodeAck(askResp)
if aerr != nil {
die("[3/3] place: decode ack: %v (resp=%x)", aerr, askResp)
}
if ackStatus != zapwire.StatusPlaced {
die("[3/3] place: ask NOT placed (status=%d, resp=%x) — the maker base deposit may be insufficient to lock the ask",
ackStatus, askResp)
}
fmt.Printf("[3/3] dex_place: maker ask RESTING (orderId=%d, status=placed)\n", orderID)
// ---- read back: confirm the market + resting order landed ----
if *settle > 0 {
time.Sleep(*settle)
}
fmt.Printf("\n=== read-back: dex_get_markets + dex_get_book ===\n")
var mkts getMarketsResp
if err := getJSON(node, methodGetMarkets, "", &mkts); err != nil {
die("read clob_get_markets: %v", err)
}
poolHex := hex.EncodeToString(plan.PoolID[:])
var seen *marketJSON
for i := range mkts.Markets {
if strings.EqualFold(strings.TrimPrefix(mkts.Markets[i].PoolID, "0x"), poolHex) {
seen = &mkts.Markets[i]
break
}
}
if seen == nil {
die("dex_get_markets (height=%d, count=%d) does NOT list our poolID 0x%s — market did not commit",
mkts.Height, mkts.Count, poolHex)
}
fmt.Printf(" market: poolId=0x%s assetsBound=%v orders=%d bestAsk=%.6f (head height=%d)\n",
poolHex, seen.AssetsBound, seen.Orders, seen.BestAsk, mkts.Height)
if !seen.AssetsBound {
die("market is listed but assetsBound=false — the (base,quote) binding did not persist")
}
var book getBookResp
if err := getJSON(node, methodGetBook, "market=0x"+poolHex, &book); err != nil {
die("read clob_get_book: %v", err)
}
if book.Orders < 1 || len(book.Asks) < 1 {
die("dex_get_book shows NO resting ask (orders=%d, asks=%d) — the resting order did not land",
book.Orders, len(book.Asks))
}
fmt.Printf(" book : orders=%d remaining=%.6f bestAsk=%.6f (top ask: px=%.6f size=%.6f count=%d)\n",
book.Orders, book.Remaining, book.BestAsk, book.Asks[0].Price, book.Asks[0].Size, book.Asks[0].Count)
fmt.Printf("\nSEEDED: REAL market 0x%s is live on the native D-Chain with a resting maker ask.\n", poolHex)
fmt.Printf(" base=%s quote=%s — both canonical real-asset ids (NOT ascii-of-symbol).\n", plan.BaseID, plan.QuoteID)
}
// depositAmountFor returns a maker base deposit comfortably covering the resting ask
// lock. A SELL of askSize base units locks floor(askSize) base; we credit a generous
// whole-unit cushion so the place always funds. (The native D-Chain credits and locks
// in whole base units on the deposit/place rails.)
func depositAmountFor(askSize float64) uint64 {
amt := uint64(askSize) + 2
if amt < 2 {
amt = 2
}
return amt
}
-361
View File
@@ -1,361 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package main
import (
"encoding/hex"
"testing"
"github.com/luxfi/dex/pkg/dex"
"github.com/luxfi/dex/pkg/zapwire"
"github.com/luxfi/ids"
)
// devnetCChain is the canonical devnet C-Chain blockchain id the seeder derives asset
// ids under. It is the same id the live devnet validators serve.
const devnetCChain = "Bm8X7THQtS2txLrQWTDXN8a4JDuhP4KGybUE754LLSiVFLQ7v"
// Golden AssetID vectors — pinned bytes the dex identity primitive MUST produce for
// fixed (networkID, C-chainID, kind, ref) tuples. These are NOT recomputed from the
// same call under test; they are CONSTANTS frozen here, so a drift in DeriveAssetID's
// fold (domain tag, length-prefix discipline, field order) breaks this test. The C-Chain
// id below decodes to 186f101d…91e57b71 (32 bytes).
const (
// goldenNativeID3 = DeriveAssetID(3, devnetCChain, EVM_NATIVE, EVMNativeMarker).
goldenNativeID3 = "b3e7122cf728552ca6e7336e95ade83c2f48711b7ac963cebe549afeaf6a73f4"
// goldenERC20ID3 = DeriveAssetID(3, devnetCChain, ERC20, 0x00..01) (20-byte address,
// last byte 0x01).
goldenERC20ID3 = "c7e26133e8a0c2201a65341b24b98446d6a5bd4dfa369a34682bfa6da2b4455b"
)
func mustCChain(t *testing.T) ids.ID {
t.Helper()
cid, err := ids.FromString(devnetCChain)
if err != nil {
t.Fatalf("parse devnet C-Chain id: %v", err)
}
return cid
}
func erc20Addr(lastByte byte) []byte {
a := make([]byte, 20)
a[19] = lastByte
return a
}
// TestGoldenAssetIDs is the GOLDEN VECTOR proof: DeriveAssetID produces the exact,
// frozen 32-byte ids for known tuples. This pins the consensus-native identity the
// seeder uses to the chain's own derivation — if either drifts, a seeded market would
// bind a different id than the chain admits, and this test fails first.
func TestGoldenAssetIDs(t *testing.T) {
cid := mustCChain(t)
natID, err := dex.DeriveAssetID(3, cid, dex.AssetKindEVMNative, dex.EVMNativeMarker)
if err != nil {
t.Fatalf("derive native id: %v", err)
}
if got := hex.EncodeToString(natID[:]); got != goldenNativeID3 {
t.Fatalf("native(3) AssetID drift:\n got %s\n want %s", got, goldenNativeID3)
}
ercID, err := dex.DeriveAssetID(3, cid, dex.AssetKindERC20, erc20Addr(0x01))
if err != nil {
t.Fatalf("derive erc20 id: %v", err)
}
if got := hex.EncodeToString(ercID[:]); got != goldenERC20ID3 {
t.Fatalf("erc20(3,..01) AssetID drift:\n got %s\n want %s", got, goldenERC20ID3)
}
// The two ids MUST differ (kind domain-separation): native and an ERC-20 are never
// the same asset even by accident.
if natID == ercID {
t.Fatal("native and ERC-20 ids collided — kind domain separation broken")
}
}
// TestResolveID_MatchesGolden proves the seeder's assetSpec.resolveID delegates to the
// SAME primitive: a native spec and an ERC-20 spec resolve to the golden ids.
func TestResolveID_MatchesGolden(t *testing.T) {
cid := mustCChain(t)
natID, err := nativeSpec().resolveID(3, cid)
if err != nil {
t.Fatalf("native resolveID: %v", err)
}
if got := hex.EncodeToString(natID[:]); got != goldenNativeID3 {
t.Fatalf("nativeSpec.resolveID drift:\n got %s\n want %s", got, goldenNativeID3)
}
ercID, err := erc20Spec(erc20Addr(0x01)).resolveID(3, cid)
if err != nil {
t.Fatalf("erc20 resolveID: %v", err)
}
if got := hex.EncodeToString(ercID[:]); got != goldenERC20ID3 {
t.Fatalf("erc20Spec.resolveID drift:\n got %s\n want %s", got, goldenERC20ID3)
}
}
// TestResolveID_RejectsSynthetic proves the seeder REFUSES synthetic / malformed asset
// refs at derivation time — the property that makes dexseed safe against a shared
// cluster. None of these can ever become a poolID or a bound asset.
func TestResolveID_RejectsSynthetic(t *testing.T) {
cid := mustCChain(t)
cases := []struct {
name string
spec assetSpec
}{
{
// THE BRICKING FORM: an ascii-of-symbol "address" — "LUX" left in a byte
// slice that is NOT a 20-byte token address.
name: "ascii-ticker-LUX (not 20 bytes)",
spec: assetSpec{Kind: dex.AssetKindERC20, Ref: []byte("LUX")},
},
{
// An ascii ticker right-shaped to 20 bytes is still NOT a real token, but the
// derivation can only refuse it on shape; the CHAIN's reality gate (no code)
// refuses the rest. We assert the seeder refuses the clearly-malformed shapes
// here and rely on the chain's EXTCODESIZE gate for shaped-but-fake. The
// all-zero ERC-20 address (== the native marker) MUST be refused locally.
name: "ERC20 zero address (== native marker, not a token)",
spec: assetSpec{Kind: dex.AssetKindERC20, Ref: make([]byte, 20)},
},
{
name: "ERC20 wrong length (33 bytes)",
spec: assetSpec{Kind: dex.AssetKindERC20, Ref: make([]byte, 33)},
},
{
name: "invalid kind (0)",
spec: assetSpec{Kind: dex.AssetKindInvalid, Ref: erc20Addr(0x01)},
},
{
name: "EVM_NATIVE with a non-marker ref",
spec: assetSpec{Kind: dex.AssetKindEVMNative, Ref: erc20Addr(0x01)},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if id, err := tc.spec.resolveID(3, cid); err == nil {
t.Fatalf("resolveID MUST reject %s, but it derived id %x", tc.name, id)
}
})
}
}
// TestPoolIDDeterministicAndNonAscii proves the poolID derivation is (a) deterministic
// and (b) structurally NOT an ascii-of-symbol id (the removed synthetic-seeder form).
func TestPoolIDDeterministicAndNonAscii(t *testing.T) {
cid := mustCChain(t)
baseID, err := nativeSpec().resolveID(3, cid)
if err != nil {
t.Fatalf("native resolveID: %v", err)
}
quoteID, err := erc20Spec(erc20Addr(0x01)).resolveID(3, cid)
if err != nil {
t.Fatalf("erc20 resolveID: %v", err)
}
p1 := poolIDFor(baseID, quoteID)
p2 := poolIDFor(baseID, quoteID)
if p1 != p2 {
t.Fatalf("poolIDFor is not deterministic: %x != %x", p1, p2)
}
// Order matters: (base,quote) and (quote,base) are distinct markets, so distinct ids.
if pSwapped := poolIDFor(quoteID, baseID); pSwapped == p1 {
t.Fatal("poolIDFor(base,quote) must differ from poolIDFor(quote,base)")
}
// NOT ascii-of-symbol: the removed synthetic-seeder form copied a ticker into the high
// bytes and set the last byte to 0xD0. Our derivation must match neither the
// "LUX/<ERC20>" ascii prefix nor the 0xD0 suffix sentinel.
ascii := asciiPoolID("LUX/ERC20")
if p1 == ascii {
t.Fatal("poolID collided with an ascii-of-symbol poolID")
}
if p1[31] == 0xD0 {
t.Fatal("poolID ends in the 0xD0 synthetic sentinel — looks ascii-shaped")
}
// No leading run of printable-ascii ticker bytes (the ascii form starts with the
// symbol). A real hash is uniformly random; assert the first 8 bytes are not all in
// the printable-ascii band an ascii ticker would occupy.
allPrintable := true
for _, b := range p1[:8] {
if b < 0x20 || b > 0x7e {
allPrintable = false
break
}
}
if allPrintable {
t.Fatalf("poolID first 8 bytes are all printable ascii (%q) — looks like a synthetic ticker id", p1[:8])
}
}
// asciiPoolID reproduces the removed synthetic-seeder poolID form — the SYNTHETIC shape
// dexseed must never produce (kept here only as the negative oracle for the test above).
func asciiPoolID(sym string) [32]byte {
var p [32]byte
copy(p[:], sym)
p[31] = 0xD0
return p
}
// TestRealMarketFrames proves the frozen frames are built correctly and bind the
// canonical real ids: the OpenMarket frame is exactly 96 bytes, its base/quote slots
// equal the derived ids, the Deposit frame is 88 bytes binding the base id, and the
// Place frame is a 65-byte resting SELL by the maker at the given price/size.
func TestRealMarketFrames(t *testing.T) {
cid := mustCChain(t)
const (
net uint32 = 3
maker = "maker"
askPrice = 1.25
askSize = 7.0
depAmt uint64 = 9
)
var depRef [32]byte
depRef[0] = 0xAB
open, dep, place, plan, err := realMarketFrames(net, cid, nativeSpec(), erc20Spec(erc20Addr(0x01)), maker, askPrice, askSize, depAmt, depRef)
if err != nil {
t.Fatalf("realMarketFrames: %v", err)
}
// The plan's ids must be the golden ids.
if got := hex.EncodeToString(plan.BaseID[:]); got != goldenNativeID3 {
t.Fatalf("plan baseID = %s, want golden native %s", got, goldenNativeID3)
}
if got := hex.EncodeToString(plan.QuoteID[:]); got != goldenERC20ID3 {
t.Fatalf("plan quoteID = %s, want golden erc20 %s", got, goldenERC20ID3)
}
// (a) OpenMarket is 96 bytes and its slots equal the derived ids.
if len(open) != zapwire.OpenMarketReqSize {
t.Fatalf("OpenMarket frame len = %d, want %d", len(open), zapwire.OpenMarketReqSize)
}
gotPool, gotBase, gotQuote, err := zapwire.DecodeOpenMarket(open)
if err != nil {
t.Fatalf("DecodeOpenMarket: %v", err)
}
if gotPool != plan.PoolID {
t.Fatalf("OpenMarket poolID slot = %x, want %x", gotPool, plan.PoolID)
}
if hex.EncodeToString(gotBase[:]) != goldenNativeID3 {
t.Fatalf("OpenMarket base slot = %x, want golden native", gotBase)
}
if hex.EncodeToString(gotQuote[:]) != goldenERC20ID3 {
t.Fatalf("OpenMarket quote slot = %x, want golden erc20", gotQuote)
}
// (b) Deposit is 88 bytes, binds the maker + base id + amount + ref.
if len(dep) != zapwire.DepositReqSize {
t.Fatalf("Deposit frame len = %d, want %d", len(dep), zapwire.DepositReqSize)
}
gotUser, gotAsset, gotAmt, gotRef, err := zapwire.DecodeDeposit(dep)
if err != nil {
t.Fatalf("DecodeDeposit: %v", err)
}
if gotUser != maker {
t.Fatalf("Deposit user = %q, want %q", gotUser, maker)
}
if hex.EncodeToString(gotAsset[:]) != goldenNativeID3 {
t.Fatalf("Deposit asset = %x, want golden native (base)", gotAsset)
}
if gotAmt != depAmt {
t.Fatalf("Deposit amount = %d, want %d", gotAmt, depAmt)
}
if gotRef != depRef {
t.Fatalf("Deposit ref = %x, want %x", gotRef, depRef)
}
// (c) Place is 65 bytes, a resting SELL by the maker at askPrice x askSize on poolID.
if len(place) != zapwire.PlaceReqSize {
t.Fatalf("Place frame len = %d, want %d", len(place), zapwire.PlaceReqSize)
}
gotPP, gotSide, gotPrice, gotSize, gotPlaceUser, err := zapwire.DecodePlace(place)
if err != nil {
t.Fatalf("DecodePlace: %v", err)
}
if gotPP != plan.PoolID {
t.Fatalf("Place poolID = %x, want %x", gotPP, plan.PoolID)
}
if gotSide != zapwire.SideSell {
t.Fatalf("Place side = %d, want SELL(%d)", gotSide, zapwire.SideSell)
}
if gotPrice != askPrice || gotSize != askSize {
t.Fatalf("Place price/size = %v/%v, want %v/%v", gotPrice, gotSize, askPrice, askSize)
}
if gotPlaceUser != maker {
t.Fatalf("Place user = %q, want %q", gotPlaceUser, maker)
}
}
// TestRealMarketFrames_RejectsSyntheticQuote proves the whole frame-build refuses a
// synthetic quote ref — no frames are returned for a market the chain would refuse.
func TestRealMarketFrames_RejectsSyntheticQuote(t *testing.T) {
cid := mustCChain(t)
syntheticQuote := assetSpec{Kind: dex.AssetKindERC20, Ref: []byte("LUSD")} // ascii ticker, not 20 bytes
open, dep, place, _, err := realMarketFrames(3, cid, nativeSpec(), syntheticQuote, "maker", 1.0, 1.0, 3, [32]byte{})
if err == nil {
t.Fatal("realMarketFrames MUST reject a synthetic (ascii-ticker) quote ref")
}
if open != nil || dep != nil || place != nil {
t.Fatal("no frames may be returned when asset admission fails")
}
}
// TestRealMarketFrames_RejectsSameBaseQuote proves a market whose base and quote
// resolve to the SAME id is refused (it is not a market).
func TestRealMarketFrames_RejectsSameBaseQuote(t *testing.T) {
cid := mustCChain(t)
same := erc20Spec(erc20Addr(0x07))
if _, _, _, _, err := realMarketFrames(3, cid, same, same, "maker", 1.0, 1.0, 3, [32]byte{}); err == nil {
t.Fatal("realMarketFrames MUST reject base==quote")
}
}
// TestDepositRefDeterministic proves the deposit idempotency ref is deterministic in
// (poolID, asset, amount) — a byte-identical re-seed dedups to exactly-once on the
// content-addressed D-Chain.
func TestDepositRefDeterministic(t *testing.T) {
cid := mustCChain(t)
baseID, err := nativeSpec().resolveID(3, cid)
if err != nil {
t.Fatalf("resolveID: %v", err)
}
pid := poolIDFor(baseID, baseID) // any pid for the test
r1 := depositRefFor(pid, baseID, 100)
r2 := depositRefFor(pid, baseID, 100)
if r1 != r2 {
t.Fatalf("depositRefFor not deterministic: %x != %x", r1, r2)
}
if r3 := depositRefFor(pid, baseID, 101); r3 == r1 {
t.Fatal("depositRefFor must change with amount")
}
}
// TestParseERC20Addr exercises the boundary parser: it accepts a 0x-prefixed and a bare
// 20-byte address, and rejects wrong-length / non-hex.
func TestParseERC20Addr(t *testing.T) {
ok := []string{
"0x0000000000000000000000000000000000000001",
"0000000000000000000000000000000000000001",
}
for _, s := range ok {
b, err := parseERC20Addr(s)
if err != nil {
t.Fatalf("parseERC20Addr(%q) unexpected error: %v", s, err)
}
if len(b) != 20 {
t.Fatalf("parseERC20Addr(%q) len = %d, want 20", s, len(b))
}
}
bad := []string{"", "0x", "0x01", "0xZZ00000000000000000000000000000000000001",
"0x000000000000000000000000000000000000000001"} // 21 bytes
for _, s := range bad {
if _, err := parseERC20Addr(s); err == nil {
t.Fatalf("parseERC20Addr(%q) must error", s)
}
}
}
-188
View File
@@ -1,188 +0,0 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package main
import (
"crypto/sha256"
"fmt"
"github.com/luxfi/dex/pkg/dex"
"github.com/luxfi/dex/pkg/zapwire"
"github.com/luxfi/ids"
)
// seed.go is the PURE, testable core of the canonical real-market seeder. It holds
// no I/O: every function here turns inputs into bytes (frozen zapwire frames) or
// derives the canonical 32-byte identities the native D-Chain keys its ledger by.
// main.go is the thin transport that POSTs these frames over HTTP.
//
// THE IDENTITY RULE this file enforces (and the reason this seeder is SAFE against a
// shared cluster, with no guard needed): every asset handle is the dex CANONICAL
// AssetID — DeriveAssetID(networkID, C-chainID, kind, canonicalRef) — the SAME
// consensus-native identity primitive the on-chain resolver computes. It is NEVER an
// ascii-of-symbol id (the synthetic form, e.g. 0x4c5558 = "LUX", that bricked the
// chain by binding a resting order to a market with no real asset). A synthetic /
// malformed ref is rejected HERE, at frame-build time, before a single byte is sent,
// by CanonicalRefFor inside DeriveAssetID.
// poolIDDomain is the domain-separation tag for the canonical poolID derivation. It
// is folded as the FIRST field so a poolID preimage can never collide with any other
// SHA-256 use, and so the result is structurally unrelated to an ascii ticker.
var poolIDDomain = []byte("lux:dex:dexseed:poolid:v1")
// assetSpec names a real on-chain asset by WHERE IT LIVES: its kind (EVM_NATIVE /
// ERC20 / UTXO) and its canonical on-chain reference (the native marker for
// EVM_NATIVE, the 20-byte token address for ERC20, the 32-byte source assetID for
// UTXO). It is the seeder's input unit; resolveID turns it into the canonical
// AssetID the chain keys by. It deliberately carries NO ticker/symbol — identity is
// the derived id, never a string.
type assetSpec struct {
Kind dex.AssetKind
Ref []byte
}
// nativeSpec is the canonical EVM_NATIVE (LUX) asset spec: the EVM zero-address
// native marker. Its derived id is the chain's own coin under the bound network.
func nativeSpec() assetSpec {
return assetSpec{Kind: dex.AssetKindEVMNative, Ref: dex.EVMNativeMarker}
}
// erc20Spec builds an ERC20 asset spec from a 20-byte token contract address. The
// address is validated (length, non-zero) when the spec is resolved, not here.
func erc20Spec(addr []byte) assetSpec {
ref := make([]byte, len(addr))
copy(ref, addr)
return assetSpec{Kind: dex.AssetKindERC20, Ref: ref}
}
// resolveID derives the canonical, consensus-native 32-byte AssetID for this spec
// under (networkID, cChainID). It is the SINGLE admission gate in the seeder: a
// synthetic/malformed ref (wrong length, zero ERC-20 address, non-marker native ref,
// invalid kind) is REFUSED here by dex.DeriveAssetID — so no frame is ever built,
// let alone sent, for an asset that is not a well-formed real on-chain identity. This
// is byte-identical to what the chain's resolver derives, so a market this seeder
// opens binds the SAME id the chain admits.
func (a assetSpec) resolveID(networkID uint32, cChainID ids.ID) (ids.ID, error) {
id, err := dex.DeriveAssetID(networkID, cChainID, a.Kind, a.Ref)
if err != nil {
return ids.Empty, fmt.Errorf("resolve %s asset id: %w", a.Kind, err)
}
return id, nil
}
// poolIDFor derives the canonical, deterministic, NON-ascii poolID for a (base,
// quote) market from the two REAL derived asset ids:
//
// poolID = SHA-256( len|domain ‖ len|baseID ‖ len|quoteID )
//
// It is reproducible (the same base/quote always yield the same poolID, so a re-seed
// is idempotent at the market level) and structurally cannot equal an ascii ticker
// (the domain tag dominates the preimage). It is keyed by the DERIVED ids, not the
// refs, so two markets over the same economic pair on the same network share one
// poolID by construction.
func poolIDFor(baseID, quoteID ids.ID) [32]byte {
h := sha256.New()
writeLenPrefixed(h, poolIDDomain)
writeLenPrefixed(h, baseID[:])
writeLenPrefixed(h, quoteID[:])
var pid [32]byte
copy(pid[:], h.Sum(nil))
return pid
}
// writeLenPrefixed writes an 8-byte big-endian length followed by b, so the fold is
// injective (no two distinct field tuples share a preimage) — the same discipline as
// dex's assetFolder.
func writeLenPrefixed(h interface{ Write([]byte) (int, error) }, b []byte) {
var lp [8]byte
lp[0] = byte(len(b) >> 56)
lp[1] = byte(len(b) >> 48)
lp[2] = byte(len(b) >> 40)
lp[3] = byte(len(b) >> 32)
lp[4] = byte(len(b) >> 24)
lp[5] = byte(len(b) >> 16)
lp[6] = byte(len(b) >> 8)
lp[7] = byte(len(b))
_, _ = h.Write(lp[:])
_, _ = h.Write(b)
}
// marketPlan is the fully-derived, validated description of one real market the
// seeder will create: the canonical poolID and both REAL asset ids. It is produced by
// realMarketFrames (which also returns the frozen frames) and is what main.go reads
// back the book/markets against.
type marketPlan struct {
PoolID [32]byte
BaseID ids.ID
QuoteID ids.ID
}
// realMarketFrames is the pure heart of the seeder. Given the network identity, the
// C-Chain id, the base/quote REAL asset specs, the maker's 16-byte CLOB user handle,
// and the resting ask price/size, it:
//
// 1. derives both canonical asset ids (REFUSING any synthetic/malformed ref);
// 2. derives the canonical NON-ascii poolID from those ids;
// 3. builds the three FROZEN zapwire frames that seed the market end-to-end:
// - OpenMarket(poolID, baseID, quoteID) — bind the real (base,quote);
// - Deposit(maker, baseID, depositAmount, ref) — fund the maker's base balance;
// - Place(poolID, SELL, askPrice, askSize, maker) — rest a maker limit ask.
//
// It returns the raw frames, the validated plan, and any derivation error. It does NO
// I/O — main.go posts the frames. depositAmount must cover the ask lock (>= askSize in
// base whole units); ref is the deposit idempotency reference (originating-op id).
func realMarketFrames(
networkID uint32,
cChainID ids.ID,
base, quote assetSpec,
maker string,
askPrice, askSize float64,
depositAmount uint64,
depositRef [32]byte,
) (openMarket, deposit, place []byte, plan marketPlan, err error) {
baseID, err := base.resolveID(networkID, cChainID)
if err != nil {
return nil, nil, nil, marketPlan{}, fmt.Errorf("base: %w", err)
}
quoteID, err := quote.resolveID(networkID, cChainID)
if err != nil {
return nil, nil, nil, marketPlan{}, fmt.Errorf("quote: %w", err)
}
if baseID == quoteID {
return nil, nil, nil, marketPlan{}, fmt.Errorf("base and quote resolve to the same asset id %s — not a market", baseID)
}
pid := poolIDFor(baseID, quoteID)
openMarket = zapwire.EncodeOpenMarket(pid, baseID, quoteID)
deposit = zapwire.EncodeDeposit(maker, baseID, depositAmount, depositRef)
place = zapwire.EncodePlace(pid, zapwire.SideSell, askPrice, askSize, maker)
return openMarket, deposit, place, marketPlan{PoolID: pid, BaseID: baseID, QuoteID: quoteID}, nil
}
// depositRefFor derives a deterministic idempotency reference for the maker's seed
// deposit, domain-separated and bound to (poolID, asset, amount). The native D-Chain
// content-addresses its dedup index on the full frame, so a byte-identical re-seed
// (same pool, same asset, same amount) dedups to exactly-once — a re-run does not
// double-credit. It folds the canonical asset id (32 bytes), not a symbol.
func depositRefFor(poolID [32]byte, assetID ids.ID, amount uint64) [32]byte {
h := sha256.New()
writeLenPrefixed(h, []byte("lux:dex:dexseed:depositref:v1"))
writeLenPrefixed(h, poolID[:])
writeLenPrefixed(h, assetID[:])
var amt [8]byte
amt[0] = byte(amount >> 56)
amt[1] = byte(amount >> 48)
amt[2] = byte(amount >> 40)
amt[3] = byte(amount >> 32)
amt[4] = byte(amount >> 24)
amt[5] = byte(amount >> 16)
amt[6] = byte(amount >> 8)
amt[7] = byte(amount)
writeLenPrefixed(h, amt[:])
var ref [32]byte
copy(ref[:], h.Sum(nil))
return ref
}
-82
View File
@@ -1,82 +0,0 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command fix-gateway runs the FIX 4.4 acceptor as a long-lived TCP server in
// front of a running D-Chain venue (cmd/dvenue / the node runtime). It is the
// SERVER side of the FIX trading path: a FIX client (the SDK's EnableFIX(), or
// any FIX 4.4 engine) connects, logs on, and sends NewOrderSingle / OrderCancel
// messages; the gateway translates each into the SAME clob_* venue operation that
// ZAP, WS, and the 0x9010 precompile use, and renders the consensus-computed
// result back as a FIX ExecutionReport.
//
// It owns NO matcher and NO book — it dials the venue's clob_* ZAP endpoint
// (-venue) and is a pure protocol front-end (pkg/fix + pkg/venue). This is the
// "one matcher, many protocols" topology: the matcher lives in the venue
// (pkg/dchain), and fix-gateway is the FIX dialect onto it, exactly parallel to
// the WS and precompile front-ends.
package main
import (
"context"
"flag"
"os"
"os/signal"
"syscall"
"time"
"github.com/luxfi/dex/pkg/fix"
"github.com/luxfi/dex/pkg/venue"
log "github.com/luxfi/log"
)
func main() {
addr := flag.String("addr", "127.0.0.1:5001", "TCP listen address for the FIX acceptor")
venueAddr := flag.String("venue", "127.0.0.1:9099", "ZAP endpoint of the D-Chain venue (clob_*)")
target := flag.String("target", "LXDEX", "this acceptor's CompID (the client's TargetCompID)")
heartBt := flag.Int("heartbeat", 30, "heartbeat interval advertised on Logon (seconds)")
dialTimeout := flag.Duration("dial-timeout", 10*time.Second, "venue dial timeout")
flag.Parse()
logger := log.Root()
// Dial the venue's clob_* surface — the SAME socket the precompile and proxy
// dial. The gateway holds this one connection and routes every FIX order
// through it.
dialCtx, dialCancel := context.WithTimeout(context.Background(), *dialTimeout)
v, err := venue.DialVenue(dialCtx, *venueAddr)
dialCancel()
if err != nil {
logger.Error("dial venue", "venue", *venueAddr, "err", err)
os.Exit(1)
}
acc, err := fix.NewAcceptor(fix.Config{
Addr: *addr,
Venue: v,
TargetCompID: *target,
HeartBtInt: *heartBt,
Logger: logger,
})
if err != nil {
logger.Error("new acceptor", "addr", *addr, "err", err)
os.Exit(1)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Graceful shutdown on SIGINT/SIGTERM.
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sig
logger.Info("fix-gateway shutting down")
cancel()
}()
logger.Info("fix-gateway up", "addr", acc.Addr(), "venue", *venueAddr, "target", *target, "heartbeat", *heartBt)
if err := acc.Serve(ctx); err != nil {
logger.Error("acceptor serve", "err", err)
os.Exit(1)
}
}
-80
View File
@@ -1,80 +0,0 @@
// Gateway API server that aggregates multiple DEX providers.
// Connects to EVM nodes for on-chain pool data via DEX precompiles.
//
// Configuration via flags or environment variables:
//
// --rpc / RPC_URL EVM JSON-RPC endpoint (required)
// --chain-id / CHAIN_ID EVM chain ID (required)
// --addr Listen address (default :8080)
package main
import (
"flag"
"fmt"
"log"
"os"
"strconv"
"github.com/luxfi/dex/pkg/gateway"
"github.com/luxfi/dex/pkg/gateway/lux"
)
func main() {
addr := flag.String("addr", ":8080", "Server listen address")
rpcURL := flag.String("rpc", "", "EVM JSON-RPC endpoint (env: RPC_URL)")
chainIDFlag := flag.String("chain-id", "", "EVM chain ID (env: CHAIN_ID)")
flag.Parse()
// Resolve from env if not set via flags
if *rpcURL == "" {
*rpcURL = os.Getenv("RPC_URL")
}
if *chainIDFlag == "" {
*chainIDFlag = os.Getenv("CHAIN_ID")
}
if *rpcURL == "" {
log.Fatal("--rpc or RPC_URL is required")
}
if *chainIDFlag == "" {
log.Fatal("--chain-id or CHAIN_ID is required")
}
chainID, err := strconv.ParseUint(*chainIDFlag, 10, 64)
if err != nil {
log.Fatalf("invalid chain ID %q: %v", *chainIDFlag, err)
}
gwChainID := gateway.ChainID(chainID)
config := gateway.GatewayConfig{
DefaultChainID: gwChainID,
EnableFallback: false,
CacheEnabled: true,
CacheTTLSeconds: 60,
}
gw := gateway.New(config)
provider := lux.NewProvider(lux.ProviderConfig{
Name: "native",
Priority: 10,
Chains: []gateway.ChainID{gwChainID},
RPCEndpoints: map[gateway.ChainID]string{
gwChainID: *rpcURL,
},
})
if err := gw.RegisterProvider(provider); err != nil {
log.Fatalf("Failed to register provider: %v", err)
}
log.Printf("DEX gateway starting on %s", *addr)
log.Printf(" Chain ID: %d", chainID)
log.Printf(" RPC: %s", *rpcURL)
fmt.Println()
if err := gw.StartWithGracefulShutdown(*addr); err != nil {
log.Fatalf("Server error: %v", err)
}
}
-424
View File
@@ -1,424 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
"github.com/nats-io/nats.go"
)
type Monitor struct {
nc *nats.Conn
stats Stats
clients map[*websocket.Conn]bool
mu sync.RWMutex
upgrader websocket.Upgrader
}
type Stats struct {
Orders int64 `json:"orders"`
Trades int64 `json:"trades"`
OrdersPerSec float64 `json:"orders_per_sec"`
TradesPerSec float64 `json:"trades_per_sec"`
ActiveNodes int `json:"active_nodes"`
Memory MemStats `json:"memory"`
Timestamp time.Time `json:"timestamp"`
}
type MemStats struct {
Alloc uint64 `json:"alloc_mb"`
Sys uint64 `json:"sys_mb"`
NumGC uint32 `json:"num_gc"`
NumCPU int `json:"num_cpu"`
Goroutines int `json:"goroutines"`
}
var dashboardHTML = `
<!DOCTYPE html>
<html>
<head>
<title>LX Monitor</title>
<meta charset="utf-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.metric {
font-size: 3em;
font-weight: bold;
margin: 10px 0;
background: linear-gradient(45deg, #f093fb 0%, #f5576c 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.label {
font-size: 0.9em;
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 1px;
}
.chart {
height: 200px;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
position: relative;
overflow: hidden;
}
.bar {
position: absolute;
bottom: 0;
width: 3px;
background: linear-gradient(to top, #00f260 0%, #0575e6 100%);
}
.status {
padding: 5px 10px;
border-radius: 20px;
display: inline-block;
font-size: 0.9em;
}
.status.online {
background: #4caf50;
}
.status.offline {
background: #f44336;
}
#log {
background: rgba(0, 0, 0, 0.3);
padding: 15px;
border-radius: 10px;
font-family: monospace;
font-size: 0.9em;
max-height: 300px;
overflow-y: auto;
}
.log-entry {
padding: 5px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 LX Real-Time Monitor</h1>
<div class="grid">
<div class="card">
<div class="label">Total Orders</div>
<div class="metric" id="orders">0</div>
<div class="label">Orders/sec: <span id="orders-rate">0</span></div>
</div>
<div class="card">
<div class="label">Total Trades</div>
<div class="metric" id="trades">0</div>
<div class="label">Trades/sec: <span id="trades-rate">0</span></div>
</div>
<div class="card">
<div class="label">Memory Usage</div>
<div class="metric" id="memory">0 MB</div>
<div class="label">GC Runs: <span id="gc">0</span></div>
</div>
<div class="card">
<div class="label">System</div>
<div class="metric" id="goroutines">0</div>
<div class="label">Goroutines | CPUs: <span id="cpus">0</span></div>
</div>
</div>
<div class="card">
<div class="label">Orders/sec History (Last 60 seconds)</div>
<div class="chart" id="chart">
<canvas id="canvas" width="1200" height="200"></canvas>
</div>
</div>
<div class="card">
<div class="label">Live Log</div>
<div id="log"></div>
</div>
</div>
<script>
const ws = new WebSocket('ws://localhost:8080/ws');
const history = [];
const maxHistory = 60;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Update metrics
document.getElementById('orders').textContent = data.orders.toLocaleString();
document.getElementById('trades').textContent = data.trades.toLocaleString();
document.getElementById('orders-rate').textContent = Math.round(data.orders_per_sec);
document.getElementById('trades-rate').textContent = Math.round(data.trades_per_sec);
document.getElementById('memory').textContent = data.memory.alloc_mb + ' MB';
document.getElementById('gc').textContent = data.memory.num_gc;
document.getElementById('goroutines').textContent = data.memory.goroutines;
document.getElementById('cpus').textContent = data.memory.num_cpu;
// Update history
history.push(data.orders_per_sec);
if (history.length > maxHistory) {
history.shift();
}
// Draw chart
drawChart();
// Add log entry
addLogEntry(data);
};
function drawChart() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (history.length < 2) return;
const maxValue = Math.max(...history, 1);
const barWidth = canvas.width / maxHistory;
// Draw grid lines
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {
const y = (canvas.height / 5) * i;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// Draw bars
const gradient = ctx.createLinearGradient(0, canvas.height, 0, 0);
gradient.addColorStop(0, '#00f260');
gradient.addColorStop(1, '#0575e6');
ctx.fillStyle = gradient;
history.forEach((value, index) => {
const height = (value / maxValue) * canvas.height;
const x = index * barWidth;
ctx.fillRect(x, canvas.height - height, barWidth - 1, height);
});
}
function addLogEntry(data) {
const log = document.getElementById('log');
const entry = document.createElement('div');
entry.className = 'log-entry';
const time = new Date(data.timestamp).toLocaleTimeString();
entry.textContent = time + ' | Orders: ' + data.orders + ' | Rate: ' +
Math.round(data.orders_per_sec) + '/s | Memory: ' +
data.memory.alloc_mb + 'MB';
log.insertBefore(entry, log.firstChild);
// Keep only last 20 entries
while (log.children.length > 20) {
log.removeChild(log.lastChild);
}
}
ws.onopen = () => {
addLogEntry({ timestamp: new Date(), orders: 0, orders_per_sec: 0, memory: { alloc_mb: 0 } });
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
</script>
</body>
</html>
`
func main() {
monitor := &Monitor{
clients: make(map[*websocket.Conn]bool),
upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
}
// Connect to NATS
nc, err := nats.Connect(nats.DefaultURL)
if err != nil {
log.Printf("Running without NATS: %v", err)
} else {
monitor.nc = nc
defer nc.Close()
}
// Start background workers
go monitor.collectStats()
go monitor.broadcastStats()
// HTTP handlers
http.HandleFunc("/", monitor.handleDashboard)
http.HandleFunc("/ws", monitor.handleWebSocket)
http.HandleFunc("/api/stats", monitor.handleAPI)
fmt.Println("📊 LX Monitor")
fmt.Println("=================")
fmt.Println("Dashboard: http://localhost:8080")
fmt.Println("WebSocket: ws://localhost:8080/ws")
fmt.Println("API: http://localhost:8080/api/stats")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func (m *Monitor) handleDashboard(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(dashboardHTML))
}
func (m *Monitor) handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := m.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade error: %v", err)
return
}
defer conn.Close()
m.mu.Lock()
m.clients[conn] = true
m.mu.Unlock()
defer func() {
m.mu.Lock()
delete(m.clients, conn)
m.mu.Unlock()
}()
// Keep connection alive
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
}
func (m *Monitor) handleAPI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
m.mu.RLock()
json.NewEncoder(w).Encode(m.stats)
m.mu.RUnlock()
}
func (m *Monitor) collectStats() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
var lastOrders, lastTrades int64
lastTime := time.Now()
for range ticker.C {
now := time.Now()
deltaTime := now.Sub(lastTime).Seconds()
// Get memory stats
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
// Calculate rates
orders := atomic.LoadInt64(&m.stats.Orders)
trades := atomic.LoadInt64(&m.stats.Trades)
ordersPerSec := float64(orders-lastOrders) / deltaTime
tradesPerSec := float64(trades-lastTrades) / deltaTime
// Update stats
m.mu.Lock()
m.stats.OrdersPerSec = ordersPerSec
m.stats.TradesPerSec = tradesPerSec
m.stats.Memory = MemStats{
Alloc: memStats.Alloc / 1024 / 1024,
Sys: memStats.Sys / 1024 / 1024,
NumGC: memStats.NumGC,
NumCPU: runtime.NumCPU(),
Goroutines: runtime.NumGoroutine(),
}
m.stats.Timestamp = now
m.mu.Unlock()
lastOrders = orders
lastTrades = trades
lastTime = now
}
}
func (m *Monitor) broadcastStats() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
m.mu.RLock()
data, _ := json.Marshal(m.stats)
clients := make([]*websocket.Conn, 0, len(m.clients))
for client := range m.clients {
clients = append(clients, client)
}
m.mu.RUnlock()
// Broadcast to all connected clients
for _, client := range clients {
if err := client.WriteMessage(websocket.TextMessage, data); err != nil {
client.Close()
m.mu.Lock()
delete(m.clients, client)
m.mu.Unlock()
}
}
}
}
// Simulate some data for demo
func init() {
go func() {
time.Sleep(2 * time.Second)
// Demo data generation commented out - will use real data
// for {
// atomic.AddInt64(&Stats{}.Orders, int64(100+rand.Intn(500)))
// atomic.AddInt64(&Stats{}.Trades, int64(50+rand.Intn(250)))
// time.Sleep(100 * time.Millisecond)
// }
}()
}
-207
View File
@@ -1,207 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/signal"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/luxfi/dex/pkg/lx"
"github.com/nats-io/nats.go"
)
type PersistentServer struct {
ob *lx.OrderBook
nc *nats.Conn
js nats.JetStreamContext
ordersCount int64
tradesCount int64
snapshotFile string
mu sync.RWMutex
lastSnapshot time.Time
}
type Snapshot struct {
Timestamp time.Time `json:"timestamp"`
OrdersCount int64 `json:"orders_count"`
TradesCount int64 `json:"trades_count"`
OrderBook string `json:"orderbook_state"`
}
func main() {
var (
natsURL = flag.String("nats", nats.DefaultURL, "NATS server URL")
snapshotFile = flag.String("snapshot", "dex-snapshot.json", "Snapshot file for persistence")
restore = flag.Bool("restore", true, "Restore from snapshot on startup")
)
flag.Parse()
// Setup signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
server := &PersistentServer{
ob: lx.NewOrderBook("BTC-USD"),
snapshotFile: *snapshotFile,
}
// Restore from snapshot if exists
if *restore {
if err := server.restoreSnapshot(); err != nil {
log.Printf("⚠️ No snapshot to restore: %v", err)
} else {
log.Printf("✅ Restored from snapshot: %d orders, %d trades",
server.ordersCount, server.tradesCount)
}
}
// Connect to NATS
nc, err := nats.Connect(*natsURL,
nats.MaxReconnects(-1),
nats.ReconnectWait(1*time.Second),
)
if err != nil {
log.Fatalf("Failed to connect to NATS: %v", err)
}
defer nc.Close()
server.nc = nc
js, err := nc.JetStream()
if err != nil {
log.Fatalf("Failed to get JetStream: %v", err)
}
server.js = js
fmt.Println("🏦 Persistent DEX Server")
fmt.Println("========================")
fmt.Printf("📍 ID: persistent-%d\n", os.Getpid())
fmt.Printf("💾 Snapshot: %s\n", *snapshotFile)
fmt.Printf("📊 Orders: %d, Trades: %d\n", server.ordersCount, server.tradesCount)
fmt.Println("✅ Server ready!")
// Start workers
go server.orderProcessor()
go server.snapshotWorker()
go server.statsReporter()
go server.heartbeat()
// Wait for shutdown signal
<-sigChan
fmt.Println("\n⚠️ Shutting down...")
// Save final snapshot
if err := server.saveSnapshot(); err != nil {
log.Printf("Failed to save snapshot: %v", err)
} else {
fmt.Printf("✅ Snapshot saved: %d orders, %d trades\n",
server.ordersCount, server.tradesCount)
}
}
func (s *PersistentServer) orderProcessor() {
// Subscribe to orders
sub, err := s.nc.SubscribeSync("orders.>")
if err != nil {
log.Fatal(err)
}
for {
msg, err := sub.NextMsg(5 * time.Second)
if err != nil {
continue
}
// Process order
s.mu.Lock()
// Simulate order processing
atomic.AddInt64(&s.ordersCount, 1)
if atomic.LoadInt64(&s.ordersCount)%2 == 0 {
atomic.AddInt64(&s.tradesCount, 1)
}
s.mu.Unlock()
// Acknowledge
msg.Ack()
}
}
func (s *PersistentServer) snapshotWorker() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
if err := s.saveSnapshot(); err != nil {
log.Printf("Failed to save snapshot: %v", err)
}
}
}
func (s *PersistentServer) saveSnapshot() error {
s.mu.RLock()
snapshot := Snapshot{
Timestamp: time.Now(),
OrdersCount: atomic.LoadInt64(&s.ordersCount),
TradesCount: atomic.LoadInt64(&s.tradesCount),
OrderBook: fmt.Sprintf("%+v", s.ob.GetSnapshot()),
}
s.mu.RUnlock()
data, err := json.MarshalIndent(snapshot, "", " ")
if err != nil {
return err
}
// Write to temp file first
tempFile := s.snapshotFile + ".tmp"
if err := os.WriteFile(tempFile, data, 0644); err != nil {
return err
}
// Atomic rename
return os.Rename(tempFile, s.snapshotFile)
}
func (s *PersistentServer) restoreSnapshot() error {
data, err := os.ReadFile(s.snapshotFile)
if err != nil {
return err
}
var snapshot Snapshot
if err := json.Unmarshal(data, &snapshot); err != nil {
return err
}
s.ordersCount = snapshot.OrdersCount
s.tradesCount = snapshot.TradesCount
s.lastSnapshot = snapshot.Timestamp
return nil
}
func (s *PersistentServer) statsReporter() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
orders := atomic.LoadInt64(&s.ordersCount)
trades := atomic.LoadInt64(&s.tradesCount)
fmt.Printf("📊 Stats: Orders=%d, Trades=%d\n", orders, trades)
}
}
func (s *PersistentServer) heartbeat() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
data := fmt.Sprintf("persistent-%d", os.Getpid())
s.nc.Publish("dex.heartbeat", []byte(data))
}
}
-266
View File
@@ -1,266 +0,0 @@
// Command price-feed runs a local price aggregator with all sources.
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/luxfi/dex/pkg/price"
)
func main() {
var (
interval = flag.Duration("interval", 500*time.Millisecond, "display interval")
symbols = flag.String("symbols", "LUX-USDC,LZOO-USDC,LETH-USDC,ZOO-USDC", "symbols to watch")
sources = flag.String("sources", "all", "sources: all,xchain,achain,cchain,zoo,pyth,chainlink")
noVerifier = flag.Bool("no-verify", false, "disable Q-Chain quantum finality verification")
)
flag.Parse()
fmt.Println("LX Price Feed")
fmt.Println("=================")
fmt.Println()
// Create oracle
oracle := price.NewOracle()
sourceList := strings.Split(*sources, ",")
useAll := *sources == "all"
// X-Chain - native DEX (highest priority)
if useAll || contains(sourceList, "xchain") {
xchain := price.NewXChainSource(
"http://localhost:9650/ext/bc/X",
"ws://localhost:9650/ext/bc/X/ws",
)
if err := xchain.Start(); err != nil {
log.Printf("Warning: X-Chain start failed: %v", err)
} else {
oracle.AddSource("x-chain", xchain)
fmt.Println("✓ X-Chain source started (native DEX)")
}
}
// A-Chain - attestations
if useAll || contains(sourceList, "achain") {
achain := price.NewAChainSource(
"http://localhost:9650/ext/bc/A",
"ws://localhost:9650/ext/bc/A/ws",
)
if err := achain.Start(); err != nil {
log.Printf("Warning: A-Chain start failed: %v", err)
} else {
oracle.AddSource("a-chain", achain)
fmt.Println("✓ A-Chain source started (attestations)")
}
}
// C-Chain - AMM pools
if useAll || contains(sourceList, "cchain") {
cchain := price.NewCChainSource(
"http://localhost:9650/ext/bc/C/rpc",
"ws://localhost:9650/ext/bc/C/ws",
)
if err := cchain.Start(); err != nil {
log.Printf("Warning: C-Chain start failed: %v", err)
} else {
oracle.AddSource("c-chain", cchain)
fmt.Println("✓ C-Chain source started (AMM pools)")
}
}
// Pyth Network
if useAll || contains(sourceList, "pyth") {
pyth := price.NewPythSource(
"wss://hermes.pyth.network/ws",
"https://hermes.pyth.network",
)
if err := pyth.Connect(); err != nil {
log.Printf("Warning: Pyth connect failed: %v (will use HTTP fallback)", err)
}
oracle.AddSource("pyth", pyth)
fmt.Println("✓ Pyth source added (real-time oracle)")
}
// Zoo Chain - Zoo Labs DEX
if useAll || contains(sourceList, "zoo") {
zoo := price.NewZooChainSource(
"http://localhost:9651/rpc",
"ws://localhost:9651/ws",
)
if err := zoo.Start(); err != nil {
log.Printf("Warning: Zoo Chain start failed: %v", err)
} else {
oracle.AddSource("zoo-chain", zoo)
fmt.Println("✓ Zoo Chain source started (Zoo Labs DEX)")
}
}
// Q-Chain - Quantum Finality Verifier (enabled by default)
// This is NOT a price source - it verifies finality of prices from other sources
if !*noVerifier {
qchain := price.NewQChainVerifier(
"http://localhost:9650/ext/bc/Q",
"ws://localhost:9650/ext/bc/Q/ws",
)
if err := qchain.Start(); err != nil {
log.Printf("Warning: Q-Chain start failed: %v", err)
} else {
oracle.SetVerifier(qchain)
fmt.Println("✓ Q-Chain verifier attached (quantum finality)")
}
}
// Chainlink
if useAll || contains(sourceList, "chainlink") {
chainlink := price.NewChainlinkSource()
if err := chainlink.Start(); err != nil {
log.Printf("Warning: Chainlink start failed: %v", err)
} else {
oracle.AddSource("chainlink", chainlink)
fmt.Println("✓ Chainlink source started (polling oracle)")
}
}
fmt.Println()
// Parse and watch symbols
syms := strings.Split(*symbols, ",")
for _, sym := range syms {
oracle.Watch(strings.TrimSpace(sym))
}
// Start oracle
if err := oracle.Start(); err != nil {
log.Fatalf("Failed to start oracle: %v", err)
}
fmt.Println("Symbols:", strings.Join(syms, ", "))
fmt.Println()
fmt.Println("Press Ctrl+C to stop")
fmt.Println()
// Signal handling
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
fmt.Println("\nShutting down...")
cancel()
}()
// Wait for initial data
fmt.Println("Waiting for price data...")
time.Sleep(time.Second)
// Display loop
ticker := time.NewTicker(*interval)
defer ticker.Stop()
// Show header based on whether verifier is attached
if oracle.Verifier() != nil {
fmt.Printf("\n%-15s %-12s %-12s %-12s %-10s %-10s %-10s %s\n",
"SYMBOL", "PRICE", "TWAP", "VWAP", "CONF", "SOURCE", "FINALITY", "AGE")
} else {
fmt.Printf("\n%-15s %-12s %-12s %-12s %-10s %-10s %s\n",
"SYMBOL", "PRICE", "TWAP", "VWAP", "CONF", "SOURCE", "AGE")
}
fmt.Println(strings.Repeat("-", 100))
for {
select {
case <-ctx.Done():
oracle.Stop()
fmt.Println("Stopped.")
return
case <-ticker.C:
display(oracle, syms)
}
}
}
func display(oracle *price.Oracle, symbols []string) {
hasVerifier := oracle.Verifier() != nil
for _, sym := range symbols {
// Use VerifiedPrice to get finality status if verifier attached
verified, err := oracle.VerifiedPrice(sym)
if err != nil {
fmt.Printf("%-15s %-12s\n", sym, "--")
continue
}
data := verified.Data
age := time.Since(data.Timestamp)
ageStr := fmt.Sprintf("%dms", age.Milliseconds())
if age > time.Second {
ageStr = fmt.Sprintf("%.1fs", age.Seconds())
}
twap := oracle.TWAP(sym)
vwap := oracle.VWAP(sym)
stale := ""
if data.Stale {
stale = " [STALE]"
}
if hasVerifier {
// Show finality status
finalityStr := "PENDING"
if verified.Finalized {
finalityStr = "QUANTUM"
}
fmt.Printf("%-15s $%-11.4f $%-11.4f $%-11.4f %-10.2f %-10s %-10s %s%s\n",
data.Symbol,
data.Price,
twap,
vwap,
data.Confidence,
data.Source,
finalityStr,
ageStr,
stale,
)
} else {
fmt.Printf("%-15s $%-11.4f $%-11.4f $%-11.4f %-10.2f %-10s %s%s\n",
data.Symbol,
data.Price,
twap,
vwap,
data.Confidence,
data.Source,
ageStr,
stale,
)
}
}
// Show finality latency if verifier attached
if hasVerifier {
latency := oracle.Verifier().FinalityLatency()
fmt.Printf("Quantum finality latency: %v\n", latency)
}
fmt.Println()
}
func contains(list []string, s string) bool {
for _, v := range list {
if strings.TrimSpace(v) == s {
return true
}
}
return false
}
BIN
View File
Binary file not shown.
-50
View File
@@ -1,50 +0,0 @@
# Multi-stage build for LX
FROM golang:1.23-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git make
# Set working directory
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o lx-dex ./cmd/demo
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dex-server ./cmd/dex-server
# Final stage
FROM alpine:latest
# Install ca-certificates for HTTPS
RUN apk --no-cache add ca-certificates
# Create non-root user
RUN addgroup -g 1000 -S dex && \
adduser -u 1000 -S dex -G dex
# Set working directory
WORKDIR /app
# Copy binaries from builder
COPY --from=builder /app/lx-dex /app/lx-dex
COPY --from=builder /app/dex-server /app/dex-server
# Change ownership
RUN chown -R dex:dex /app
# Switch to non-root user
USER dex
# Expose ports
EXPOSE 8080 8081
# Default command
CMD ["/app/dex-server"]
-65
View File
@@ -1,65 +0,0 @@
# Multi-stage build for LX with CUDA support
FROM nvidia/cuda:12.3.0-devel-ubuntu22.04 AS builder
# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
wget \
git \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Install Go
RUN wget -q https://go.dev/dl/go1.22.5.linux-amd64.tar.gz && \
tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz && \
rm go1.22.5.linux-amd64.tar.gz
ENV PATH="/usr/local/go/bin:${PATH}"
ENV CGO_ENABLED=0
# Set working directory
WORKDIR /build
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build binaries
RUN go build -ldflags="-s -w" -o lx-dex ./cmd/demo
RUN go build -ldflags="-s -w" -o dex-server ./cmd/dex-server
# Final stage
FROM nvidia/cuda:12.3.0-runtime-ubuntu22.04
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -g 1000 dex && \
useradd -u 1000 -g dex -s /bin/bash -m dex
WORKDIR /app
# Copy binaries from builder
COPY --from=builder /build/lx-dex /app/lx-dex
COPY --from=builder /build/dex-server /app/dex-server
# Change ownership
RUN chown -R dex:dex /app
# Switch to non-root user
USER dex
# Expose ports
EXPOSE 8080 8081
# Environment for CUDA
ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64:${LD_LIBRARY_PATH}
# Default command
CMD ["/app/dex-server"]
-52
View File
@@ -1,52 +0,0 @@
# Build stage
FROM golang:1.21-alpine AS builder
# Install build dependencies
RUN apk add --no-cache gcc musl-dev linux-headers git make
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build with CGO for hybrid engine
ENV CGO_ENABLED=1
ENV GOOS=linux
ENV GOARCH=amd64
# Build the binary
RUN make build
# Runtime stage
FROM alpine:latest
# Install runtime dependencies
RUN apk add --no-cache ca-certificates curl
WORKDIR /app
# Copy binary from builder
COPY --from=builder /app/bin/lx-dex /app/lx-dex
# Copy configuration files
COPY --from=builder /app/config /app/config
# Create data directory
RUN mkdir -p /data
# Expose ports
EXPOSE 8080 8081 5000 9090
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Set entrypoint
ENTRYPOINT ["/app/lx-dex"]
# Default arguments
CMD ["-http", "8080", "-ws", "8081", "-node-port", "5000", "-test", "-engine", "hybrid"]
-100
View File
@@ -1,100 +0,0 @@
services:
# Use pre-built Go image to run DEX
dex-node:
image: golang:1.21-alpine
container_name: lux-dex-node
working_dir: /app
volumes:
- ../backend:/app
- ../bin:/bin/dex
environment:
- NODE_ID=node-1
- CONSENSUS_K=1
- CONSENSUS_N=1
- CGO_ENABLED=1
ports:
- "8080:8080"
- "8081:8081"
- "5000:5000"
command: >
sh -c "
apk add --no-cache gcc musl-dev &&
go build -o /bin/dex/lx-server ./cmd/dex-server &&
echo 'DEX Server built successfully' &&
/bin/dex/lx-server -port 8080
"
depends_on:
- postgres
- redis
networks:
- dex-network
# Use pre-built Node image to run UI
dex-ui:
image: node:18-alpine
container_name: lux-dex-ui
working_dir: /app
volumes:
- ../ui:/app
environment:
- NODE_ENV=development
- NEXT_PUBLIC_WS_URL=ws://localhost:8081
- NEXT_PUBLIC_API_URL=http://localhost:8080
- NEXT_PUBLIC_BASE_PATH=/v2
ports:
- "3000:3000"
command: >
sh -c "
npm install &&
npm run dev
"
depends_on:
- dex-node
networks:
- dex-network
# PostgreSQL Database
postgres:
image: postgres:16-alpine
container_name: lux-dex-db
environment:
- POSTGRES_DB=luxdex
- POSTGRES_USER=dexuser
- POSTGRES_PASSWORD=dexpass123
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- dex-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dexuser -d luxdex"]
interval: 10s
timeout: 5s
retries: 5
# Redis Cache
redis:
image: redis:7-alpine
container_name: lux-dex-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- dex-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
dex-network:
driver: bridge
volumes:
postgres-data:
driver: local
redis-data:
driver: local
BIN
View File
Binary file not shown.
+49
View File
@@ -0,0 +1,49 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package plugin boots the native D-Chain DEX VM as a luxd rpcchainvm plugin,
// mirroring github.com/luxfi/evm/plugin/runner. rpc.Serve takes the engine
// address from the runtime.EngineAddressKey env var, opens this chain's own
// badgerdb/zapdb, hands it to VM.Initialize, and serves block requests so
// BuildBlock/Verify/Accept run inside luxd's multi-validator consensus — the
// exact harness lux/evm boots through. The VM (pkg/dchain) is pure-Go (CGO=0);
// this entrypoint links no native code.
package plugin
import (
"context"
"fmt"
"os"
"github.com/luxfi/dex/pkg/dchain"
log "github.com/luxfi/log"
"github.com/luxfi/version"
"github.com/luxfi/vm/rpc"
)
// Run boots the d-chain VM over the rpcchainvm plugin transport. It blocks
// until the node runtime shuts the VM down. When luxd probes the plugin version
// (a single "version" arg), it prints the version line and exits 0.
func Run() {
versionStr := fmt.Sprintf("Lux-DChain/%s [node=%s, rpcchainvm=%d]",
dchain.Version, version.Current, version.RPCChainVMProtocol)
if printVersion() {
fmt.Println(versionStr)
os.Exit(0)
}
if err := rpc.Serve(context.Background(), log.Root(), &dchain.VM{}); err != nil {
fmt.Printf("dchain rpc.Serve error: %s\n", err)
os.Exit(1)
}
}
// printVersion reports whether the process was asked to just print its version
// and exit. The node passes a single "version" arg when probing a plugin.
func printVersion() bool {
for _, a := range os.Args[1:] {
if a == "version" || a == "--version" || a == "-version" {
return true
}
}
return false
}
@@ -4,7 +4,7 @@
//go:build cgo
// +build cgo
package main
package standalone
// engineName labels which matcher pkg/lx links into this build. Under cgo,
// pkg/lx selects the GPU matcher (amm_gpu_cuda / amm_gpu_metal / orderbook_cuda).
@@ -4,7 +4,7 @@
//go:build !cgo
// +build !cgo
package main
package standalone
// engineName labels which matcher pkg/lx links into this build. Without cgo,
// pkg/lx selects the pure-Go CPU matcher (amm_nogpu / orderbook_cuda_stub).
@@ -1,26 +1,25 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command dvenue runs the standalone D-Chain DEX VENUE as a long-lived ZAP
// server on a fixed TCP address — the dial-able endpoint the 0x9010 EVM
// precompile (NewZAPEngine(dex-zap-endpoint)) and the chains/dexvm atomic proxy
// relay connect to. It is the production form of the capstone venue proof
// (pkg/dchain/localnet_e2e_test.go), promoted from a test harness to a daemon:
// Package standalone runs the D-Chain DEX VENUE as a long-lived ZAP server on a
// fixed TCP address — the dial-able endpoint the 0x9010 EVM precompile
// (NewZAPEngine(dex-zap-endpoint)) and the chains/dexvm atomic proxy relay
// connect to. It:
//
// - opens the authoritative on-disk badger/zapdb (real persisted chainstate),
// - serves the FROZEN clob_* surface (RegisterCLOB) over rpc.Listen,
// - serves the FROZEN dex_* surface (RegisterDEX) over rpc.Listen,
// - runs the consensus sealer loop (WaitForEvent -> BuildBlock -> Verify ->
// Accept), so every write crosses the full match -> Verify -> Accept path
// and the bytes a caller gets back are consensus-computed fills,
// - exposes read-only clob_depth + clob_balance observation methods for
// - exposes read-only dex_depth + dex_balance observation methods for the
// settled book and custody state (reads need no consensus round-trip).
//
// This is the ONE venue binary. CGO_ENABLED is the single axis that picks the
// matcher: pkg/lx links the GPU matcher under cgo (amm_gpu_cuda / amm_gpu_metal
// / orderbook_cuda) and the pure-Go CPU matcher without it (amm_nogpu /
// orderbook_cuda_stub). The main is matcher-agnostic — it drives dchain.VM
// either way; engineName (engine_{cgo,nocgo}.go) only labels the active build.
package main
// CGO_ENABLED is the single axis that picks the matcher: pkg/lx links the GPU
// matcher under cgo (amm_gpu_cuda / amm_gpu_metal / orderbook_cuda) and the
// pure-Go CPU matcher without it (amm_nogpu / orderbook_cuda_stub). This package
// is matcher-agnostic — it drives dchain.VM either way; engineName
// (engine_{cgo,nocgo}.go) only labels the active build.
package standalone
import (
"context"
@@ -40,21 +39,24 @@ import (
"github.com/luxfi/rpc"
)
// clobDepthMethod is the read-only book-observation method this daemon adds on
// top of the four frozen clob_* write methods. Request: poolId[32]. Response:
// dexDepthMethod is the read-only book-observation method this daemon adds on
// top of the four frozen dex_* write methods. Request: poolId[32]. Response:
// orders[4] | remaining[f64:8] | bestBid[f64:8] | bestAsk[f64:8] | found[1].
const clobDepthMethod = "dex_depth"
const dexDepthMethod = "dex_depth"
// clobBalanceMethod is the read-only custody-balance observation method. It lets
// dexBalanceMethod is the read-only custody-balance observation method. It lets
// the bring-up harness watch funds DEPOSIT into the book, move on a fill, and
// WITHDRAW out — the "money lives in the order book" proof. Request: user[16] +
// asset[8]. Response: available[8] | locked[8] (big-endian uint64 atomic units).
const clobBalanceMethod = "dex_balance"
const dexBalanceMethod = "dex_balance"
func main() {
addr := flag.String("addr", "127.0.0.1:9099", "ZAP listen address for the venue")
dir := flag.String("db", "/tmp/dchain_venue_db", "on-disk zapdb directory (authoritative chainstate)")
flag.Parse()
// Run serves the standalone venue. args is the post-subcommand argv
// (os.Args[2:] from `dexd run`/`dexd standalone`); it carries -addr and -db.
func Run(args []string) {
fs := flag.NewFlagSet("dexd run", flag.ExitOnError)
addr := fs.String("addr", "127.0.0.1:9099", "ZAP listen address for the venue")
dir := fs.String("db", "/tmp/dchain_venue_db", "on-disk zapdb directory (authoritative chainstate)")
_ = fs.Parse(args)
logger := log.Root()
@@ -83,16 +85,16 @@ func main() {
_ = db.Close()
os.Exit(1)
}
if err := vm.RegisterCLOB(server); err != nil {
logger.Error("RegisterCLOB", "err", err)
if err := vm.RegisterDEX(server); err != nil {
logger.Error("RegisterDEX", "err", err)
os.Exit(1)
}
if err := server.RegisterRaw(clobDepthMethod, depthHandler(vm)); err != nil {
logger.Error("RegisterRaw clob_depth", "err", err)
if err := server.RegisterRaw(dexDepthMethod, depthHandler(vm)); err != nil {
logger.Error("RegisterRaw dex_depth", "err", err)
os.Exit(1)
}
if err := server.RegisterRaw(clobBalanceMethod, balanceHandler(vm)); err != nil {
logger.Error("RegisterRaw clob_balance", "err", err)
if err := server.RegisterRaw(dexBalanceMethod, balanceHandler(vm)); err != nil {
logger.Error("RegisterRaw dex_balance", "err", err)
os.Exit(1)
}
@@ -151,7 +153,7 @@ func sealer(ctx context.Context, vm *dchain.VM, logger log.Logger) {
}
}
// depthHandler serves clob_depth: a read-only observation of a market's settled
// depthHandler serves dex_depth: a read-only observation of a market's settled
// resting book. It reads the in-RAM book via the VM's BookDepth accessor (a fold
// of committed rows) — no consensus round-trip.
func depthHandler(vm *dchain.VM) rpc.RawHandler {
@@ -171,7 +173,7 @@ func depthHandler(vm *dchain.VM) rpc.RawHandler {
}
}
// balanceHandler serves clob_balance: a read-only observation of an account's
// balanceHandler serves dex_balance: a read-only observation of an account's
// custody balances for an asset (available + locked), read from the durable
// ledger via the VM's Balance accessor. Request: user[16] + asset[32]. Response:
// available[8] | locked[8].
+5 -5
View File
@@ -17,15 +17,15 @@ import (
// venue_ws.go is the WebSocket trading FRONT-END onto THE matcher — the WS
// counterpart of the FIX acceptor (pkg/fix) and the 0x9010 precompile. It does
// ONE thing: accept a JSON order over /ws, translate it into a venue.Venue call
// (the SAME clob_* surface ZAP/FIX/precompile use), and stream the
// (the SAME dex_* surface ZAP/FIX/precompile use), and stream the
// consensus-computed fills back as JSON. It owns NO matcher and NO book — that is
// the whole point of the decomplection: every access path is a thin wire dialect
// onto the single D-Chain CLOB.
// onto the single D-Chain DEX.
//
// This is deliberately separate from WebSocketServer (this file), which drives the
// in-process lx.TradingEngine for the rich margin/lending/vault product surface.
// The two are orthogonal: WebSocketServer is the product API; VenueWS is the
// on-chain CLOB trading path. Mixing the two matchers into one handler would braid
// on-chain DEX trading path. Mixing the two matchers into one handler would braid
// "what the product does" with "where the order book lives"; keeping VenueWS a
// focused Venue translator keeps the on-chain path identical across all four
// access methods.
@@ -42,7 +42,7 @@ func NewVenueWS(v venue.Venue) *VenueWS {
return &VenueWS{
venue: v,
upgrader: websocket.Upgrader{
// The CLOB trading path serves both browsers (allow-listed origins, the
// The DEX trading path serves both browsers (allow-listed origins, the
// CSRF surface) and non-browser trading clients (server-to-server, CLI,
// the SDK) that send NO Origin header. An absent Origin is therefore
// allowed (there is no browser to forge a cross-site request); a present
@@ -56,7 +56,7 @@ func NewVenueWS(v venue.Venue) *VenueWS {
}
// venueOrder is the inbound JSON order over the WS venue path. It mirrors the FIX
// NewOrderSingle fields and the clob_submit frame: a marketable order bounded by
// NewOrderSingle fields and the dex_submit frame: a marketable order bounded by
// an optional limit price that crosses the resting book.
type venueOrder struct {
Type string `json:"type"` // "submit" (marketable) — the cross primitive
+45 -45
View File
@@ -42,7 +42,7 @@ const (
TradeWireSize = 48 // Trade: id(8) + price(8) + size(8) + buyer(8) + seller(8) + ts(8)
)
// CLOB market-routed method names, frame sizes, status bytes, and FillWireSize
// DEX market-routed method names, frame sizes, status bytes, and FillWireSize
// are the FROZEN wire frame defined ONCE in github.com/luxfi/dex/pkg/zapwire —
// a pure-Go leaf so the public EVM precompile can import them without dragging
// this cgo/GPU engine. They are re-exported here as package-local aliases so
@@ -56,27 +56,27 @@ const (
// PoolKey). There is exactly one matcher (*lx.OrderBook); two wire framings for
// two callers (maker = 8-byte symbol, precompile/proxy = 32-byte poolId).
const (
CLOBMethodEnsureMarket = zapwire.MethodEnsureMarket
CLOBMethodPlace = zapwire.MethodPlace
CLOBMethodCancel = zapwire.MethodCancel
CLOBMethodSubmit = zapwire.MethodSubmit
DEXMethodEnsureMarket = zapwire.MethodEnsureMarket
DEXMethodPlace = zapwire.MethodPlace
DEXMethodCancel = zapwire.MethodCancel
DEXMethodSubmit = zapwire.MethodSubmit
)
// CLOB ack status bytes (shared with the legacy ack/reject codec).
// DEX ack status bytes (shared with the legacy ack/reject codec).
const (
clobStatusPlaced = zapwire.StatusPlaced
clobStatusCanceled = zapwire.StatusCanceled
clobStatusRejected = zapwire.StatusRejected
dexStatusPlaced = zapwire.StatusPlaced
dexStatusCanceled = zapwire.StatusCanceled
dexStatusRejected = zapwire.StatusRejected
)
// FillWireSize is one fill in a clob_submit response. See zapwire.
// FillWireSize is one fill in a dex_submit response. See zapwire.
const FillWireSize = zapwire.FillWireSize
// ZAPServer provides ultra-low-latency order handling for HFT.
//
// It is multi-market: legacy symbol-keyed methods (place_order/…) operate on a
// default book (or a per-symbol book when a symbol is supplied), and the
// poolId-keyed CLOB methods (clob_*) operate on per-market books keyed by the
// poolId-keyed DEX methods (dex_*) operate on per-market books keyed by the
// 32-byte V4 poolId. Every method routes to the same *lx.OrderBook matcher.
type ZAPServer struct {
// orderBook is the default (single) book the legacy symbol-keyed handlers
@@ -86,7 +86,7 @@ type ZAPServer struct {
logger log.Logger
addr string
// markets holds per-poolId books for the CLOB (clob_*) surface. Keyed by the
// markets holds per-poolId books for the DEX (dex_*) surface. Keyed by the
// raw 32-byte poolId.
//
// This is the GATEWAY to the d-chain: the in-memory book here is the
@@ -145,7 +145,7 @@ func (s *ZAPServer) market(id [32]byte, createIfAbsent bool) *lx.OrderBook {
return ob
}
// MarketCount returns the number of canonical CLOB markets held server-side.
// MarketCount returns the number of canonical DEX markets held server-side.
// Diagnostic: proves resting-order state lives here, not in the adapter.
func (s *ZAPServer) MarketCount() int {
s.marketsMu.Lock()
@@ -196,11 +196,11 @@ func (s *ZAPServer) register(server rpc.Server) error {
{"best_ask", s.handleGetBestAsk},
{"orderbook", s.handleGetOrderBook},
{"order", s.handleGetOrder},
// CLOB poolId-keyed surface (per-market books) — the V4 precompile path.
{CLOBMethodEnsureMarket, s.handleEnsureMarket},
{CLOBMethodPlace, s.handleCLOBPlace},
{CLOBMethodCancel, s.handleCLOBCancel},
{CLOBMethodSubmit, s.handleCLOBSubmit},
// DEX poolId-keyed surface (per-market books) — the V4 precompile path.
{DEXMethodEnsureMarket, s.handleEnsureMarket},
{DEXMethodPlace, s.handleDEXPlace},
{DEXMethodCancel, s.handleDEXCancel},
{DEXMethodSubmit, s.handleDEXSubmit},
}
for _, r := range raw {
if err := server.RegisterRaw(r.method, r.handler); err != nil {
@@ -210,19 +210,19 @@ func (s *ZAPServer) register(server rpc.Server) error {
return nil
}
// RegisterCLOB registers ONLY the poolId-keyed CLOB handlers on an rpc.Server.
// RegisterDEX registers ONLY the poolId-keyed DEX handlers on an rpc.Server.
// It is the integration seam the LP-9010 precompile tests use to stand up the
// real matching engine without the legacy single-book surface. Additive: the
// caller may register other handlers on the same server.
func RegisterCLOB(server rpc.Server, s *ZAPServer) error {
func RegisterDEX(server rpc.Server, s *ZAPServer) error {
regs := []struct {
method string
handler rpc.RawHandler
}{
{CLOBMethodEnsureMarket, s.handleEnsureMarket},
{CLOBMethodPlace, s.handleCLOBPlace},
{CLOBMethodCancel, s.handleCLOBCancel},
{CLOBMethodSubmit, s.handleCLOBSubmit},
{DEXMethodEnsureMarket, s.handleEnsureMarket},
{DEXMethodPlace, s.handleDEXPlace},
{DEXMethodCancel, s.handleDEXCancel},
{DEXMethodSubmit, s.handleDEXSubmit},
}
for _, r := range regs {
if err := server.RegisterRaw(r.method, r.handler); err != nil {
@@ -254,7 +254,7 @@ func (s *ZAPServer) Stats() (orders, trades, cancels uint64) {
}
// =========================================================================
// CLOB (poolId-keyed) handlers — the V4 PoolManager facade.
// DEX (poolId-keyed) handlers — the V4 PoolManager facade.
// =========================================================================
// handleEnsureMarket: payload = poolId[32]. Idempotently creates the market
@@ -266,10 +266,10 @@ func (s *ZAPServer) handleEnsureMarket(_ context.Context, payload []byte) ([]byt
var id [32]byte
copy(id[:], payload[0:32])
s.market(id, true) // create if absent
return s.encodeAck(0, clobStatusPlaced, s.sequence.Add(1)), nil
return s.encodeAck(0, dexStatusPlaced, s.sequence.Add(1)), nil
}
// handleCLOBPlace places a RESTING limit order on a poolId market — the CLOB
// handleDEXPlace places a RESTING limit order on a poolId market — the DEX
// meaning of V4 modifyLiquidity(+delta). Payload (65 bytes):
//
// [0:32] poolId
@@ -279,10 +279,10 @@ func (s *ZAPServer) handleEnsureMarket(_ context.Context, payload []byte) ([]byt
// [49:65] user (16 bytes, null-padded)
//
// Returns ack(orderId, placed). A rest order never crosses here: the precompile
// places liquidity, takers cross it via clob_submit.
func (s *ZAPServer) handleCLOBPlace(_ context.Context, payload []byte) ([]byte, error) {
// places liquidity, takers cross it via dex_submit.
func (s *ZAPServer) handleDEXPlace(_ context.Context, payload []byte) ([]byte, error) {
if len(payload) < 65 {
return s.encodeReject(0, "clob_place: short payload"), nil
return s.encodeReject(0, "dex_place: short payload"), nil
}
var id [32]byte
copy(id[:], payload[0:32])
@@ -292,7 +292,7 @@ func (s *ZAPServer) handleCLOBPlace(_ context.Context, payload []byte) ([]byte,
user := string(trimNull(payload[49:65]))
if price <= 0 || size <= 0 {
return s.encodeReject(0, "clob_place: invalid price or size"), nil
return s.encodeReject(0, "dex_place: invalid price or size"), nil
}
ob := s.market(id, true)
@@ -307,17 +307,17 @@ func (s *ZAPServer) handleCLOBPlace(_ context.Context, payload []byte) ([]byte,
}
orderID := ob.AddOrder(order)
if orderID == 0 {
return s.encodeReject(0, "clob_place: order rejected"), nil
return s.encodeReject(0, "dex_place: order rejected"), nil
}
s.ordersProcessed.Add(1)
return s.encodeAck(orderID, clobStatusPlaced, s.sequence.Add(1)), nil
return s.encodeAck(orderID, dexStatusPlaced, s.sequence.Add(1)), nil
}
// handleCLOBCancel cancels a resting order on a poolId market — the CLOB meaning
// handleDEXCancel cancels a resting order on a poolId market — the DEX meaning
// of V4 modifyLiquidity(-delta). Payload (40 bytes): poolId[32] + orderId[8].
func (s *ZAPServer) handleCLOBCancel(_ context.Context, payload []byte) ([]byte, error) {
func (s *ZAPServer) handleDEXCancel(_ context.Context, payload []byte) ([]byte, error) {
if len(payload) < 40 {
return s.encodeReject(0, "clob_cancel: short payload"), nil
return s.encodeReject(0, "dex_cancel: short payload"), nil
}
var id [32]byte
copy(id[:], payload[0:32])
@@ -325,16 +325,16 @@ func (s *ZAPServer) handleCLOBCancel(_ context.Context, payload []byte) ([]byte,
ob := s.market(id, false)
if ob == nil {
return s.encodeReject(orderID, "clob_cancel: unknown market"), nil
return s.encodeReject(orderID, "dex_cancel: unknown market"), nil
}
if err := ob.CancelOrder(orderID); err != nil {
return s.encodeReject(orderID, err.Error()), nil
}
s.cancelProcessed.Add(1)
return s.encodeAck(orderID, clobStatusCanceled, s.sequence.Add(1)), nil
return s.encodeAck(orderID, dexStatusCanceled, s.sequence.Add(1)), nil
}
// handleCLOBSubmit submits a MARKETABLE order against a poolId market — the CLOB
// handleDEXSubmit submits a MARKETABLE order against a poolId market — the DEX
// meaning of V4 swap. It crosses the resting book and returns the resulting
// fills; the adapter derives the BalanceDelta from those fills alone. Payload
// (66 bytes):
@@ -347,9 +347,9 @@ func (s *ZAPServer) handleCLOBCancel(_ context.Context, payload []byte) ([]byte,
// [50:66] user (16 bytes, null-padded)
//
// Response: fillCount[4] then fillCount × (price[8] + size[8] + takerSide[1]).
func (s *ZAPServer) handleCLOBSubmit(_ context.Context, payload []byte) ([]byte, error) {
func (s *ZAPServer) handleDEXSubmit(_ context.Context, payload []byte) ([]byte, error) {
if len(payload) < 66 {
return nil, fmt.Errorf("clob_submit: short payload: %d", len(payload))
return nil, fmt.Errorf("dex_submit: short payload: %d", len(payload))
}
var id [32]byte
copy(id[:], payload[0:32])
@@ -360,12 +360,12 @@ func (s *ZAPServer) handleCLOBSubmit(_ context.Context, payload []byte) ([]byte,
user := string(trimNull(payload[50:66]))
if size <= 0 {
return nil, fmt.Errorf("clob_submit: non-positive size")
return nil, fmt.Errorf("dex_submit: non-positive size")
}
ob := s.market(id, false)
if ob == nil {
return nil, fmt.Errorf("clob_submit: unknown market")
return nil, fmt.Errorf("dex_submit: unknown market")
}
order := &lx.Order{
@@ -379,7 +379,7 @@ func (s *ZAPServer) handleCLOBSubmit(_ context.Context, payload []byte) ([]byte,
order.Type = lx.Market
} else {
if limitPrice <= 0 {
return nil, fmt.Errorf("clob_submit: IOC limit needs positive price")
return nil, fmt.Errorf("dex_submit: IOC limit needs positive price")
}
order.Type = lx.Limit
order.Price = limitPrice
@@ -387,7 +387,7 @@ func (s *ZAPServer) handleCLOBSubmit(_ context.Context, payload []byte) ([]byte,
fills, err := ob.SubmitMarketable(order)
if err != nil {
return nil, fmt.Errorf("clob_submit: %w", err)
return nil, fmt.Errorf("dex_submit: %w", err)
}
s.tradesExecuted.Add(uint64(len(fills)))
+6 -7
View File
@@ -29,12 +29,12 @@ import (
// the zapwire user[16] field the wire carries and the ledger keys balances by.
type testAcct struct {
scheme lx.AuthScheme
priv *ecdsa.PrivateKey // secp256k1 (nil for PQ)
mlpriv *mldsa.PrivateKey // ML-DSA-65 (nil for classical)
pubKey []byte // ML-DSA pubkey bytes (nil for secp)
account userKey // 16-byte settlement account
user string // string(account[:]) — the wire user
nonce uint64 // next nonce to use
priv *ecdsa.PrivateKey // secp256k1 (nil for PQ)
mlpriv *mldsa.PrivateKey // ML-DSA-65 (nil for classical)
pubKey []byte // ML-DSA pubkey bytes (nil for secp)
account userKey // 16-byte settlement account
user string // string(account[:]) — the wire user
nonce uint64 // next nonce to use
}
// authFor produces an authorization envelope binding (type, nonce, body) under
@@ -165,4 +165,3 @@ func signedPayload(t *testing.T, name string, typ TxType, body []byte) []byte {
out = append(out, body...)
return append(out, auth.encode()...)
}
+1 -1
View File
@@ -484,7 +484,7 @@ func (b *Block) execute(ctx context.Context, overlay *versiondb.Database) (execR
// cannot be funded, the order is REJECTED here — it never matches, never
// rests. (A market with unbound assets falls back to the no-custody path so
// asset-less test/legacy markets still function; the live custody e2e binds
// assets via clob_open_market.)
// assets via dex_open_market.)
base, quote, assetsBound, aerr := readMarketAssets(overlay, poolID)
if aerr != nil {
return execResult{}, aerr
+17 -17
View File
@@ -11,20 +11,20 @@
// custody balance change, with GLOBAL value conservation (I = available + locked)
// intact across all four crosses.
//
// THE FOUR PATHS — each is a thin wire dialect onto the SAME clob_* matcher
// THE FOUR PATHS — each is a thin wire dialect onto the SAME dex_* matcher
// surface (pkg/zapwire), never its own matcher:
//
// 1. V4 precompile (0x9010): the engine_zap boundary — the EXACT 66-byte
// clob_submit frame lux/precompile/dex/engine_zap.go Swap() builds (the V4
// zeroForOne/AmountSpecified/SqrtPriceLimit -> CLOB side/size/limit mapping),
// dex_submit frame lux/precompile/dex/engine_zap.go Swap() builds (the V4
// zeroForOne/AmountSpecified/SqrtPriceLimit -> DEX side/size/limit mapping),
// submitted to the venue, with the V4 BalanceDelta derived from the
// consensus fills exactly as fillsToDelta does. See v4SubmitFrame /
// v4BalanceDelta below and the NOTE on what a live luxd EVM adds.
// 2. ZAP native: pkg/zapwire clob_* frames over the venue's rpc.Listen socket —
// 2. ZAP native: pkg/zapwire dex_* frames over the venue's rpc.Listen socket —
// the existing e2eClient.submit path.
// 3. FIX: the new pkg/fix acceptor — a real TCP FIX 4.4 session (Logon,
// NewOrderSingle, ExecutionReport) whose order routes to the SAME venue via
// the venue.Venue (clob_*) seam.
// the venue.Venue (dex_*) seam.
// 4. WS: the new pkg/api VenueWS handler — a JSON order over a real
// gorilla/websocket /ws connection routed to the SAME venue via venue.Venue.
//
@@ -59,7 +59,7 @@ import (
)
// signingVenue is a test gateway that implements dexvenue.Venue by emitting
// SIGNED clob_* frames over its own ZAP connection. The real zapVenue builds
// SIGNED dex_* frames over its own ZAP connection. The real zapVenue builds
// UNSIGNED frames; the d-chain authorization gate now refuses those, so a
// protocol front-end (FIX/WS) must present a frame signed by the authenticated
// session's account. This models the production reality where the gateway holds a
@@ -201,7 +201,7 @@ func (c *fixClient) logon(t *testing.T) {
}
}
// custody seeds a deposit over the venue's ZAP socket (the SAME clob_deposit wire
// custody seeds a deposit over the venue's ZAP socket (the SAME dex_deposit wire
// path the chains/dexvm proxy uses). Uses contentRef so a retry would dedup.
func (c *e2eClient) deposit(t *testing.T, user string, asset [32]byte, amount uint64) {
t.Helper()
@@ -209,28 +209,28 @@ func (c *e2eClient) deposit(t *testing.T, user string, asset [32]byte, amount ui
body := zapwire.EncodeDeposit(wireUser(t, user), asset, amount, ref)
resp, err := c.conn.Call(c.ctx, zapwire.MethodDeposit, signedPayload(t, user, TxDeposit, body))
if err != nil {
t.Fatalf("clob_deposit %s %d: %v", user, amount, err)
t.Fatalf("dex_deposit %s %d: %v", user, amount, err)
}
status, credited, derr := zapwire.DecodeBalanceResp(resp)
if derr != nil || status != zapwire.StatusPlaced || credited != amount {
t.Fatalf("clob_deposit %s %d: status=%d credited=%d err=%v", user, amount, status, credited, derr)
t.Fatalf("dex_deposit %s %d: status=%d credited=%d err=%v", user, amount, status, credited, derr)
}
}
// openMarket binds a market's (base, quote) asset ids over ZAP (clob_open_market)
// openMarket binds a market's (base, quote) asset ids over ZAP (dex_open_market)
// so the venue knows which asset each order locks/settles.
func (c *e2eClient) openMarket(t *testing.T, pool, base, quote [32]byte) {
t.Helper()
resp, err := c.conn.Call(c.ctx, zapwire.MethodOpenMarket, zapwire.EncodeOpenMarket(pool, base, quote))
if err != nil {
t.Fatalf("clob_open_market: %v", err)
t.Fatalf("dex_open_market: %v", err)
}
if len(resp) < 9 || resp[8] != zapwire.StatusPlaced {
t.Fatalf("clob_open_market not placed: resp=%x", resp)
t.Fatalf("dex_open_market not placed: resp=%x", resp)
}
}
// v4SubmitFrame builds the EXACT clob_submit payload lux/precompile/dex
// v4SubmitFrame builds the EXACT dex_submit payload lux/precompile/dex
// engine_zap.go Swap() emits for a marketable order, so the V4 leg exercises the
// real engine_zap WIRE CONTRACT (the frozen 66-byte frame), not a re-derivation:
//
@@ -436,9 +436,9 @@ func TestFourPathTradingE2E(t *testing.T) {
t.Logf("maker rested %d asks of %g LUX @ {10,20,30,40} LUSD (%d LUX locked)", len(legs), askSize, len(legs))
// --- stand up the FIX acceptor and the WS venue front-end, BOTH onto the SAME
// venue via the venue.Venue (clob_*) seam — one matcher, many protocols. The
// venue via the venue.Venue (dex_*) seam — one matcher, many protocols. The
// gateway holds the per-session signer: a signingVenue translates a protocol
// order into a SIGNED clob_* frame on the authenticated session's behalf (the
// order into a SIGNED dex_* frame on the authenticated session's behalf (the
// FIX SenderCompID / WS user is the account), so the d-chain gate authorizes it.
fixVenue := newSigningVenue(t, v.addr)
defer fixVenue.close()
@@ -497,7 +497,7 @@ func TestFourPathTradingE2E(t *testing.T) {
}
// ===== PATH 1: V4 precompile (engine_zap boundary) =====
// Build the EXACT engine_zap clob_submit frame and submit it to the venue. A
// Build the EXACT engine_zap dex_submit frame and submit it to the venue. A
// BUY of 1 LUX is !zeroForOne, AmountSpecified magnitude 1, a bounded
// (non-market) IOC limited at the leg price. Assert the frame is byte-identical
// to zapwire.EncodeSubmit (the frozen-frame three-homes guarantee), then derive
@@ -518,7 +518,7 @@ func TestFourPathTradingE2E(t *testing.T) {
resp, err := cli.conn.Call(cli.ctx, zapwire.MethodSubmit,
signedPayload(t, dexvenue.UserHandle(l.user), TxSubmit, frame))
if err != nil {
t.Fatalf("V4 clob_submit: %v", err)
t.Fatalf("V4 dex_submit: %v", err)
}
fills, err := zapwire.DecodeFills(resp)
if err != nil {
+12 -12
View File
@@ -11,7 +11,7 @@ import (
"github.com/luxfi/rpc"
)
// handler.go is the d-chain's CLOB ZAP surface: the clob_* methods a client (the
// handler.go is the d-chain's DEX ZAP surface: the dex_* methods a client (the
// chains/dexvm proxy relay, the EVM precompile adapter, the maker) calls over
// github.com/luxfi/rpc. It is the gateway INTO consensus, not a matcher.
//
@@ -25,28 +25,28 @@ import (
// rows), needing no consensus round-trip.
//
// WIRE PARITY: every frame here is the FROZEN form from pkg/zapwire — the same
// method names (clob_ensure_market/place/cancel/submit), the same request sizes,
// method names (dex_ensure_market/place/cancel/submit), the same request sizes,
// and the same response codecs (zapwire.EncodeAck / zapwire.EncodeFills, 17-byte
// fill). The chains/dexvm relay client and the precompile adapter re-define the
// identical constants (they cannot import this cgo-tagged package), so a frame
// built by any of them is byte-identical to what this server consumes/produces.
// clobMethod pairs a frozen ZAP method name with its raw handler. The handler
// dexMethod pairs a frozen ZAP method name with its raw handler. The handler
// signature is github.com/luxfi/rpc.RawHandler (func(ctx, payload) (resp, err)) —
// the SAME func value whether it is reached over the ZAP socket transport
// (RegisterCLOB) or over the node's HTTP router (CreateHandlers / ingest.go). One
// (RegisterDEX) or over the node's HTTP router (CreateHandlers / ingest.go). One
// table, two transports: the ingestion seam never duplicates the handler set.
type clobMethod struct {
type dexMethod struct {
method string
handler rpc.RawHandler
}
// clobMethods is the single authoritative method->handler table for the CLOB
// surface. Both transports fold over it: RegisterCLOB registers each on a ZAP
// dexMethods is the single authoritative method->handler table for the DEX
// surface. Both transports fold over it: RegisterDEX registers each on a ZAP
// rpc.Server; the HTTP mux (ingest.go) routes POST .../<method> to the same
// handler. A method added here is reachable over both transports automatically.
func (vm *VM) clobMethods() []clobMethod {
return []clobMethod{
func (vm *VM) dexMethods() []dexMethod {
return []dexMethod{
{zapwire.MethodEnsureMarket, vm.handleEnsureMarket},
{zapwire.MethodPlace, vm.handlePlace},
{zapwire.MethodCancel, vm.handleCancel},
@@ -57,13 +57,13 @@ func (vm *VM) clobMethods() []clobMethod {
}
}
// RegisterCLOB wires the clob_* handlers onto an rpc.Server (the ZAP socket
// RegisterDEX wires the dex_* handlers onto an rpc.Server (the ZAP socket
// transport). It is the seam the standalone venue entrypoint (cmd/dvenue) and the
// socket-level tests use. The in-luxd plugin does NOT use this path — it exposes
// the SAME handlers over the node's HTTP router via CreateHandlers (ingest.go).
// Additive: the caller may register other methods on the same server.
func (vm *VM) RegisterCLOB(server rpc.Server) error {
for _, m := range vm.clobMethods() {
func (vm *VM) RegisterDEX(server rpc.Server) error {
for _, m := range vm.dexMethods() {
if err := server.RegisterRaw(m.method, m.handler); err != nil {
return fmt.Errorf("dchain: register %s: %w", m.method, err)
}
+7 -7
View File
@@ -19,7 +19,7 @@ import (
"github.com/luxfi/rpc"
)
// handler_test.go proves the d-chain CLOB ZAP surface end to end over a REAL
// handler_test.go proves the d-chain DEX ZAP surface end to end over a REAL
// rpc.Listen socket, using the SAME client (rpc.ZAPDial) the chains/dexvm proxy
// relay uses, and decoding responses with a locally-redefined framing that is
// byte-identical to chains/dexvm/relay.go. This is the step-4 proxy-boundary
@@ -28,7 +28,7 @@ import (
// Because a write blocks until consensus accepts it, the test runs an auto-sealer
// goroutine that plays the consensus engine: on a PendingTxs signal it drains the
// mempool via BuildBlock -> Verify -> Accept. This is the harness standing in for
// the real engine that cmd/dchain wires via rpc.Serve.
// the real engine that cmd/dexd (plugin mode) wires via rpc.Serve.
// ---- relay-side framing, redefined locally to prove byte-parity (must equal
// chains/dexvm/relay.go exactly: methods, FillWireSize=17, count[4]+N*17) ----
@@ -50,7 +50,7 @@ type relayFill struct {
// relayDecodeFills mirrors chains/dexvm/relay.go::DecodeFills byte for byte,
// including the finite-positive and side∈{0,1} validation, so a successful decode
// here proves the d-chain's clob_submit response is exactly what the proxy parses.
// here proves the d-chain's dex_submit response is exactly what the proxy parses.
func relayDecodeFills(data []byte) ([]relayFill, error) {
if len(data) < 4 {
return nil, fmt.Errorf("fills response too short: %d", len(data))
@@ -122,7 +122,7 @@ func autoSealer(ctx context.Context, t *testing.T, vm *VM) {
}
}
// startServer brings up a VM + a real rpc.Listen ZAP server with the clob_*
// startServer brings up a VM + a real rpc.Listen ZAP server with the dex_*
// handlers, plus the auto-sealer. Returns the dial address and a stop func.
func startServer(t *testing.T) (*VM, string, func()) {
t.Helper()
@@ -141,8 +141,8 @@ func startServer(t *testing.T) (*VM, string, func()) {
if err != nil {
t.Fatalf("rpc.Listen: %v", err)
}
if err := vm.RegisterCLOB(server); err != nil {
t.Fatalf("RegisterCLOB: %v", err)
if err := vm.RegisterDEX(server); err != nil {
t.Fatalf("RegisterDEX: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
@@ -244,7 +244,7 @@ func TestHandlerByteParityOverSocket(t *testing.T) {
}
// TestHandlerIdempotentReplay proves the d-chain STATE idempotency: replaying the
// identical clob_submit frame (a proxy retry on a dropped socket) executes the
// identical dex_submit frame (a proxy retry on a dropped socket) executes the
// order EXACTLY ONCE — the second call returns zero fills, and the resting book
// is consumed only once.
func TestHandlerIdempotentReplay(t *testing.T) {
+14 -14
View File
@@ -12,7 +12,7 @@ import (
"github.com/luxfi/rpc"
)
// ingest.go is the D-Chain CLOB ingestion seam for the IN-LUXD native VM. When
// ingest.go is the D-Chain DEX ingestion seam for the IN-LUXD native VM. When
// the VM runs as a luxd plugin (cmd/dchain via github.com/luxfi/vm/rpc.Serve),
// luxd asks the VM for HTTP handlers (CreateHandlers); the plugin transport stands
// up a local http.Server per handler inside the plugin process and the node
@@ -21,23 +21,23 @@ import (
// `r.router.Handle(base+endpoint, handler)` — exact path, no subtree). So we
// return ONE handler per method, each keyed by its full sub-path:
//
// /ext/bc/D/dex/clob_place -> handlePlace
// /ext/bc/D/dex/clob_submit -> handleSubmit
// /ext/bc/D/dex/clob_cancel -> handleCancel
// /ext/bc/D/dex/dex_place -> handlePlace
// /ext/bc/D/dex/dex_submit -> handleSubmit
// /ext/bc/D/dex/dex_cancel -> handleCancel
// ...
//
// The request body is the FROZEN zapwire payload verbatim; the response body is
// the handler's raw bytes (zapwire ack / fills / balance frame).
//
// THE INVARIANT THAT MATTERS: this is pure transport. The clob_* handler each
// route dispatches to is the EXACT func value RegisterCLOB binds on the ZAP socket
// (vm.clobMethods) — so an order POSTed here flows submitTx -> mempool.Add ->
// THE INVARIANT THAT MATTERS: this is pure transport. The dex_* handler each
// route dispatches to is the EXACT func value RegisterDEX binds on the ZAP socket
// (vm.dexMethods) — so an order POSTed here flows submitTx -> mempool.Add ->
// consensus PendingTxs -> BuildBlock -> Block.Verify (the matcher) -> Block.Accept,
// and the bytes returned are the CONSENSUS-COMPUTED outcome (every validator
// re-derived the same fills at Verify). It NEVER relays to an external matcher and
// NEVER mutates the book synchronously: the chain is the matcher.
// ingestNamespace is the path segment grouping the CLOB methods under the chain
// ingestNamespace is the path segment grouping the DEX methods under the chain
// route. The full external path of a method m is /ext/bc/<chainID>/dex/<m>.
const ingestNamespace = "dex"
@@ -46,18 +46,18 @@ const ingestNamespace = "dex"
// without parsing it (defence at the boundary — the handlers also length-check).
const maxIngestBody = 4 << 10
// httpHandlers returns the VM's CLOB methods as luxd HTTP handlers, keyed by their
// full endpoint sub-path ("/dex/<method>"). It folds the SAME vm.clobMethods table
// httpHandlers returns the VM's DEX methods as luxd HTTP handlers, keyed by their
// full endpoint sub-path ("/dex/<method>"). It folds the SAME vm.dexMethods table
// the ZAP socket transport uses — one method->handler table, two transports — so a
// method added to clobMethods is reachable over HTTP automatically. luxd registers
// method added to dexMethods is reachable over HTTP automatically. luxd registers
// each key as an exact route under /ext/bc/<chainID> (see file doc).
//
// It ALSO folds the read surface (vm.readMethods, read.go): the clob_get_* query
// It ALSO folds the read surface (vm.readMethods, read.go): the dex_get_* query
// endpoints that return committed state as JSON. Reads are mounted on the same
// route group but never touch the mempool/consensus — orthogonal to the writes.
func (vm *VM) httpHandlers() map[string]http.Handler {
out := make(map[string]http.Handler, len(vm.clobMethods())+len(vm.readMethods()))
for _, m := range vm.clobMethods() {
out := make(map[string]http.Handler, len(vm.dexMethods())+len(vm.readMethods()))
for _, m := range vm.dexMethods() {
out["/"+ingestNamespace+"/"+m.method] = rawHandlerHTTP(m.handler)
}
for _, m := range vm.readMethods() {
+4 -4
View File
@@ -27,13 +27,13 @@ import (
// committed as D-Chain state (a trade:<height><seq> row). The matcher is the
// chain; there is no external venue and no relay.
//
// The transport under test is vm.clobHTTPHandler() driven through a real
// The transport under test is vm.dexHTTPHandler() driven through a real
// httptest.Server (which is exactly what the plugin transport stands up inside the
// plugin process). An auto-sealer goroutine plays the consensus engine, identical
// to handler_test.go's socket harness — proving the SAME submitTx->mempool->
// Verify-match path is reached whether the order arrives over HTTP or ZAP.
// startIngestServer brings up a VM + the in-luxd HTTP CLOB handler over a real
// startIngestServer brings up a VM + the in-luxd HTTP DEX handler over a real
// httptest.Server, plus the auto-sealer. Returns the VM, the base URL
// (.../dex), and a stop func.
func startIngestServer(t *testing.T) (*VM, string, func()) {
@@ -221,14 +221,14 @@ func TestIngestRejectsNonPost(t *testing.T) {
}
// TestIngestUnknownMethod404 proves an unrouted method path is a 404 (the mux only
// mounts the frozen clob_* methods); it must not panic or hang.
// mounts the frozen dex_* methods); it must not panic or hang.
func TestIngestUnknownMethod404(t *testing.T) {
_, base, stop := startIngestServer(t)
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, methodURL(base, "clob_bogus"), bytes.NewReader([]byte{0x00}))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, methodURL(base, "dex_bogus"), bytes.NewReader([]byte{0x00}))
if err != nil {
t.Fatalf("new request: %v", err)
}
+21 -21
View File
@@ -7,7 +7,7 @@
// Built under //go:build !cgo: this venue e2e exercises the PURE-GO consensus
// path (rebuildable book, zapdb persistence, ZAP socket, match->Verify->Accept)
// and needs no cgo/GPU matcher — the GPU accelerator is an opt-in venue
// accelerator (cmd/dvenue, //go:build cgo), and the matching CORRECTNESS proven
// accelerator (cmd/dexd run, under cgo), and the matching CORRECTNESS proven
// here is identical on the CPU and GPU paths. The !cgo gate also keeps this test
// in the package's CGO_ENABLED=0 test binary (the only one that links on a host
// without luxcpp), so `CGO_ENABLED=0 go test ./pkg/dchain` runs the full venue
@@ -18,19 +18,19 @@ package dchain
// localnet_e2e_test.go is the CAPSTONE venue-level localnet proof for the
// standalone D-Chain DEX VM. Unlike handler_test.go (memdb, single market), it
// stands the venue up exactly as cmd/dchain would at runtime:
// stands the venue up exactly as cmd/dexd would at runtime:
//
// - a REAL on-disk zapdb (the authoritative persisted chainstate, not memdb),
// - the clob_* ZAP surface served over a REAL rpc.Listen socket (the same
// - the dex_* ZAP surface served over a REAL rpc.Listen socket (the same
// transport the chains/dexvm proxy + 0x9010 precompile dial),
// - an auto-sealer goroutine playing the consensus engine (BuildBlock ->
// Verify -> Accept) — the harness that cmd/dchain's rpc.Serve gets from the
// Verify -> Accept) — the harness that cmd/dexd's plugin rpc.Serve gets from the
// node runtime,
// - the TWO canonical first-pair markets the maker seeds: LUX/LUSD and
// LUX/LETH (dex-architecture-canonical: maker seeds REAL D-Chain pools via
// resting limit orders = V4 "liquidity"),
// - an external ZAP client (rpc.ZAPDial) that SEEDS resting liquidity
// (clob_place), then crosses it with marketable takers (clob_submit), and
// (dex_place), then crosses it with marketable takers (dex_submit), and
// verifies the CONSENSUS-COMPUTED fills + the resulting book,
// - a Shutdown + restart that rebuilds the authoritative book from persisted
// rows and proves the resting liquidity SURVIVED (it is a real chain, not an
@@ -97,8 +97,8 @@ func bootVenue(t *testing.T, dir string) *venue {
if err != nil {
t.Fatalf("rpc.Listen: %v", err)
}
if err := vm.RegisterCLOB(server); err != nil {
t.Fatalf("RegisterCLOB: %v", err)
if err := vm.RegisterDEX(server); err != nil {
t.Fatalf("RegisterDEX: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
@@ -122,7 +122,7 @@ func (v *venue) stop() {
_ = v.db.Close()
}
// e2eClient is an external ZAP client speaking the FROZEN clob_* frames — the
// e2eClient is an external ZAP client speaking the FROZEN dex_* frames — the
// exact surface the chains/dexvm proxy relay and the 0x9010 precompile use.
type e2eClient struct {
conn *rpc.ZAPConn
@@ -144,10 +144,10 @@ func (c *e2eClient) ensureMarket(t *testing.T, pool [32]byte) {
t.Helper()
resp, err := c.conn.Call(c.ctx, zapwire.MethodEnsureMarket, zapwire.EncodeEnsureMarket(pool))
if err != nil {
t.Fatalf("clob_ensure_market: %v", err)
t.Fatalf("dex_ensure_market: %v", err)
}
if len(resp) < 9 || resp[8] != zapwire.StatusPlaced {
t.Fatalf("clob_ensure_market not placed: resp=%x", resp)
t.Fatalf("dex_ensure_market not placed: resp=%x", resp)
}
}
@@ -157,10 +157,10 @@ func (c *e2eClient) place(t *testing.T, pool [32]byte, side uint8, price, size f
body := zapwire.EncodePlace(pool, side, price, size, wireUser(t, user))
resp, err := c.conn.Call(c.ctx, zapwire.MethodPlace, signedPayload(t, user, TxPlace, body))
if err != nil {
t.Fatalf("clob_place %s %v@%v: %v", sideName(side), size, price, err)
t.Fatalf("dex_place %s %v@%v: %v", sideName(side), size, price, err)
}
if len(resp) < 9 || resp[8] != zapwire.StatusPlaced {
t.Fatalf("clob_place rejected %s %v@%v: resp=%x", sideName(side), size, price, resp)
t.Fatalf("dex_place rejected %s %v@%v: resp=%x", sideName(side), size, price, resp)
}
return binary.BigEndian.Uint64(resp[0:8])
}
@@ -172,11 +172,11 @@ func (c *e2eClient) submit(t *testing.T, pool [32]byte, side uint8, limit, size
body := zapwire.EncodeSubmit(pool, side, false, limit, size, wireUser(t, user))
resp, err := c.conn.Call(c.ctx, zapwire.MethodSubmit, signedPayload(t, user, TxSubmit, body))
if err != nil {
t.Fatalf("clob_submit %s %v@%v: %v", sideName(side), size, limit, err)
t.Fatalf("dex_submit %s %v@%v: %v", sideName(side), size, limit, err)
}
fills, err := zapwire.DecodeFills(resp)
if err != nil {
t.Fatalf("clob_submit fills decode (BYTE PARITY): %v (resp=%x)", err, resp)
t.Fatalf("dex_submit fills decode (BYTE PARITY): %v (resp=%x)", err, resp)
}
return fills
}
@@ -203,11 +203,11 @@ func (v *venue) restingByMarket(pool [32]byte) (orders int, remaining float64) {
}
// TestLocalnetVenueE2E is the capstone venue bring-up + on-chain e2e:
// 1. boot the venue on an on-disk zapdb (real persisted chainstate),
// 2. seed LUX/LUSD and LUX/LETH with resting maker liquidity (clob_place),
// 3. cross both books with marketable takers (clob_submit),
// 4. assert the consensus-computed fills are correct + books updated,
// 5. restart the venue and prove the resting liquidity SURVIVED (real chain).
// 1. boot the venue on an on-disk zapdb (real persisted chainstate),
// 2. seed LUX/LUSD and LUX/LETH with resting maker liquidity (dex_place),
// 3. cross both books with marketable takers (dex_submit),
// 4. assert the consensus-computed fills are correct + books updated,
// 5. restart the venue and prove the resting liquidity SURVIVED (real chain).
func TestLocalnetVenueE2E(t *testing.T) {
dir := t.TempDir()
@@ -220,7 +220,7 @@ func TestLocalnetVenueE2E(t *testing.T) {
leth := poolIDForSymbol("LUX/LETH")
// --- maker seeding: ensure markets, then rest two-sided liquidity. This is
// what `maker -dex <endpoint>` does for the first pairs, in clob_* form. ---
// what `maker -dex <endpoint>` does for the first pairs, in dex_* form. ---
cli.ensureMarket(t, lusd)
cli.ensureMarket(t, leth)
t.Logf("markets ensured: LUX/LUSD, LUX/LETH")
@@ -272,7 +272,7 @@ func TestLocalnetVenueE2E(t *testing.T) {
t.Logf("LUX/LUSD post-cross: 23.0 resting (3@101 ask + 20 bid) — consensus-settled")
// --- on-chain cross #2: a 5-unit sell @0.049 on LUX/LETH hits the [email protected]
// bid (one fill of 3), residual 2 does NOT rest (clob_submit is IOC-style:
// bid (one fill of 3), residual 2 does NOT rest (dex_submit is IOC-style:
// marketable, unfilled remainder dropped). ---
f2 := cli.submit(t, leth, zapwire.SideSell, 0.049, 5.0, "taker")
if len(f2) != 1 {
+2 -2
View File
@@ -11,7 +11,7 @@ import (
)
// mempool.go is the d-chain pending-transaction queue. ZAP write frames
// (clob_place/cancel/submit) are NOT executed synchronously — the handler decodes
// (dex_place/cancel/submit) are NOT executed synchronously — the handler decodes
// the frame into a Tx, Adds it here, and the mempool signals the consensus engine
// (init.ToEngine <- PendingTxs) that there is work. The engine responds by
// calling VM.BuildBlock, which drains the mempool in arrival (sequence) order.
@@ -21,7 +21,7 @@ import (
// mempool is a FIFO of pending transactions guarded by a mutex. It is small and
// deliberately simple: ordering is arrival order, which is the sequence the
// matcher consumes. (A fee-priority mempool is a future concern; the venue's CLOB
// matcher consumes. (A fee-priority mempool is a future concern; the venue's DEX
// is sequence-fair by design.)
//
// CANCEL / TOMBSTONE (synchronous-or-nothing custody, RED #4): a custody op that
+13 -13
View File
@@ -14,13 +14,13 @@ import (
"github.com/luxfi/dex/pkg/lx"
)
// read.go is the D-Chain's READ surface: clob_get_* query endpoints that return
// read.go is the D-Chain's READ surface: dex_get_* query endpoints that return
// COMMITTED chain state (resting orders, the trade log, markets, the book) as
// JSON. It is the orthogonal counterpart to handler.go's write surface:
//
// handler.go clob_place/submit/cancel/... -> mempool -> consensus -> Verify-match.
// handler.go dex_place/submit/cancel/... -> mempool -> consensus -> Verify-match.
// FROZEN binary zapwire frames. Mutates state through a consensus round.
// read.go clob_get_trades/orders/markets/book
// read.go dex_get_trades/orders/markets/book
// JSON. Reads committed state directly under vm.mu. ZERO consensus
// impact — no mempool, no block, no waiter; a read can never change a
// fill or a balance.
@@ -53,7 +53,7 @@ const (
MethodGetBook = "dex_get_book"
)
// defaultTradeLimit bounds an unparameterized clob_get_trades response so a chain
// defaultTradeLimit bounds an unparameterized dex_get_trades response so a chain
// with a long fill history cannot return an unbounded body by default. A caller
// paging the full log passes an explicit limit + since cursor.
const defaultTradeLimit = 1000
@@ -116,7 +116,7 @@ type tradeJSON struct {
Timestamp int64 `json:"timestamp"` // unix nanos
}
// getTradesResp is the clob_get_trades body: the chain head plus the committed
// getTradesResp is the dex_get_trades body: the chain head plus the committed
// fills for the requested range, in matcher production order (height then seq).
type getTradesResp struct {
chainHead
@@ -126,7 +126,7 @@ type getTradesResp struct {
// handleGetTrades serves the committed trade log:
//
// GET .../dex/clob_get_trades?since=<height>&limit=<n>
// GET .../dex/dex_get_trades?since=<height>&limit=<n>
//
// since (optional): first accepted height to include (inclusive); default 0.
// limit (optional): max rows; default defaultTradeLimit.
@@ -190,7 +190,7 @@ type orderJSON struct {
User string `json:"user"`
}
// getOrdersResp is the clob_get_orders body: the chain head plus the market's
// getOrdersResp is the dex_get_orders body: the chain head plus the market's
// resting book rows.
type getOrdersResp struct {
chainHead
@@ -201,7 +201,7 @@ type getOrdersResp struct {
// handleGetOrders serves a market's resting orders:
//
// GET .../dex/clob_get_orders?market=<hex>
// GET .../dex/dex_get_orders?market=<hex>
//
// The orders are read from the in-RAM book (a deterministic fold of the committed
// order:* rows), so a follower and the proposer at the same height return the same
@@ -262,7 +262,7 @@ type marketJSON struct {
BestAsk float64 `json:"bestAsk"`
}
// getMarketsResp is the clob_get_markets body: the chain head plus every market.
// getMarketsResp is the dex_get_markets body: the chain head plus every market.
type getMarketsResp struct {
chainHead
Count int `json:"count"`
@@ -271,11 +271,11 @@ type getMarketsResp struct {
// handleGetMarkets serves every market on the chain:
//
// GET .../dex/clob_get_markets
// GET .../dex/dex_get_markets
//
// Each record carries the market's durable identity (poolID, symbol, bound
// assets) and an in-RAM book summary (resting orders, best bid/ask). This is what
// the markets-display lists; combined with clob_get_trades it renders a venue.
// the markets-display lists; combined with dex_get_trades it renders a venue.
func (vm *VM) handleGetMarkets(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeReadErr(w, http.StatusMethodNotAllowed, "method not allowed")
@@ -341,7 +341,7 @@ type levelJSON struct {
Count int `json:"count"`
}
// getBookResp is the clob_get_book body: the chain head plus the market's
// getBookResp is the dex_get_book body: the chain head plus the market's
// aggregated bid/ask ladder and totals.
type getBookResp struct {
chainHead
@@ -356,7 +356,7 @@ type getBookResp struct {
// handleGetBook serves a market's aggregated order book:
//
// GET .../dex/clob_get_book?market=<hex>&depth=<n>
// GET .../dex/dex_get_book?market=<hex>&depth=<n>
//
// depth (optional): number of price levels per side; default 20. Levels and
// totals are read from the in-RAM book (the committed-row fold).
+7 -7
View File
@@ -22,17 +22,17 @@ import (
"github.com/luxfi/log"
)
// read_test.go proves the D-Chain READ surface (read.go / clob_get_*):
// read_test.go proves the D-Chain READ surface (read.go / dex_get_*):
//
// 1. The read endpoints are reachable over the EXACT in-luxd HTTP transport
// (CreateHandlers, mounted at /ext/bc/<D>/dex/<method>) — the same routes a
// deployed validator exposes.
// 2. After a crossing order is matched into a committed block (via the write
// path + the auto-sealer playing consensus), clob_get_trades returns the
// committed fill, clob_get_markets lists the market, clob_get_orders/book
// path + the auto-sealer playing consensus), dex_get_trades returns the
// committed fill, dex_get_markets lists the market, dex_get_orders/book
// reflect the resting set.
// 3. CROSS-VALIDATOR INVARIANT (in-process model): two INDEPENDENT VMs that
// Accept the SAME block bytes return BYTE-IDENTICAL clob_get_trades JSON and
// Accept the SAME block bytes return BYTE-IDENTICAL dex_get_trades JSON and
// the same head root. This is the local proof of the property the live
// 5-validator test checks: same accepted block => same committed read on
// every node. (A follower in production reaches the same state by Verify+
@@ -186,7 +186,7 @@ func crossOneMarket(t *testing.T, base string, f readFixture) []zapwire.Fill {
}
// TestRead_TradesAfterCommittedFill is the core happy path: a crossing order is
// matched into a committed block, and clob_get_trades returns that fill from
// matched into a committed block, and dex_get_trades returns that fill from
// durable state with the right price/size/side and a non-genesis head.
func TestRead_TradesAfterCommittedFill(t *testing.T) {
vm, base, stop := startReadServer(t)
@@ -257,7 +257,7 @@ func TestRead_MarketsAndOrdersAndBook(t *testing.T) {
}
}
if found == nil {
t.Fatalf("market %s not in clob_get_markets: %+v", marketHex, markets.Markets)
t.Fatalf("market %s not in dex_get_markets: %+v", marketHex, markets.Markets)
}
if !found.AssetsBound {
t.Errorf("market assetsBound=false, want true (open_market bound them)")
@@ -328,7 +328,7 @@ func TestRead_ZeroConsensusImpact(t *testing.T) {
// TestRead_CrossValidatorIdenticalTrades is the in-process model of the live
// 5-validator question: two INDEPENDENT VMs (separate DBs) that Accept the SAME
// sequence of block bytes must return BYTE-IDENTICAL clob_get_trades JSON and the
// sequence of block bytes must return BYTE-IDENTICAL dex_get_trades JSON and the
// same head root. This isolates the replication property from the network: if a
// block's bytes are delivered (gossip in production) and Verify+Accept succeed,
// the committed read is identical — which is exactly what the read RPC lets the
+3 -3
View File
@@ -15,7 +15,7 @@ import (
// OUTCOME a block's execution produced, and the VM's registry that lets a ZAP
// handler block until "its" tx is decided by an accepted block.
//
// The decomplected shape: a clob_* write is NOT executed when it arrives — it is
// The decomplected shape: a dex_* write is NOT executed when it arrives — it is
// queued, ordered by consensus, matched at Verify, committed at Accept. But the
// caller (the proxy's synchronous Relay, the precompile's swap) needs the
// resulting fills/ack back. So the handler registers a waiter keyed by the tx's
@@ -32,9 +32,9 @@ import (
type txOutcome struct {
txID ids.ID
typ TxType
orderID uint64 // placed / cancelled order id (0 for submit/ensure)
orderID uint64 // placed / cancelled order id (0 for submit/ensure)
fills []zapwire.Fill // submit fills (nil otherwise)
status uint8 // zapwire.StatusPlaced / StatusCanceled / StatusRejected
status uint8 // zapwire.StatusPlaced / StatusCanceled / StatusRejected
}
// toWireFills converts a submit's matched trades into the FROZEN wire Fill form
+2 -2
View File
@@ -111,7 +111,7 @@ const (
)
// marketConstraint is the MINIMAL notional scaffold: just enough to express the
// one constraint a value-bearing CLOB must enforce against integer flooring — an
// one constraint a value-bearing DEX must enforce against integer flooring — an
// executable order MUST reserve a non-zero lock. It deliberately does NOT model a
// full fee/tick/lot/minNotional engine (that is a separate roadmap); the only
// invariant here is "arithmetic flooring can never create a FREE executable order".
@@ -204,7 +204,7 @@ func debitWithdraw(db database.KeyValueReaderWriterDeleter, user string, asset [
//
// IDENTITY: the settlement identity is the WALLET (AccountID); UserID8 is only a
// matcher hint. The TAKER is the submitter, whose full 16-byte wallet is carried on
// the ephemeral order this block from the decoded clob_submit invocation context
// the ephemeral order this block from the decoded dex_submit invocation context
// (never the 8-byte row handle — the taker never rests, so its full wallet is always
// in hand). The MAKER is a RESTING order whose in-RAM user is only 8 bytes after a
// restart (RowToOrder folds the GPU row's 8-byte UserID handle), so its full wallet
+1 -1
View File
@@ -809,7 +809,7 @@ func TestSettlementIdentityWidthEnforced(t *testing.T) {
// the width property the ledger depends on.
v16 := idOf(t, victim)
var colliding userKey
copy(colliding[:], v16[:8]) // SAME 8-byte matcher handle ...
copy(colliding[:], v16[:8]) // SAME 8-byte matcher handle ...
copy(colliding[8:], []byte{0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8}) // ... distinct tail
if v16 == colliding {
t.Fatal("colliding-handle identities resolved to the SAME 16-byte identity (width fold lost bytes 8..15)")
+3 -3
View File
@@ -42,7 +42,7 @@ const (
prefixSeen = "seen:" // processed tx ids (idempotency / replay dedup)
prefixMeta = "meta:"
// The CLOB custody ledger: where the money LIVES inside the d-chain. A
// The DEX custody ledger: where the money LIVES inside the d-chain. A
// deposit credits balance:; placing/submitting an order moves available ->
// locked: for the asset the order spends; a fill moves locked: between maker
// and taker; a cancel/reject returns locked: -> balance:; a withdraw debits
@@ -94,7 +94,7 @@ const (
// accepted block; re-applying it is a no-op. This is the d-chain STATE
// idempotency index: the frozen zapwire frame carries no nonce, so dedup is
// content-addressed on the tx id (which subsumes the wire user[16] and the full
// payload). A relay that retries a dropped clob_submit re-sends the identical
// payload). A relay that retries a dropped dex_submit re-sends the identical
// bytes -> identical tx id -> deduped, exactly once.
func seenKey(txID ids.ID) []byte {
k := make([]byte, len(prefixSeen)+32)
@@ -120,7 +120,7 @@ func isSeen(db database.KeyValueReader, txID ids.ID) (bool, error) {
// ORIGINAL outcome (status + orderID + fills) so a later replay of the same
// content-addressed tx returns the FIRST result verbatim rather than a blanket
// reject — the idempotency contract a caller (precompile / proxy) needs: a
// retried clob_place returns its original Placed+orderID, a retried clob_submit
// retried dex_place returns its original Placed+orderID, a retried dex_submit
// returns its original fills, never a spurious "rejected". (A bare presence
// byte made a deduped place indistinguishable from a genuine rejection, which
// reverted the EVM tx whose precompile re-ran the place across the host's
+1 -1
View File
@@ -107,7 +107,7 @@ var (
// would mint against the ledger.
ErrInsufficientLocked = errors.New("dchain: insufficient locked balance")
// ErrMarketAssetsUnbound: a balance-bearing order on a market whose (base,
// quote) assets were never bound (clob_open_market) — cannot value-check it.
// quote) assets were never bound (dex_open_market) — cannot value-check it.
ErrMarketAssetsUnbound = errors.New("dchain: market assets not bound")
// ErrFillMissingUnits: a consensus fill reached settlement without the exact
// integer lane (BaseUnits/QuoteUnits) — the VM must set SizeUnits on every
+9 -9
View File
@@ -59,11 +59,11 @@ type VM struct {
books map[[32]byte]*lx.OrderBook
// Accepted-block index + linear-chain head.
acceptedBlocks map[ids.ID]*Block
heightIndex map[uint64]ids.ID
lastAcceptedID ids.ID
acceptedBlocks map[ids.ID]*Block
heightIndex map[uint64]ids.ID
lastAcceptedID ids.ID
lastAcceptedHeight uint64
lastRoot [Size]byte
lastRoot [Size]byte
// processingBlocks indexes built/verified blocks not yet accepted, so a
// plugin-transport Accept — which carries only the block ID and re-resolves
@@ -81,7 +81,7 @@ type VM struct {
// toEngine is retained for the mempool signal path.
toEngine chan<- block.Message
// zapIngest is the canonical co-located ZAP CLOB socket (zapingest.go), served
// zapIngest is the canonical co-located ZAP DEX socket (zapingest.go), served
// in-plugin when init Config sets zapIngestAddr. nil when HTTP-compat-only.
// zapIngestCancel stops its serve goroutine on Shutdown. Both guarded by vm.mu.
zapIngest rpc.Server
@@ -132,7 +132,7 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
vm.preferred = vm.lastAcceptedID
// Start the canonical co-located ZAP CLOB ingestion socket if the chain config
// Start the canonical co-located ZAP DEX ingestion socket if the chain config
// names a listen address (zapingest.go). This runs AFTER the durable state is
// loaded so the socket never accepts an order the VM cannot yet sequence. It is
// a no-op (HTTP-compat-only) when no address is configured. Transport ⟂
@@ -503,14 +503,14 @@ func (vm *VM) SetState(ctx context.Context, state uint32) error {
// Version returns the VM version string.
func (vm *VM) Version(ctx context.Context) (string, error) { return Version, nil }
// NewHTTPHandler returns the VM's HTTP handler. The d-chain serves its CLOB API
// NewHTTPHandler returns the VM's HTTP handler. The d-chain serves its DEX API
// over the ZAP gateway (handler.go), not HTTP, so this is nil.
func (vm *VM) NewHTTPHandler(ctx context.Context) (http.Handler, error) { return nil, nil }
// CreateHandlers returns the VM's named HTTP handlers for luxd to mount under
// /ext/bc/<DCHAIN_ID>/ (and the "D" alias). It returns one handler per CLOB method
// /ext/bc/<DCHAIN_ID>/ (and the "D" alias). It returns one handler per DEX method
// keyed by its full sub-path ("/dex/<method>"), so an order POSTed to
// /ext/bc/D/dex/clob_submit reaches the matcher through the node's own router —
// /ext/bc/D/dex/dex_submit reaches the matcher through the node's own router —
// the in-luxd ingestion seam (ingest.go). This is how an order enters the native
// VM: submitTx -> mempool -> consensus -> Verify-match. The plugin transport
// (github.com/luxfi/vm/rpc) serves these handlers from a local http.Server inside
+3 -3
View File
@@ -39,7 +39,7 @@ func newTestVM(t *testing.T, db database.Database) (*VM, chan block.Message) {
return vm, toEngine
}
// submitFrame builds the [type:1][body] tx bytes for a clob op, the exact bytes
// submitFrame builds the [type:1][body] tx bytes for a dex op, the exact bytes
// the handler/proxy would Add to the mempool.
func ensureMarketTx(t *testing.T, pool [32]byte) *Tx {
t.Helper()
@@ -522,7 +522,7 @@ func TestVMRestartLegacyGenesisDB(t *testing.T) {
// Verify, which carries the full block bytes — and then calls Accept on whatever
// GetBlock returns. Before the processing-block index, GetBlock(builtID) returned
// ErrNotFound (only accepted blocks were indexed), so the engine's self-finalize
// Accept was a silent no-op, the clob_* submitTx waiter hung forever, and no
// Accept was a silent no-op, the dex_* submitTx waiter hung forever, and no
// further blocks were built. This drives the EXACT plugin-shaped path the live
// node uses — BuildBlock -> GetBlock(id) -> Accept — which every existing test
// skipped by accepting the *Block returned from BuildBlock directly.
@@ -533,7 +533,7 @@ func TestVMAcceptViaGetBlock(t *testing.T) {
vm, toEngine := newTestVM(t, db)
// A clob tx in the mempool, then the proposer's lifecycle exactly as the
// A dex tx in the mempool, then the proposer's lifecycle exactly as the
// engine drives it over the plugin transport.
vm.mempool.Add(ensureMarketTx(t, pool))
vm.mempool.Add(placePoolTx(t, pool, zapwire.SideSell, 101.0, 5.0, "maker-a"))
+13 -13
View File
@@ -11,14 +11,14 @@ import (
"github.com/luxfi/rpc"
)
// zapingest.go is the D-Chain CLOB ZAP ingestion seam for the IN-LUXD native VM —
// zapingest.go is the D-Chain DEX ZAP ingestion seam for the IN-LUXD native VM —
// the CANONICAL native order path. It is the second transport over the one
// authoritative handler core (clobMethods, handler.go), symmetric with the HTTP
// authoritative handler core (dexMethods, handler.go), symmetric with the HTTP
// compat transport (ingest.go / CreateHandlers):
//
// ZAP socket (this file) : RegisterCLOB on rpc.Listen — the HFT native path,
// ZAP socket (this file) : RegisterDEX on rpc.Listen — the HFT native path,
// binary wire, co-located on the pod. CANONICAL.
// HTTP router (ingest.go) : the same clobMethods over the node's /ext/bc route.
// HTTP router (ingest.go) : the same dexMethods over the node's /ext/bc route.
// COMPAT (web / exchange-api / debug).
//
// WHY BOTH, AND WHY THIS IS NOT DUPLICATION: transport is orthogonal to consensus.
@@ -27,8 +27,8 @@ import (
// Block.Accept — validators replay from the accepted block BYTES, never from the
// wire stream or a wall clock. So the choice of socket vs HTTP is purely a
// submission perf/UX axis with no safety implication; the ONE matcher core
// (clobMethods -> submitTx -> mempool) is shared, never re-implemented per
// transport. The ZAP socket is the canonical native path because the CLOB's whole
// (dexMethods -> submitTx -> mempool) is shared, never re-implemented per
// transport. The ZAP socket is the canonical native path because the DEX's whole
// point is the low-latency streaming HFT seam (co-locate the matcher with the ZAP
// relay; see dex perf notes); HTTP stays as the broad-compatibility surface.
//
@@ -44,7 +44,7 @@ import (
// only: unknown fields are ignored, an empty/absent Config is the zero value (no
// ZAP socket). Only the fields the VM actually consumes are declared.
type vmConfig struct {
// ZAPIngestAddr, when non-empty, is the TCP address the co-located ZAP CLOB
// ZAPIngestAddr, when non-empty, is the TCP address the co-located ZAP DEX
// socket listens on inside the plugin process (e.g. "0.0.0.0:9101"). Empty
// means the canonical socket is not served (HTTP compat only). It is a
// pod-local listen address the node/operator assigns; orders arriving here
@@ -67,9 +67,9 @@ func parseConfig(b []byte) (vmConfig, error) {
return c, nil
}
// startZAPIngest opens the canonical co-located ZAP CLOB socket and serves the SAME
// clobMethods the HTTP transport folds over. It is a no-op when addr is empty (the
// VM serves HTTP only). It is ADDITIVE: it reuses RegisterCLOB (the existing socket
// startZAPIngest opens the canonical co-located ZAP DEX socket and serves the SAME
// dexMethods the HTTP transport folds over. It is a no-op when addr is empty (the
// VM serves HTTP only). It is ADDITIVE: it reuses RegisterDEX (the existing socket
// seam cmd/dvenue uses) and adds no new handler logic. Must be called under vm.mu,
// after the durable state is loaded (so the socket never accepts an order before
// the VM can sequence it). The server runs in its own goroutine whose lifetime is
@@ -83,9 +83,9 @@ func (vm *VM) startZAPIngest(addr string) error {
if err != nil {
return fmt.Errorf("dchain: ZAP ingest listen %q: %w", addr, err)
}
if err := vm.RegisterCLOB(server); err != nil {
if err := vm.RegisterDEX(server); err != nil {
_ = server.Close()
return fmt.Errorf("dchain: register CLOB on ZAP ingest socket: %w", err)
return fmt.Errorf("dchain: register DEX on ZAP ingest socket: %w", err)
}
serveCtx, cancel := context.WithCancel(context.Background())
@@ -96,7 +96,7 @@ func (vm *VM) startZAPIngest(addr string) error {
vm.log.Error("dchain ZAP ingest serve", "addr", server.Addr(), "err", serr)
}
}()
vm.log.Info("dchain ZAP ingest serving (canonical native CLOB path)", "addr", server.Addr())
vm.log.Info("dchain ZAP ingest serving (canonical native DEX path)", "addr", server.Addr())
return nil
}
+5 -5
View File
@@ -21,10 +21,10 @@ import (
"github.com/luxfi/rpc"
)
// zapingest_test.go proves the CANONICAL in-luxd ZAP CLOB ingestion seam
// zapingest_test.go proves the CANONICAL in-luxd ZAP DEX ingestion seam
// (zapingest.go): when the chain's init Config names a zapIngestAddr, the VM
// ITSELF stands up a co-located ZAP socket bound to the one clobMethods core —
// there is NO manual RegisterCLOB by the runner (cmd/dchain). A ZAP client dialing
// ITSELF stands up a co-located ZAP socket bound to the one dexMethods core —
// there is NO manual RegisterDEX by the runner (cmd/dexd plugin mode). A ZAP client dialing
// that socket drives a real order through the IDENTICAL submitTx -> mempool ->
// BuildBlock -> Verify -> Accept path the HTTP transport uses, and the fill is
// committed D-Chain state. This is the deploy-shaped proof: the in-luxd plugin now
@@ -34,7 +34,7 @@ import (
// startZAPIngestVM boots a VM whose init Config enables the co-located ZAP socket on
// an ephemeral port, plus the auto-sealer. It returns the VM, the socket's dial
// address (chosen by the OS), and a stop func. Critically it does NOT call
// RegisterCLOB — Initialize wires the socket from Config, exactly as the in-luxd
// RegisterDEX — Initialize wires the socket from Config, exactly as the in-luxd
// plugin does in production.
func startZAPIngestVM(t *testing.T) (*VM, string, func()) {
t.Helper()
@@ -110,7 +110,7 @@ func TestZAPIngestCrossingOrdersMatchInBlock(t *testing.T) {
// TestZAPIngestSharesOneCoreWithHTTP proves transport ⟂ consensus over the SAME book:
// a maker rests over the canonical ZAP socket, and a taker crossing it through the
// HTTP compat handler (CreateHandlers) fills against that SAME resting order. Neither
// transport has its own matcher or its own book — both fold over clobMethods into the
// transport has its own matcher or its own book — both fold over dexMethods into the
// one mempool/consensus path, so an order from either side sees the other's state.
func TestZAPIngestSharesOneCoreWithHTTP(t *testing.T) {
// Boot ONE VM with the ZAP socket on, and ALSO mount its HTTP handlers — both
+1 -1
View File
@@ -50,7 +50,7 @@ var (
ErrUnknownMarket = errors.New("dex: market has no asset binding")
// ErrNoLiquidity is returned by the router when NO configured source can fill
// any of the requested amount (neither the CLOB nor an AMM crosses). A swap
// any of the requested amount (neither the OrderBook nor an AMM crosses). A swap
// that fills nothing returns this so the caller reverts rather than locking the
// taker's input against a zero fill.
ErrNoLiquidity = errors.New("dex: no liquidity across any source for this swap")
+3 -3
View File
@@ -14,7 +14,7 @@ import (
// sources and journal the joint C+D effect. The sequence is:
//
// 1. LOCK the taker's full input ONCE (available -> locked, and record the C-side
// vault debit in the journal). A split across CLOB+AMM draws from this single
// vault debit in the journal). A split across OrderBook+AMM draws from this single
// lock, so the taker can never spend more than they locked.
// 2. ROUTE: best-execution across the sources (router.Route), which mutates the
// book/ledger/reserves in the Store and records the proceeds in the journal.
@@ -29,7 +29,7 @@ import (
// SwapResult is the outcome of one routed swap.
type SwapResult struct {
// Fills are the per-source fills (CLOB trades + AMM deltas) in execution order,
// Fills are the per-source fills (OrderBook trades + AMM deltas) in execution order,
// for events/receipts/the indexer.
Fills []Fill
// AmountIn / AmountOut are the input consumed and the output produced, in the
@@ -53,7 +53,7 @@ type SwapResult struct {
// consume is refunded. The router enforces the taker price limit and MinOut; on a
// breach ExecuteSwap returns the error and the caller reverts (nothing committed).
func ExecuteSwap(db Store, router *Router, req SwapRequest) (*SwapResult, error) {
if req.Class != ClassPublicCLOB {
if req.Class != ClassPublicDEX {
// Only the public route is built in the first cut. An FHE-private swap is
// refused here (not silently downgraded) until the private-pool source lands.
return nil, ErrNoLiquidity
+2 -2
View File
@@ -115,8 +115,8 @@ func totalLedger(t *testing.T, db Store, accts []AccountID, assets []AssetID) ma
return out
}
// clobRouter returns a router with only the CLOB source (the common case).
func clobRouter() *Router { return NewRouter(NewCLOBSource()) }
// orderBookRouter returns a router with only the OrderBook source (the common case).
func orderBookRouter() *Router { return NewRouter(NewOrderBookSource()) }
// price builds a PriceInt from a human price (quote per base).
func price(p float64) lx.PriceInt { return PriceToInt(p) }
+2 -2
View File
@@ -2,7 +2,7 @@
// See the file LICENSE for licensing terms.
// Package dex is the SHARED, caller-agnostic deterministic core of the Lux
// DEX: the CLOB custody ledger, the integer-exact settlement of matcher fills,
// DEX: the OrderBook custody ledger, the integer-exact settlement of matcher fills,
// the fail-closed maker-identity discipline, the smart-order-router across
// on-chain liquidity sources, and the execution root. It is a pure function of
// (ordered txs, prior state) over a generic key/value Store, so two homes —
@@ -58,7 +58,7 @@ type AssetID = [32]byte
// WITHOUT a lossy fold:
//
// - the cEVM home left-pads the 20-byte EVM address into 32 bytes (12 zero bytes
// + the 20 address bytes), so two distinct addresses are ALWAYS distinct
// - the 20 address bytes), so two distinct addresses are ALWAYS distinct
// accounts — no address pair can collide and drain each other's resting-order
// proceeds (the cross-user-drain guard, made injective for 20-byte addresses
// that a 16-byte identity could alias);
+14 -14
View File
@@ -43,9 +43,9 @@ func TestOwnership_MakerSettlesToFullIdentity(t *testing.T) {
req := SwapRequest{
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 40, OrderID: 1_000, TimestampN: 2_000,
Class: ClassPublicCLOB,
Class: ClassPublicDEX,
}
if _, err := ExecuteSwap(db, clobRouter(), req); err != nil {
if _, err := ExecuteSwap(db, orderBookRouter(), req); err != nil {
t.Fatalf("ExecuteSwap: %v", err)
}
@@ -79,9 +79,9 @@ func TestOwnership_FailsClosedOnMissingIdentity(t *testing.T) {
req := SwapRequest{
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 40, OrderID: 1_000, TimestampN: 2_000,
Class: ClassPublicCLOB,
Class: ClassPublicDEX,
}
_, err := ExecuteSwap(db, clobRouter(), req)
_, err := ExecuteSwap(db, orderBookRouter(), req)
if !errors.Is(err, ErrMissingSettlementUser) {
t.Fatalf("expected ErrMissingSettlementUser (fail-closed), got %v", err)
}
@@ -137,11 +137,11 @@ func TestOwnership_CancelRefundsToOwner(t *testing.T) {
}
}
// TestMEVFloor_CLOBRefusesBelowFloor asserts the FIRST layer of MEV protection: a
// SELL with a floor the resting CLOB cannot meet does not cross at all (the matcher
// TestMEVFloor_OrderBookRefusesBelowFloor asserts the FIRST layer of MEV protection: a
// SELL with a floor the resting OrderBook cannot meet does not cross at all (the matcher
// won't fill below the limit), so the swap reverts with no fill — the taker keeps
// their funds rather than selling below their floor.
func TestMEVFloor_CLOBRefusesBelowFloor(t *testing.T) {
func TestMEVFloor_OrderBookRefusesBelowFloor(t *testing.T) {
db := newStore()
pid := seedMarket(t, db, 24, tokLETH, tokLUSD)
maker := account(0xAA)
@@ -155,9 +155,9 @@ func TestMEVFloor_CLOBRefusesBelowFloor(t *testing.T) {
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 40,
LimitPrice: price(50), LimitIsUpper: false, // SELL floor at 50
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicCLOB,
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicDEX,
}
_, err := ExecuteSwap(db, clobRouter(), req)
_, err := ExecuteSwap(db, orderBookRouter(), req)
if !errors.Is(err, ErrNoLiquidity) {
t.Fatalf("expected ErrNoLiquidity (matcher refuses to cross below floor), got %v", err)
}
@@ -184,16 +184,16 @@ func TestMEVFloor_AMMLegRejectedByProceedsFloor(t *testing.T) {
taker := account(0xBB)
seedDeposit(t, db, taker, tokLETH, 100)
// AMM priced ~40 LUSD/LETH (below the taker's 50 floor). No CLOB.
// AMM priced ~40 LUSD/LETH (below the taker's 50 floor). No OrderBook.
pool := newMemAMM(0)
pool.set(pid, 100_000, 4_000_000) // ~40
router := NewRouter(NewCLOBSource(), NewAMMSource(pool))
router := NewRouter(NewOrderBookSource(), NewAMMSource(pool))
req := SwapRequest{
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 40,
LimitPrice: price(50), LimitIsUpper: false, // floor 50, AMM gives ~40
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicCLOB,
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicDEX,
}
_, err := ExecuteSwap(db, router, req)
if !errors.Is(err, ErrPriceLimit) {
@@ -216,9 +216,9 @@ func TestMEVFloor_AcceptsGoodProceeds(t *testing.T) {
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 40,
LimitPrice: price(50), LimitIsUpper: false,
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicCLOB,
OrderID: 1_000, TimestampN: 2_000, Class: ClassPublicDEX,
}
res, err := ExecuteSwap(db, clobRouter(), req)
res, err := ExecuteSwap(db, orderBookRouter(), req)
if err != nil {
t.Fatalf("ExecuteSwap: %v", err)
}
+10 -10
View File
@@ -231,17 +231,17 @@ func (s AssetSide) valueKey() AssetID {
// THE EXACT STEP ORDER (the value-path call chain, run BEFORE any C-balance debit and
// BEFORE any DEX state write):
//
// Resolve(base) -> Resolve(quote) -> VerifyOnChain(base) -> VerifyOnChain(quote) -> EnsureMarket
// Resolve(base) -> Resolve(quote) -> VerifyOnChain(base) -> VerifyOnChain(quote) -> EnsureMarket
//
// - Resolve (RequireResolvedAsset): prove each side's CANONICAL IDENTITY on the node's
// bound network — PERMISSIONLESS. A well-formed real EVM-native/ERC-20 reference always
// resolves; only a malformed/wrong-network/explicitly-disabled/synthetic reference is
// refused (ErrAssetNotResolved). No resolver => ErrNoAssetResolver (fail-closed).
// - VerifyOnChain (RequireRealOnChain): the AUTHORITATIVE reality check — is each side
// backed by LIVE on-chain code in the state this block mutates? A fabricated/synthetic
// ERC-20 (an ASCII-ticker address, a self-destructed or never-deployed contract) has no
// code => ErrAssetNotOnChain; no verifier => ErrNoOnChainVerifier. Native (the
// zero-address marker) is the chain's own coin and is always real.
// - Resolve (RequireResolvedAsset): prove each side's CANONICAL IDENTITY on the node's
// bound network — PERMISSIONLESS. A well-formed real EVM-native/ERC-20 reference always
// resolves; only a malformed/wrong-network/explicitly-disabled/synthetic reference is
// refused (ErrAssetNotResolved). No resolver => ErrNoAssetResolver (fail-closed).
// - VerifyOnChain (RequireRealOnChain): the AUTHORITATIVE reality check — is each side
// backed by LIVE on-chain code in the state this block mutates? A fabricated/synthetic
// ERC-20 (an ASCII-ticker address, a self-destructed or never-deployed contract) has no
// code => ErrAssetNotOnChain; no verifier => ErrNoOnChainVerifier. Native (the
// zero-address marker) is the chain's own coin and is always real.
//
// Both steps run for BOTH sides before ANY write, so a refused open leaves the store
// untouched (fail-closed): the market existence row is created only after all four checks
+5 -5
View File
@@ -63,9 +63,9 @@ func TestReplay_SameInputSameStateAllValidators(t *testing.T) {
req := SwapRequest{
PoolID: pid, TakerUser: account(0xBB), Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 137, OrderID: 5_000, TimestampN: 9_000,
Class: ClassPublicCLOB,
Class: ClassPublicDEX,
}
res, err := ExecuteSwap(db, clobRouter(), req)
res, err := ExecuteSwap(db, orderBookRouter(), req)
if err != nil {
t.Fatalf("validator %d ExecuteSwap: %v", v, err)
}
@@ -94,13 +94,13 @@ func TestReplay_ExecutionRootDeterministic(t *testing.T) {
req := SwapRequest{
PoolID: pid, TakerUser: account(0xBB), Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 200, OrderID: 5_000, TimestampN: 9_000,
Class: ClassPublicCLOB,
Class: ClassPublicDEX,
}
r1, err := ExecuteSwap(db1, clobRouter(), req)
r1, err := ExecuteSwap(db1, orderBookRouter(), req)
if err != nil {
t.Fatalf("db1: %v", err)
}
r2, err := ExecuteSwap(db2, clobRouter(), req)
r2, err := ExecuteSwap(db2, orderBookRouter(), req)
if err != nil {
t.Fatalf("db2: %v", err)
}
+3 -3
View File
@@ -34,7 +34,7 @@ const Size = hash.KeccakSize
// db for market poolID, plus the swap's realized fills. It folds:
//
// - the canonical resting rows of poolID (rebuilt from order:* rows), and
// - the swap's CLOB fills (the trade rows),
// - the swap's OrderBook fills (the trade rows),
//
// into two RFC-6962 tagged Merkle sub-roots and composes them with a fixed-shape
// keccak256. Deterministic over deterministic state, so two validators replaying
@@ -65,7 +65,7 @@ func bookRootForMarket(db Store, poolID [32]byte) [Size]byte {
return merkle.Root(leaves)
}
// tradeRootForResult folds a swap's realized CLOB fills into the RFC-6962 trade
// tradeRootForResult folds a swap's realized OrderBook fills into the RFC-6962 trade
// root. Trades are folded in execution order (the router's source/fill order), each
// bound to its index. AMM fills carry no maker rows; their (base,quote) deltas are
// committed via the reserve write + the journal and are included here as a synthetic
@@ -104,7 +104,7 @@ func tradeLeafDigest(t lx.DEXTrade, i uint32) [Size]byte {
// ammLeafDigest = keccak256("amm" ‖ base_le ‖ quote_le ‖ index_le) — binds an AMM
// leg's (base,quote) move into the trade root so a tampered AMM amount changes the
// root just as a tampered CLOB fill does.
// root just as a tampered OrderBook fill does.
func ammLeafDigest(base, quote uint64, i uint32) [Size]byte {
var b [8]byte
var q [8]byte
+20 -20
View File
@@ -12,10 +12,10 @@ import (
// router.go is THE on-chain smart-order-router — the real product. It routes one
// swap across the on-chain liquidity SOURCES, in-process and deterministically:
//
// 1. the V4 native CLOB (the in-process matcher) FIRST;
// 2. the V2/V3 AMM pools (EVM contracts in the SAME cEVM) when the CLOB has
// 1. the V4 native OrderBook (the in-process matcher) FIRST;
// 2. the V2/V3 AMM pools (EVM contracts in the SAME cEVM) when the OrderBook has
// insufficient depth — read their state and execute the swap in-process;
// 3. best-execution: take the better price across the CLOB and the AMM, or split
// 3. best-execution: take the better price across the OrderBook and the AMM, or split
// the input across them when splitting beats either alone.
//
// Every source is a pure, replayable function of (prior state, input): no live
@@ -31,7 +31,7 @@ import (
// quote state. The router never knows or cares which concrete source it routes to.
// RouteClass tags the privacy/visibility regime a swap routes under. Only
// ClassPublicCLOB is built in the first cut. ClassFHEPrivate is the staged-next
// ClassPublicDEX is built in the first cut. ClassFHEPrivate is the staged-next
// private-pool route (owner spec §13): a swap whose amounts are FHE-encrypted and
// matched in a private pool. The router carries the class so the source selection
// can later restrict an FHE swap to FHE-capable sources WITHOUT reshaping the
@@ -39,9 +39,9 @@ import (
type RouteClass uint8
const (
// ClassPublicCLOB is the public route: plaintext amounts, the public CLOB +
// ClassPublicDEX is the public route: plaintext amounts, the public OrderBook +
// public AMM pools. The only class built now.
ClassPublicCLOB RouteClass = iota
ClassPublicDEX RouteClass = iota
// ClassFHEPrivate is the private route (STAGED — not built): FHE-encrypted
// amounts, matched in a private pool. Reserved so the router can route it to an
// FHE source later without an interface change.
@@ -64,7 +64,7 @@ type SwapRequest struct {
OrderID uint64 // = blockDeterministicID(height, callIndex); never zero
TimestampN int64 // block timestamp (UnixNano), for the ephemeral order
// LimitPrice is the taker's OWN worst-acceptable CLOB price (quote-per-base, in
// LimitPrice is the taker's OWN worst-acceptable OrderBook price (quote-per-base, in
// PriceInt grid units), derived from the V4 SqrtPriceLimitX96. 0 = no limit.
// LimitIsUpper says which side it bounds (true = a BUY ceiling, false = a SELL
// floor). Enforced on the realized proceeds price — the taker-authenticated MEV
@@ -76,22 +76,22 @@ type SwapRequest struct {
// across all sources is below it. 0 = no floor.
MinOut uint64
// Class is the route class (public CLOB now; FHE-private staged). The first cut
// only routes ClassPublicCLOB.
// Class is the route class (public OrderBook now; FHE-private staged). The first cut
// only routes ClassPublicDEX.
Class RouteClass
}
// Fill is one matched quantity from one source, in the matcher's authoritative
// integer lane. The router aggregates Fills across sources into the joint journal.
type Fill struct {
// Trades are the CLOB fills (for a CLOB source) — they carry maker order ids so
// Trades are the OrderBook fills (for a OrderBook source) — they carry maker order ids so
// settlement resolves each maker's full identity via orderuser:. Empty for an
// AMM source (an AMM fill has no resting maker; see AMMBaseDelta/AMMQuoteDelta).
Trades []lx.Trade
// AMMBaseDelta / AMMQuoteDelta are the base/quote units an AMM source moved (an
// AMM has no maker rows). The router credits/debits these against the pool's
// reserves and the taker directly. Zero for a CLOB source.
// reserves and the taker directly. Zero for a OrderBook source.
AMMBaseDelta uint64
AMMQuoteDelta uint64
@@ -103,8 +103,8 @@ type Fill struct {
type SourceKind uint8
const (
// SourceCLOB is the V4 native CLOB (the in-process matcher). The primary source.
SourceCLOB SourceKind = iota
// SourceOrderBook is the V4 native OrderBook (the in-process matcher). The primary source.
SourceOrderBook SourceKind = iota
// SourceAMM is an in-cEVM V2/V3 AMM pool, read + executed in-process.
SourceAMM
// SourceRFQ is the STAGED external-CEX/market-maker source (on-chain quotes
@@ -128,7 +128,7 @@ type Quote struct {
// Source is one on-chain liquidity source the router routes across. It is a pure,
// replayable view+executor over the Store. Both methods read prior state only; a
// source NEVER issues a live external query (that forks consensus). The CLOB
// source NEVER issues a live external query (that forks consensus). The OrderBook
// source and the AMM source implement it; an RFQ source can implement it LATER
// over on-chain-committed quote state without the router changing.
type Source interface {
@@ -141,7 +141,7 @@ type Source interface {
QuoteSwap(db Store, req SwapRequest, remainingIn uint64) (Quote, error)
// ExecuteSwap fills up to remainingIn against this source, MUTATING the Store
// (the matcher's book/ledger rows for the CLOB; the pool reserves for the AMM)
// (the matcher's book/ledger rows for the OrderBook; the pool reserves for the AMM)
// and appending the value moves to the journal. Returns the Fill and the input
// units actually consumed. It MUST be consistent with QuoteSwap over the same
// state (the router relies on quote==execute for the split it chose).
@@ -150,14 +150,14 @@ type Source interface {
// Router routes a swap across an ordered set of Sources, best-execution first.
// The order is the SOURCE PRIORITY when prices tie or a source is exhausted:
// CLOB first (the native venue), then AMM. The router itself holds no state — it
// OrderBook first (the native venue), then AMM. The router itself holds no state — it
// is a pure function of the sources + the Store.
type Router struct {
sources []Source
}
// NewRouter builds a router over the given sources in priority order. The first
// cut wires [CLOB, AMM]; an RFQ source appends LATER with no router change.
// cut wires [OrderBook, AMM]; an RFQ source appends LATER with no router change.
func NewRouter(sources ...Source) *Router {
return &Router{sources: sources}
}
@@ -169,9 +169,9 @@ func NewRouter(sources ...Source) *Router {
// 1. While input remains and progress is possible:
// a. QuoteSwap every source for the remaining input.
// b. Pick the source with the best marginal price (most output per input
// consumed), ties broken by source priority (CLOB before AMM).
// consumed), ties broken by source priority (OrderBook before AMM).
// c. ExecuteSwap that source for the remaining input; subtract the consumed
// input, add the produced output.
// input, add the produced output.
// 2. Stop when input is exhausted, no source can fill more, or this iteration
// made no progress.
//
@@ -251,7 +251,7 @@ func (r *Router) Route(db Store, req SwapRequest, j *Journal) (totalOut, totalIn
// quote b for the taker, given the side. The effective price is output-per-input;
// more output per unit input is always better for the taker. Compared with cross-
// multiplication (no float): a is better iff a.out * b.in > b.out * a.in. Equal
// prices keep the incumbent (b) so source priority (the scan order, CLOB first)
// prices keep the incumbent (b) so source priority (the scan order, OrderBook first)
// breaks the tie.
func betterMarginalPrice(a, b Quote, _ lx.Side) bool {
// a.out/a.in > b.out/b.in <=> a.out*b.in > b.out*a.in
+3 -3
View File
@@ -8,7 +8,7 @@ import (
)
// source_amm.go is the V2/V3 AMM liquidity source — the router's FALLTHROUGH when
// the CLOB has insufficient depth. The AMM pools are EVM contracts in the SAME
// the OrderBook has insufficient depth. The AMM pools are EVM contracts in the SAME
// cEVM, so their state is read in-process and the swap executes in-process
// (deterministic constant-product math; every validator replays identically).
//
@@ -110,7 +110,7 @@ func (s *AMMSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j
// top-level lock holds the taker's full input in the 0x9999 vault; the AMM leg's
// input is consumed from that vault-held amount and NOT refunded. We spend the
// taker's LOCKED input ledger units so the conservation invariant (lock == fills
// spent + refund) holds across BOTH CLOB and AMM legs uniformly.
// spent + refund) holds across BOTH OrderBook and AMM legs uniformly.
inAsset := req.Quote
outAsset := req.Base
if req.Side == lx.Sell {
@@ -120,7 +120,7 @@ func (s *AMMSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j
if serr := SpendLocked(db, req.TakerUser, inAsset, inUsed); serr != nil {
return Fill{}, 0, 0, serr
}
// Credit the taker's dex AVAILABLE with the proceeds — SYMMETRIC with the CLOB
// Credit the taker's dex AVAILABLE with the proceeds — SYMMETRIC with the OrderBook
// source (whose SettleFills credits the taker's available). This makes the D-surface
// settlement consistent across BOTH sources: after any swap the taker's available
// holds exactly the proceeds the journal records, so a caller that drains the
@@ -10,7 +10,7 @@ import (
"github.com/luxfi/dex/pkg/lx"
)
// source_clob.go is the V4 native CLOB liquidity source — the in-process matcher.
// source_orderBook.go is the V4 native OrderBook liquidity source — the in-process matcher.
// It is the router's PRIMARY source: a marketable order crosses the resting book,
// each fill settles value-exactly inside the custody ledger (taker locked spend ->
// maker available; maker locked -> taker available), and the proceeds are recorded
@@ -22,27 +22,27 @@ import (
// against a deep-copied book so its predicted output equals what Execute produces —
// the router relies on quote==execute for the split it picks.
// CLOBSource matches against the in-trie resting book. It holds no state; the book
// OrderBookSource matches against the in-trie resting book. It holds no state; the book
// is rebuilt from the Store on each call (the rebuildable-accelerator discipline).
type CLOBSource struct{}
type OrderBookSource struct{}
// NewCLOBSource returns the CLOB liquidity source.
func NewCLOBSource() *CLOBSource { return &CLOBSource{} }
// NewOrderBookSource returns the OrderBook liquidity source.
func NewOrderBookSource() *OrderBookSource { return &OrderBookSource{} }
// Kind identifies this as the CLOB source.
func (*CLOBSource) Kind() SourceKind { return SourceCLOB }
// Kind identifies this as the OrderBook source.
func (*OrderBookSource) Kind() SourceKind { return SourceOrderBook }
// QuoteSwap rebuilds the market book, runs the matcher against a CLONE for up to
// remainingIn of the spend asset, and reports the output WITHOUT mutating state.
// Ok=false when the market is unknown, has no crossable depth, or the order yields
// no fills.
func (*CLOBSource) QuoteSwap(db Store, req SwapRequest, remainingIn uint64) (Quote, error) {
func (*OrderBookSource) QuoteSwap(db Store, req SwapRequest, remainingIn uint64) (Quote, error) {
symbol, exists, err := ReadMarketSymbol(db, req.PoolID)
if err != nil {
return Quote{}, err
}
if !exists {
return Quote{}, nil // no such market on the CLOB: not an error, just no fill
return Quote{}, nil // no such market on the OrderBook: not an error, just no fill
}
book, err := RebuildBook(db, req.PoolID, symbol)
if err != nil {
@@ -76,7 +76,7 @@ func (*CLOBSource) QuoteSwap(db Store, req SwapRequest, remainingIn uint64) (Quo
// the FULL swap input once (so a split across sources draws from one lock); this
// source only spends the taker's already-locked balance per fill and refunds
// nothing (the top-level refunds the unspent remainder after all sources run).
func (*CLOBSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j *Journal) (Fill, uint64, uint64, error) {
func (*OrderBookSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j *Journal) (Fill, uint64, uint64, error) {
symbol, exists, err := ReadMarketSymbol(db, req.PoolID)
if err != nil {
return Fill{}, 0, 0, err
@@ -137,7 +137,7 @@ func (*CLOBSource) ExecuteSwap(db Store, req SwapRequest, remainingIn uint64, j
j.CreditTakerOutput(outAsset, outGot)
_ = spent // spent == inUsed for the integer lane; kept for clarity
return Fill{Trades: fills, Source: SourceCLOB}, inUsed, outGot, nil
return Fill{Trades: fills, Source: SourceOrderBook}, inUsed, outGot, nil
}
// buildMarketableOrder constructs the ephemeral IOC order for a swap. The matcher
+43 -43
View File
@@ -40,9 +40,9 @@ func TestSwap_SellIntoRestingBid(t *testing.T) {
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD,
AmountIn: 80, OrderID: 1_000, TimestampN: 2_000,
Class: ClassPublicCLOB,
Class: ClassPublicDEX,
}
res, err := ExecuteSwap(db, clobRouter(), req)
res, err := ExecuteSwap(db, orderBookRouter(), req)
if err != nil {
t.Fatalf("ExecuteSwap: %v", err)
}
@@ -54,9 +54,9 @@ func TestSwap_SellIntoRestingBid(t *testing.T) {
if res.AmountOut != 4000 {
t.Fatalf("AmountOut = %d, want 4000 (80 LETH @ 50)", res.AmountOut)
}
// The fill came from the CLOB.
if len(res.Fills) != 1 || res.Fills[0].Source != SourceCLOB {
t.Fatalf("expected one CLOB fill, got %+v", res.Fills)
// The fill came from the OrderBook.
if len(res.Fills) != 1 || res.Fills[0].Source != SourceOrderBook {
t.Fatalf("expected one OrderBook fill, got %+v", res.Fills)
}
// Taker ledger: 100-80 = 20 LETH available, 4000 LUSD available.
@@ -94,14 +94,14 @@ func TestSwap_SellIntoRestingBid(t *testing.T) {
}
}
// Test9999Swap_FillsFromV4WhenCLOBLiquid asserts the router fills entirely from the
// V4 CLOB when the CLOB has the depth — the AMM is present but untouched.
func Test9999Swap_FillsFromV4WhenCLOBLiquid(t *testing.T) {
// Test9999Swap_FillsFromV4WhenOrderBookLiquid asserts the router fills entirely from the
// V4 OrderBook when the OrderBook has the depth — the AMM is present but untouched.
func Test9999Swap_FillsFromV4WhenOrderBookLiquid(t *testing.T) {
db := newStore()
pid := seedMarket(t, db, 2, tokLETH, tokLUSD)
maker := account(0xAA)
taker := account(0xBB)
// Deep CLOB bid: 1000 LETH @ 50.
// Deep OrderBook bid: 1000 LETH @ 50.
seedDeposit(t, db, maker, tokLUSD, 1_000_000)
restOrder(t, db, pid, maker, lx.Buy, 50, 1000, 1)
seedDeposit(t, db, taker, tokLETH, 100)
@@ -109,24 +109,24 @@ func Test9999Swap_FillsFromV4WhenCLOBLiquid(t *testing.T) {
// AMM with shallow reserves (would price worse).
pool := newMemAMM(0) // 0 bps fee
pool.set(pid, 10_000, 500_000)
router := NewRouter(NewCLOBSource(), NewAMMSource(pool))
router := NewRouter(NewOrderBookSource(), NewAMMSource(pool))
req := SwapRequest{
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 50, OrderID: 9, TimestampN: 1,
Class: ClassPublicCLOB,
Class: ClassPublicDEX,
}
res, err := ExecuteSwap(db, router, req)
if err != nil {
t.Fatalf("ExecuteSwap: %v", err)
}
// CLOB @ 50 gives 2500 LUSD for 50 LETH; the AMM would give less. All from CLOB.
// OrderBook @ 50 gives 2500 LUSD for 50 LETH; the AMM would give less. All from OrderBook.
if res.AmountOut != 2500 {
t.Fatalf("AmountOut = %d, want 2500 (all from CLOB @ 50)", res.AmountOut)
t.Fatalf("AmountOut = %d, want 2500 (all from OrderBook @ 50)", res.AmountOut)
}
for _, f := range res.Fills {
if f.Source == SourceAMM {
t.Fatalf("AMM was used despite a better CLOB price: %+v", res.Fills)
t.Fatalf("AMM was used despite a better OrderBook price: %+v", res.Fills)
}
}
// AMM reserves untouched.
@@ -137,22 +137,22 @@ func Test9999Swap_FillsFromV4WhenCLOBLiquid(t *testing.T) {
}
// Test9999Swap_FallsToV2V3WhenNoV4 asserts the router falls through to the AMM when
// the CLOB has NO crossable depth.
// the OrderBook has NO crossable depth.
func Test9999Swap_FallsToV2V3WhenNoV4(t *testing.T) {
db := newStore()
pid := seedMarket(t, db, 3, tokLETH, tokLUSD)
taker := account(0xBB)
seedDeposit(t, db, taker, tokLETH, 1000)
// No CLOB liquidity at all; AMM has reserves.
// No OrderBook liquidity at all; AMM has reserves.
pool := newMemAMM(0)
pool.set(pid, 100_000, 5_000_000) // price ~50 LUSD/LETH
router := NewRouter(NewCLOBSource(), NewAMMSource(pool))
router := NewRouter(NewOrderBookSource(), NewAMMSource(pool))
req := SwapRequest{
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 100, OrderID: 9, TimestampN: 1,
Class: ClassPublicCLOB,
Class: ClassPublicDEX,
}
res, err := ExecuteSwap(db, router, req)
if err != nil {
@@ -173,15 +173,15 @@ func Test9999Swap_FallsToV2V3WhenNoV4(t *testing.T) {
}
}
// Test9999Swap_BestExecAcrossV4AndAMM asserts the router SPLITS across the CLOB and
// the AMM when neither alone is best for the whole amount: it takes the CLOB depth
// Test9999Swap_BestExecAcrossV4AndAMM asserts the router SPLITS across the OrderBook and
// the AMM when neither alone is best for the whole amount: it takes the OrderBook depth
// at the better price, then the AMM for the remainder.
func Test9999Swap_BestExecAcrossV4AndAMM(t *testing.T) {
db := newStore()
pid := seedMarket(t, db, 4, tokLETH, tokLUSD)
maker := account(0xAA)
taker := account(0xBB)
// Thin but BETTER CLOB bid: 10 LETH @ 60 (better than AMM's ~50).
// Thin but BETTER OrderBook bid: 10 LETH @ 60 (better than AMM's ~50).
seedDeposit(t, db, maker, tokLUSD, 1_000_000)
restOrder(t, db, pid, maker, lx.Buy, 60, 10, 1)
seedDeposit(t, db, taker, tokLETH, 1000)
@@ -189,38 +189,38 @@ func Test9999Swap_BestExecAcrossV4AndAMM(t *testing.T) {
// AMM priced ~50.
pool := newMemAMM(0)
pool.set(pid, 100_000, 5_000_000)
router := NewRouter(NewCLOBSource(), NewAMMSource(pool))
router := NewRouter(NewOrderBookSource(), NewAMMSource(pool))
// Taker sells 30 LETH. CLOB absorbs 10 @ 60 = 600; AMM absorbs the other 20.
// Taker sells 30 LETH. OrderBook absorbs 10 @ 60 = 600; AMM absorbs the other 20.
req := SwapRequest{
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 30, OrderID: 9, TimestampN: 1,
Class: ClassPublicCLOB,
Class: ClassPublicDEX,
}
res, err := ExecuteSwap(db, router, req)
if err != nil {
t.Fatalf("ExecuteSwap: %v", err)
}
// Expect a CLOB fill of 10 LETH -> 600 LUSD and an AMM fill of 20 LETH.
var sawCLOB, sawAMM bool
var clobIn, ammIn uint64
// Expect a OrderBook fill of 10 LETH -> 600 LUSD and an AMM fill of 20 LETH.
var sawOrderBook, sawAMM bool
var orderBookIn, ammIn uint64
for _, f := range res.Fills {
switch f.Source {
case SourceCLOB:
sawCLOB = true
case SourceOrderBook:
sawOrderBook = true
for _, tr := range f.Trades {
clobIn += tr.BaseUnits.Uint64()
orderBookIn += tr.BaseUnits.Uint64()
}
case SourceAMM:
sawAMM = true
ammIn += f.AMMBaseDelta
}
}
if !sawCLOB || !sawAMM {
t.Fatalf("expected a split across CLOB+AMM, got %+v", res.Fills)
if !sawOrderBook || !sawAMM {
t.Fatalf("expected a split across OrderBook+AMM, got %+v", res.Fills)
}
if clobIn != 10 {
t.Fatalf("CLOB leg base = %d, want 10", clobIn)
if orderBookIn != 10 {
t.Fatalf("OrderBook leg base = %d, want 10", orderBookIn)
}
if ammIn != 20 {
t.Fatalf("AMM leg base = %d, want 20", ammIn)
@@ -228,10 +228,10 @@ func Test9999Swap_BestExecAcrossV4AndAMM(t *testing.T) {
if res.AmountIn != 30 {
t.Fatalf("total AmountIn = %d, want 30", res.AmountIn)
}
// Output = 600 (CLOB) + AMM curve(20).
// Output = 600 (OrderBook) + AMM curve(20).
ammOut := lx.ConstantProductOut(100_000, 5_000_000, 20)
if res.AmountOut != 600+ammOut {
t.Fatalf("AmountOut = %d, want %d (600 CLOB + %d AMM)", res.AmountOut, 600+ammOut, ammOut)
t.Fatalf("AmountOut = %d, want %d (600 OrderBook + %d AMM)", res.AmountOut, 600+ammOut, ammOut)
}
}
@@ -244,13 +244,13 @@ func Test9999Swap_NoLiquidityReverts(t *testing.T) {
pid := seedMarket(t, db, 5, tokLETH, tokLUSD)
taker := account(0xBB)
seedDeposit(t, db, taker, tokLETH, 100)
// No CLOB, no AMM.
// No OrderBook, no AMM.
req := SwapRequest{
PoolID: pid, TakerUser: taker, Side: lx.Sell,
Base: tokLETH, Quote: tokLUSD, AmountIn: 50, OrderID: 9, TimestampN: 1,
Class: ClassPublicCLOB,
Class: ClassPublicDEX,
}
_, err := ExecuteSwap(db, clobRouter(), req)
_, err := ExecuteSwap(db, orderBookRouter(), req)
if !errors.Is(err, ErrNoLiquidity) {
t.Fatalf("expected ErrNoLiquidity, got %v", err)
}
@@ -266,9 +266,9 @@ func Test9999Swap_BuyRequiresLimit(t *testing.T) {
req := SwapRequest{
PoolID: pid, TakerUser: taker, Side: lx.Buy,
Base: tokLETH, Quote: tokLUSD, AmountIn: 5000, OrderID: 9, TimestampN: 1,
Class: ClassPublicCLOB, // LimitPrice 0 -> refused
Class: ClassPublicDEX, // LimitPrice 0 -> refused
}
_, err := ExecuteSwap(db, clobRouter(), req)
_, err := ExecuteSwap(db, orderBookRouter(), req)
if !errors.Is(err, ErrBuyRequiresLimit) {
t.Fatalf("expected ErrBuyRequiresLimit, got %v", err)
}
@@ -296,9 +296,9 @@ func TestSwap_BuyWithLimit(t *testing.T) {
PoolID: pid, TakerUser: taker, Side: lx.Buy,
Base: tokLETH, Quote: tokLUSD, AmountIn: 5000,
LimitPrice: price(50), LimitIsUpper: true,
OrderID: 9, TimestampN: 1, Class: ClassPublicCLOB,
OrderID: 9, TimestampN: 1, Class: ClassPublicDEX,
}
res, err := ExecuteSwap(db, clobRouter(), req)
res, err := ExecuteSwap(db, orderBookRouter(), req)
if err != nil {
t.Fatalf("ExecuteSwap: %v", err)
}
+1 -1
View File
@@ -373,7 +373,7 @@ func (s *session) handleTestRequest(msg *Message) {
// emits ExecutionReport(s): an ack (ExecType=New) if nothing crossed and the
// order is fully open, then one report per fill (PartialFill/Fill), or a single
// Rejected report on a venue error. The order is submitted as a MARKETABLE order
// bounded by its limit price (IOC) — the faithful CLOB taker primitive that
// bounded by its limit price (IOC) — the faithful DEX taker primitive that
// crosses the resting book and returns fills, identical to the 0x9010 Swap path.
func (s *session) handleNewOrderSingle(ctx context.Context, msg *Message) {
clOrdID, _ := msg.Get(TagClOrdID)
+4 -4
View File
@@ -5,12 +5,12 @@
// the SERVER side of the FIX trading path, the protocol front-end the SDK's
// EnableFIX() client (sdk/go/client.go) talks to.
//
// It is ONE of four protocol front-ends onto the SAME matcher: the D-Chain CLOB
// (pkg/dchain) served over the FROZEN clob_* ZAP surface (pkg/zapwire). ZAP
// clients speak clob_* directly; the 0x9010 V4 precompile speaks it via
// It is ONE of four protocol front-ends onto the SAME matcher: the D-Chain DEX
// (pkg/dchain) served over the FROZEN dex_* ZAP surface (pkg/zapwire). ZAP
// clients speak dex_* directly; the 0x9010 V4 precompile speaks it via
// engine_zap; the WS server speaks it via a JSON translation; and FIX speaks it
// via this codec + acceptor. NONE of them owns a matcher — the acceptor decodes a
// FIX NewOrderSingle, calls the SAME clob_place / clob_submit the others call, and
// FIX NewOrderSingle, calls the SAME dex_place / dex_submit the others call, and
// renders the consensus-computed result back as a FIX ExecutionReport. DRY,
// orthogonal: the matcher lives in ONE place and FIX is purely a wire translation.
//
+1 -1
View File
@@ -30,7 +30,7 @@ const (
SwapRouterAddress = "0x0000000000000000000000000000000000009012" // LXRouter — swap router
HooksRegistryAddr = "0x0000000000000000000000000000000000009013" // LXHooks — pool hooks registry
FlashLoanAddress = "0x0000000000000000000000000000000000009014" // LXFlash — flash loans
CLOBAddress = "0x0000000000000000000000000000000000009020" // LXBook — central limit order book
OrderBookAddress = "0x0000000000000000000000000000000000009020" // LXBook — central limit order book
VaultAddress = "0x0000000000000000000000000000000000009030" // LXVault — clearinghouse, margin, positions
FeedAddress = "0x0000000000000000000000000000000000009040" // LXFeed — computed price feeds
LendAddress = "0x0000000000000000000000000000000000009050" // LXLend — lending pool
+5 -5
View File
@@ -17,7 +17,7 @@ type orderManager struct {
store *OrderStore
// Precompile addresses for building unsigned transactions
clobAddress string // LXBook precompile for limit orders
dexAddress string // LXBook precompile for limit orders
// Dutch auction reactor address for EIP-712 domain
reactorAddress string
@@ -30,7 +30,7 @@ type orderManager struct {
func newOrderManager(defaultChainID ChainID) *orderManager {
return &orderManager{
store: NewOrderStore(),
clobAddress: "0x0000000000000000000000000000000000009020", // LXBook
dexAddress: "0x0000000000000000000000000000000000009020", // LXBook
reactorAddress: "0x0000000000000000000000000000000000009021", // DutchOrderReactor
defaultChainID: defaultChainID,
}
@@ -189,7 +189,7 @@ func (s *Server) handleCancelOrder(w http.ResponseWriter, _ *http.Request, order
}
// buildLimitOrderTx constructs an unsigned transaction to place a limit order
// via the LXBook (CLOB) precompile.
// via the LXBook (DEX) precompile.
func (s *Server) buildLimitOrderTx(order *GatewayOrder) *TxData {
// Function selector: placeLimitOrder(address tokenIn, address tokenOut, uint256 amountIn, uint256 limitPrice, uint64 deadline, address recipient)
selector := crypto.Keccak256([]byte("placeLimitOrder(address,address,uint256,uint256,uint64,address)"))[:4]
@@ -206,7 +206,7 @@ func (s *Server) buildLimitOrderTx(order *GatewayOrder) *TxData {
data = append(data, padAddress(order.Recipient)...)
return &TxData{
To: s.orders.clobAddress,
To: s.orders.dexAddress,
Data: "0x" + hex.EncodeToString(data),
Value: "0",
ChainID: uint64(order.ChainID),
@@ -225,7 +225,7 @@ func (s *Server) buildCancelLimitOrderTx(order *GatewayOrder) *TxData {
data = append(data, padBytes32FromString(order.ID)...)
return &TxData{
To: s.orders.clobAddress,
To: s.orders.dexAddress,
Data: "0x" + hex.EncodeToString(data),
Value: "0",
ChainID: uint64(order.ChainID),
+1 -1
View File
@@ -636,7 +636,7 @@ func TestCreateLimitOrderHandler(t *testing.T) {
t.Fatal("did not expect EIP712 data for limit order")
}
if orderResp.Tx.To != "0x0000000000000000000000000000000000009020" {
t.Fatalf("expected CLOB address, got %s", orderResp.Tx.To)
t.Fatalf("expected DEX address, got %s", orderResp.Tx.To)
}
if orderResp.Tx.ChainID != 96369 {
t.Fatalf("expected chainId 96369, got %d", orderResp.Tx.ChainID)
+30 -30
View File
@@ -13,8 +13,8 @@ const (
NativePoolManager = "0x0000000000000000000000000000000000009010"
// NativeSwapRouter is the V4 swap router (ExactInput/Output).
NativeSwapRouter = "0x0000000000000000000000000000000000009012"
// NativeCLOB is the central limit order book precompile.
NativeCLOB = "0x0000000000000000000000000000000000009020"
// NativeOrderBook is the central limit order book precompile.
NativeOrderBook = "0x0000000000000000000000000000000000009020"
)
// quoteExactInputSingle(address,address,uint256,uint24)
@@ -23,12 +23,12 @@ var selectorNativeQuote = lxrMustDecodeHex("30d07f21")
// NativeDEXVenue quotes from the Lux V4 DEX precompiles via eth_call.
// It calls the PoolManager (0x9010) for AMM quotes and falls back to
// the CLOB (0x9020) for limit-order liquidity.
// the DEX (0x9020) for limit-order liquidity.
type NativeDEXVenue struct {
evm *EVMClient
name string
feeBPS uint32 // default fee tier for pool lookup
useCLOB bool // also query CLOB for better prices
evm *EVMClient
name string
feeBPS uint32 // default fee tier for pool lookup
useDEX bool // also query DEX for better prices
}
// NativeDEXConfig configures a NativeDEXVenue.
@@ -38,8 +38,8 @@ type NativeDEXConfig struct {
// FeeBPS is the default fee tier in basis points for pool discovery.
// Common values: 1 (0.01%), 5 (0.05%), 30 (0.30%), 100 (1%).
FeeBPS uint32
// UseCLOB enables querying the CLOB precompile (0x9020) alongside AMM.
UseCLOB bool
// UseDEX enables querying the DEX precompile (0x9020) alongside AMM.
UseDEX bool
}
// NewNativeDEXVenue creates a venue that queries Lux DEX precompiles.
@@ -49,10 +49,10 @@ func NewNativeDEXVenue(cfg NativeDEXConfig) *NativeDEXVenue {
feeBPS = 30 // 0.30% default
}
return &NativeDEXVenue{
evm: NewEVMClient(cfg.RPCURL),
name: "v4_native",
feeBPS: feeBPS,
useCLOB: cfg.UseCLOB,
evm: NewEVMClient(cfg.RPCURL),
name: "v4_native",
feeBPS: feeBPS,
useDEX: cfg.UseDEX,
}
}
@@ -106,13 +106,13 @@ func (v *NativeDEXVenue) Quote(ctx context.Context, req VenueQuoteRequest) (*Ven
Executable: true,
}
// If CLOB enabled, also check order book for better price.
if v.useCLOB {
clobQuote, err := v.queryCLOB(ctx, req, amountIn)
if err == nil && clobQuote != nil {
clobOut, _ := new(big.Int).SetString(clobQuote.AmountOut, 10)
if clobOut != nil && clobOut.Cmp(amountOut) > 0 {
return clobQuote, nil // CLOB has better price
// If DEX enabled, also check order book for better price.
if v.useDEX {
dexQuote, err := v.queryDEX(ctx, req, amountIn)
if err == nil && dexQuote != nil {
dexOut, _ := new(big.Int).SetString(dexQuote.AmountOut, 10)
if dexOut != nil && dexOut.Cmp(amountOut) > 0 {
return dexQuote, nil // DEX has better price
}
}
}
@@ -120,11 +120,11 @@ func (v *NativeDEXVenue) Quote(ctx context.Context, req VenueQuoteRequest) (*Ven
return quote, nil
}
// queryCLOB checks the CLOB precompile for limit-order liquidity.
// queryDEX checks the DEX precompile for limit-order liquidity.
// getBestQuote(address,address,uint256,bool)
var selectorCLOBQuote = lxrMustDecodeHex("a2e62045")
var selectorDEXQuote = lxrMustDecodeHex("a2e62045")
func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req VenueQuoteRequest, amountIn *big.Int) (*VenueQuote, error) {
func (v *NativeDEXVenue) queryDEX(ctx context.Context, req VenueQuoteRequest, amountIn *big.Int) (*VenueQuote, error) {
inAddr, _ := lxrDecodeAddress(req.TokenIn)
outAddr, _ := lxrDecodeAddress(req.TokenOut)
@@ -133,13 +133,13 @@ func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req VenueQuoteRequest, a
buyWord[31] = isBuy
calldata := make([]byte, 0, 132)
calldata = append(calldata, selectorCLOBQuote...)
calldata = append(calldata, selectorDEXQuote...)
calldata = append(calldata, lxrPadAddress(inAddr)...)
calldata = append(calldata, lxrPadAddress(outAddr)...)
calldata = append(calldata, lxrPadUint256(amountIn)...)
calldata = append(calldata, buyWord[:]...)
result, err := v.evm.CallContract(ctx, NativeCLOB, calldata)
result, err := v.evm.CallContract(ctx, NativeOrderBook, calldata)
if err != nil {
return nil, err
}
@@ -154,9 +154,9 @@ func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req VenueQuoteRequest, a
}
return &VenueQuote{
Venue: v.name + "_clob",
Venue: v.name + "_dex",
AmountOut: amountOut.String(),
Fee: "0", // CLOB maker fees may differ
Fee: "0", // DEX maker fees may differ
GasEstimate: "200000",
Executable: true,
}, nil
@@ -195,11 +195,11 @@ func init() {
if len(selectorNativeQuote) != 4 {
panic("selectorNativeQuote must be 4 bytes")
}
if len(selectorCLOBQuote) != 4 {
panic("selectorCLOBQuote must be 4 bytes")
if len(selectorDEXQuote) != 4 {
panic("selectorDEXQuote must be 4 bytes")
}
// Validate precompile addresses are valid hex.
for _, addr := range []string{NativePoolManager, NativeSwapRouter, NativeCLOB} {
for _, addr := range []string{NativePoolManager, NativeSwapRouter, NativeOrderBook} {
if _, err := hex.DecodeString(addr[2:]); err != nil {
panic("invalid precompile address: " + addr)
}
+1 -1
View File
@@ -65,7 +65,7 @@ func (ob *OrderBook) ConsensusAddOrder(order *Order) uint64 {
// addRestingLocked is the resting-insertion core shared by the consensus path. It
// validates the order, runs self-trade / post-only guards, inserts into the
// correct price tree under ob.mu, and tracks it in the order maps. It performs NO
// matching: a resting limit order placed via the CLOB never crosses on placement
// matching: a resting limit order placed via the OrderBook never crosses on placement
// (takers cross via SubmitMarketable). It assumes order.ID and order.RemainingSize
// are already set.
//
+1 -1
View File
@@ -736,7 +736,7 @@ func (ob *OrderBook) tryMatchImmediateLocked(order *Order) []Trade {
tradeSize = math.Min(order.RemainingSize, bestRemaining)
// EXACT-INTEGER QUANTITY LANE (value conservation). When BOTH the taker
// and the resting maker carry the integer quantity lane (CLOB / consensus
// and the resting maker carry the integer quantity lane (OrderBook / consensus
// path), the matched base is min(remainingUnits) computed in big.Int — the
// authoritative quantity, immune to the float64 precision loss that mints
// or burns value for 18-decimal token amounts above 2^53. The float
+9 -9
View File
@@ -5,16 +5,16 @@
package lx
// CUDA backend for the DEX CLOB match_order kernel. Linked via
// lux-dex-clob-cuda pkg-config bundle
// -> libdex_clob_cuda.a (luxcpp/dex/gpu/cuda/dex_clob_host.cu
// CUDA backend for the DEX OrderBook match_order kernel. Linked via
// lux-dex-orderbook-cuda pkg-config bundle
// -> libdex_orderbook_cuda.a (luxcpp/dex/gpu/cuda/dex_orderbook_host.cu
// + luxcpp/cuda/kernels/gpu/dex_swap.cu)
/*
#cgo pkg-config: lux-dex-clob-cuda
#cgo pkg-config: lux-dex-orderbook-cuda
#include <stdint.h>
#include "dex_clob_host.h"
#include "dex_orderbook_host.h"
*/
import "C"
@@ -25,7 +25,7 @@ import (
)
// gpuMatchOrder is the CUDA implementation called by orderbook_gpu.go.
// Returns errCLOBGPUUnsupported when no NVIDIA device is present so the
// Returns errOrderBookGPUUnsupported when no NVIDIA device is present so the
// dispatcher falls back to MatchOrderCPU.
func gpuMatchOrder(
incoming *DEXOrder,
@@ -53,7 +53,7 @@ func gpuMatchOrder(
pinner.Pin(&tradesWritten)
pinner.Pin(&remaining)
rc := C.dex_clob_match_order_host(
rc := C.dex_orderbook_match_order_host(
(*C.DEXOrder)(unsafe.Pointer(incoming)),
(*C.DEXOrder)(unsafe.Pointer(&book[0])),
(*C.uint32_t)(unsafe.Pointer(&bookIndices[0])),
@@ -84,8 +84,8 @@ func gpuMatchOrder(
}
return trades, remaining, nil
case -2:
return nil, 0, errCLOBGPUUnsupported
return nil, 0, errOrderBookGPUUnsupported
default:
return nil, 0, fmt.Errorf("lx: dex_clob_match_order_host rc=%d", int(rc))
return nil, 0, fmt.Errorf("lx: dex_orderbook_match_order_host rc=%d", int(rc))
}
}
+6 -6
View File
@@ -91,11 +91,11 @@ func TestMatchOrder_GPUMatchesCPU(t *testing.T) {
}
}
// clobTx is one operation in a deterministic CLOB transaction sequence: either
// orderBookTx is one operation in a deterministic OrderBook transaction sequence: either
// rest a limit order (place) or cross the book with a marketable order (submit).
// IDs and timestamps are supplied explicitly — exactly as the d-chain VM supplies
// them from block context — so the replay is a pure function of the sequence.
type clobTx struct {
type orderBookTx struct {
submit bool // false = place (rest), true = submit (cross)
id uint64
side Side
@@ -105,14 +105,14 @@ type clobTx struct {
timestamp time.Time
}
// replaySequence runs a CLOB tx sequence through a fresh book via the
// replaySequence runs a OrderBook tx sequence through a fresh book via the
// deterministic consensus entrypoints (ConsensusAddOrder for places,
// SubmitMarketable for submits, with VM-supplied ID/timestamp). It returns the
// concatenated fills (as canonical DEXTrade rows, taker side bound in) and the
// final book as canonical DEXOrder rows — the two byte-streams a validator
// commits. It NEVER calls AddOrder (would mint) or MatchOrders (would stamp
// time.Now()).
func replaySequence(symbol string, txs []clobTx) (fills []DEXTrade, rows []DEXOrder) {
func replaySequence(symbol string, txs []orderBookTx) (fills []DEXTrade, rows []DEXOrder) {
ob := NewOrderBook(symbol)
for _, tx := range txs {
if tx.submit {
@@ -139,7 +139,7 @@ func replaySequence(symbol string, txs []clobTx) (fills []DEXTrade, rows []DEXOr
}
// TestConsensusReplayDeterminism is the CRITICAL step-2 determinism proof: two
// replays of the IDENTICAL CLOB tx sequence produce BYTE-IDENTICAL fills and
// replays of the IDENTICAL OrderBook tx sequence produce BYTE-IDENTICAL fills and
// BYTE-IDENTICAL book rows. This is the property a multi-validator d-chain
// depends on — every honest validator that replays the same ordered block
// derives the same fills and the same committed state. The consensus path
@@ -151,7 +151,7 @@ func TestConsensusReplayDeterminism(t *testing.T) {
// A representative sequence: rest both sides, then cross with marketable
// orders that partially fill across multiple price levels, then rest more.
txs := []clobTx{
txs := []orderBookTx{
{false, 1, Sell, 101.0, 5.0, "maker-a", ts(0)},
{false, 2, Sell, 101.5, 3.0, "maker-b", ts(1)},
{false, 3, Sell, 102.0, 4.0, "maker-c", ts(2)},
+2 -2
View File
@@ -6,7 +6,7 @@
package lx
// gpuMatchOrder has no GPU backend on non-Linux or no-cgo builds. The
// dispatcher in orderbook_gpu.go turns errCLOBGPUUnsupported into a
// dispatcher in orderbook_gpu.go turns errOrderBookGPUUnsupported into a
// transparent fallback to MatchOrderCPU.
func gpuMatchOrder(
incoming *DEXOrder,
@@ -15,5 +15,5 @@ func gpuMatchOrder(
tradeIDBase uint64,
timestamp uint64,
) ([]DEXTrade, uint64, error) {
return nil, 0, errCLOBGPUUnsupported
return nil, 0, errOrderBookGPUUnsupported
}
+5 -5
View File
@@ -40,14 +40,14 @@ func MatchOrderGPU(
}
if backend.GPUDisabled() {
backend.RecordFallback(backend.FallbackDisabled, "clob")
backend.RecordFallback(backend.FallbackDisabled, "orderBook")
t, r := MatchOrderCPU(incoming, book, bookIndices, tradeIDBase, timestamp)
return t, r, nil
}
trades, remaining, err := gpuMatchOrder(incoming, book, bookIndices, tradeIDBase, timestamp)
if err == errCLOBGPUUnsupported {
backend.RecordFallback(backend.FallbackUnsupported, "clob")
if err == errOrderBookGPUUnsupported {
backend.RecordFallback(backend.FallbackUnsupported, "orderBook")
t, r := MatchOrderCPU(incoming, book, bookIndices, tradeIDBase, timestamp)
return t, r, nil
}
@@ -57,7 +57,7 @@ func MatchOrderGPU(
return trades, remaining, nil
}
// errCLOBGPUUnsupported is returned by per-platform gpuMatchOrder when
// errOrderBookGPUUnsupported is returned by per-platform gpuMatchOrder when
// no GPU dispatch is available. The dispatcher turns it into a CPU
// fallback rather than surfacing the error.
var errCLOBGPUUnsupported = fmt.Errorf("lx: CLOB CUDA dispatch not available on this host")
var errOrderBookGPUUnsupported = fmt.Errorf("lx: OrderBook CUDA dispatch not available on this host")
+1 -1
View File
@@ -120,7 +120,7 @@ func TestSubmitMarketableSweepAcrossTruncation(t *testing.T) {
// warm + sweepLots > tradeLogCap, the trim fires mid-match and (pre-fix) makes
// the start offset (== warm, which is > tradeLogKeep) stale -> panic.
const sweepLots = (tradeLogCap - warm) + 5000 // pushes warm+sweep over the cap
const sweepIDBase = 1_000_000 // disjoint from the warm maker IDs above
const sweepIDBase = 1_000_000 // disjoint from the warm maker IDs above
for j := 0; j < sweepLots; j++ {
ob.AddOrder(&Order{
ID: uint64(sweepIDBase + j), Type: Limit, Side: Sell, Price: 200, Size: 1, User: "mm-sweep",
+8 -8
View File
@@ -157,7 +157,7 @@ func dexToSide(s uint8) Side {
// typeToDEX maps the book OrderType to the DEXOrder type byte. Only the resting
// forms are persistable (a resting order is always a Limit by construction in
// the consensus CLOB path); Market/IOC/FOK takers never rest and so never reach
// the consensus OrderBook path); Market/IOC/FOK takers never rest and so never reach
// a row. Anything non-Limit maps to DEXOrderLimit defensively.
func typeToDEX(t OrderType) uint8 {
switch t {
@@ -302,13 +302,13 @@ func DecodeTrade(b []byte) (DEXTrade, bool) {
// by the caller because the in-RAM Trade does not always carry it.
func TradeToRow(t Trade, takerSide Side) DEXTrade {
row := DEXTrade{
TradeID: t.ID,
MakerUserID: userToUint64(makerUser(t, takerSide)),
TakerUserID: userToUint64(takerUser(t, takerSide)),
Price: floatToFixedPrice(t.Price),
Quantity: floatToFixedQty(t.Size),
Timestamp: uint64(t.Timestamp.UnixNano()),
TakerSide: sideToDEX(takerSide),
TradeID: t.ID,
MakerUserID: userToUint64(makerUser(t, takerSide)),
TakerUserID: userToUint64(takerUser(t, takerSide)),
Price: floatToFixedPrice(t.Price),
Quantity: floatToFixedQty(t.Size),
Timestamp: uint64(t.Timestamp.UnixNano()),
TakerSide: sideToDEX(takerSide),
}
// Maker/taker order IDs: the buy order is BuyOrder, the sell is SellOrder;
// the taker is whichever matches takerSide.
+1 -1
View File
@@ -97,7 +97,7 @@ func TestBookToRowsRoundTrip(t *testing.T) {
}
}
place(1, Buy, 100.0, 2.0, "alice", 0)
place(2, Buy, 100.0, 1.5, "bob", 1) // same level, later -> behind id1
place(2, Buy, 100.0, 1.5, "bob", 1) // same level, later -> behind id1
place(3, Buy, 99.5, 3.0, "carol", 2)
place(4, Sell, 101.0, 2.5, "dave", 3)
place(5, Sell, 101.5, 1.0, "erin", 4)
+2 -2
View File
@@ -6,7 +6,7 @@ package lx
import "math/big"
// settlement_units.go is the SINGLE home for the exact-integer quantity lane the
// CLOB / consensus path uses for value conservation. The matcher's price grid
// OrderBook / consensus path uses for value conservation. The matcher's price grid
// stays float64 (a ratio is fine as a float, and price-time priority is unchanged
// by it), but the SETTLED QUANTITY — the base units a fill moves and the quote
// units owed for them — must be exact: an 18-decimal token amount (e.g. 1e20)
@@ -47,7 +47,7 @@ func quoteUnitsFor(baseUnits *big.Int, priceInt PriceInt) *big.Int {
}
// ensureRemainingUnits lazily initializes an order's exact integer remainder from
// its SizeUnits the first time the matcher touches it on the CLOB path. An order
// its SizeUnits the first time the matcher touches it on the OrderBook path. An order
// without SizeUnits (legacy float API) yields nil — the integer lane stays off and
// the caller falls back to the float quantity, preserving legacy behavior.
func ensureRemainingUnits(o *Order) *big.Int {
+3 -3
View File
@@ -60,13 +60,13 @@ type Order struct {
Flags OrderFlags
ExecutedSize float64 // Total executed size
// Exact-integer quantity lane (CLOB / consensus path only). The float Size
// Exact-integer quantity lane (OrderBook / consensus path only). The float Size
// above is the price-grid/validation/legacy quantity; it loses precision for
// 18-decimal token amounts above 2^53. SizeUnits is the AUTHORITATIVE base
// quantity in atomic token units (no float, no rounding) and is the only
// quantity settlement is derived from. RemainingUnits tracks the unfilled
// integer remainder as the order crosses. Both are nil on the legacy float
// API path (AddOrder/MatchOrders), set on the CLOB path (ConsensusAddOrder /
// API path (AddOrder/MatchOrders), set on the OrderBook path (ConsensusAddOrder /
// SubmitMarketable) by the wire decoder. See settlement_units.go.
SizeUnits *big.Int
RemainingUnits *big.Int
@@ -88,7 +88,7 @@ type Trade struct {
MatchType string // Type of match (e.g., "normal", "liquidation")
TakerSide Side // Side that took liquidity
// Exact-integer execution truth (CLOB / consensus path). Price/Size above are
// Exact-integer execution truth (OrderBook / consensus path). Price/Size above are
// the float view; BaseUnits and QuoteUnits are the AUTHORITATIVE atomic-unit
// amounts this fill moved, computed with big.Int only (no float, see
// settlement_units.go: QuoteUnits = BaseUnits*priceInt/PriceMultiplier). These
@@ -5,7 +5,7 @@
// dexvm_gpu.go — direct dlopen + dlsym to the gpu-kernels DEX plugin.
//
// Why dlopen, not pkg-config: the AMM/CLOB host launchers live in a
// Why dlopen, not pkg-config: the AMM/OrderBook host launchers live in a
// per-backend plugin (libluxgpu_backend_{cuda,hip,metal,vulkan,webgpu})
// whose presence is platform-dependent — Apple has only Metal+Vulkan
// (via MoltenVK), Linux has CUDA+Vulkan+WebGPU, ROCm only on a small
@@ -21,16 +21,16 @@
//
// int amm_xyk_batch(const void* reserves, const void* amounts,
// void* outs, uint32_t n, void* stream);
// int dex_clob_match(void* arena, const uint8_t* calldata,
// int dex_orderbook_match(void* arena, const uint8_t* calldata,
// uint8_t* out, uint32_t* num_fills);
// int dex_clob_arena_create(void** out_arena);
// int dex_clob_arena_destroy(void* arena);
// int dex_orderbook_arena_create(void** out_arena);
// int dex_orderbook_arena_destroy(void* arena);
//
// All inputs are HOST pointers (no separate device address space is
// exposed across the ABI — the launcher does H2D/D2H copies
// internally). The `stream` argument on AMM is ignored by every
// backend except CUDA/HIP (where it would be a cudaStream_t / hipStream_t).
// `arena` for CLOB is opaque — created/destroyed via the symmetric
// `arena` for OrderBook is opaque — created/destroyed via the symmetric
// pair, persisted across calls so the BookArena lives device-resident
// for the lifetime of the book.
@@ -56,14 +56,14 @@ typedef int (*lux_amm_xyk_batch_fn)(
uint32_t n,
void* stream);
typedef int (*lux_dex_clob_match_fn)(
typedef int (*lux_dex_orderbook_match_fn)(
void* arena,
const uint8_t* calldata,
uint8_t* out,
uint32_t* num_fills);
typedef int (*lux_dex_clob_arena_create_fn)(void** out_arena);
typedef int (*lux_dex_clob_arena_destroy_fn)(void* arena);
typedef int (*lux_dex_orderbook_arena_create_fn)(void** out_arena);
typedef int (*lux_dex_orderbook_arena_destroy_fn)(void* arena);
static int call_amm_xyk_batch(
void* fn,
@@ -75,22 +75,22 @@ static int call_amm_xyk_batch(
return ((lux_amm_xyk_batch_fn)fn)(reserves, amounts, outs, n, NULL);
}
static int call_dex_clob_match(
static int call_dex_orderbook_match(
void* fn,
void* arena,
const uint8_t* calldata,
uint8_t* out,
uint32_t* num_fills)
{
return ((lux_dex_clob_match_fn)fn)(arena, calldata, out, num_fills);
return ((lux_dex_orderbook_match_fn)fn)(arena, calldata, out, num_fills);
}
static int call_dex_clob_arena_create(void* fn, void** out_arena) {
return ((lux_dex_clob_arena_create_fn)fn)(out_arena);
static int call_dex_orderbook_arena_create(void* fn, void** out_arena) {
return ((lux_dex_orderbook_arena_create_fn)fn)(out_arena);
}
static int call_dex_clob_arena_destroy(void* fn, void* arena) {
return ((lux_dex_clob_arena_destroy_fn)fn)(arena);
static int call_dex_orderbook_arena_destroy(void* fn, void* arena) {
return ((lux_dex_orderbook_arena_destroy_fn)fn)(arena);
}
// dlopen / dlsym wrappers. dlerror() returns thread-local NULL when
@@ -134,10 +134,10 @@ type gpuHandle struct {
hLib unsafe.Pointer // dlopen handle
fnAMMSwap unsafe.Pointer // lux_<backend>_amm_xyk_batch
fnCLOBMatch unsafe.Pointer // lux_<backend>_dex_clob_match
fnArenaCreate unsafe.Pointer // lux_<backend>_dex_clob_arena_create
fnArenaDestroy unsafe.Pointer // lux_<backend>_dex_clob_arena_destroy
fnAMMSwap unsafe.Pointer // lux_<backend>_amm_xyk_batch
fnOrderBookMatch unsafe.Pointer // lux_<backend>_dex_orderbook_match
fnArenaCreate unsafe.Pointer // lux_<backend>_dex_orderbook_arena_create
fnArenaDestroy unsafe.Pointer // lux_<backend>_dex_orderbook_arena_destroy
}
// globalGPU holds the result of the init() probe. nil after a clean
@@ -245,7 +245,7 @@ func pluginCandidatePaths(b GPUBackend) []string {
// dylib won't load OR any required symbol is missing. Symbol misses
// are treated as fatal-for-this-backend (we don't accept a partial
// plugin — that would mask a build skew and produce a "GPU loaded
// but CLOB returns garbage" failure mode that's worse than CPU).
// but OrderBook returns garbage" failure mode that's worse than CPU).
func tryLoadBackend(b GPUBackend) *gpuHandle {
candidates := pluginCandidatePaths(b)
for _, path := range candidates {
@@ -261,12 +261,12 @@ func tryLoadBackend(b GPUBackend) *gpuHandle {
prefix := "lux_" + b.String() + "_"
var (
fnAMM = dlsymOrNil(hLib, prefix+"amm_xyk_batch")
fnCLOB = dlsymOrNil(hLib, prefix+"dex_clob_match")
fnCreate = dlsymOrNil(hLib, prefix+"dex_clob_arena_create")
fnDestr = dlsymOrNil(hLib, prefix+"dex_clob_arena_destroy")
fnAMM = dlsymOrNil(hLib, prefix+"amm_xyk_batch")
fnOrderBook = dlsymOrNil(hLib, prefix+"dex_orderbook_match")
fnCreate = dlsymOrNil(hLib, prefix+"dex_orderbook_arena_create")
fnDestr = dlsymOrNil(hLib, prefix+"dex_orderbook_arena_destroy")
)
if fnAMM == nil || fnCLOB == nil || fnCreate == nil || fnDestr == nil {
if fnAMM == nil || fnOrderBook == nil || fnCreate == nil || fnDestr == nil {
// Partial export set — plausible cause is a stale plugin
// build that pre-dates the DEX launcher landing. Close
// the handle so we don't keep a dangling mapping, and
@@ -275,13 +275,13 @@ func tryLoadBackend(b GPUBackend) *gpuHandle {
continue
}
return &gpuHandle{
backend: b,
libPath: path,
hLib: hLib,
fnAMMSwap: fnAMM,
fnCLOBMatch: fnCLOB,
fnArenaCreate: fnCreate,
fnArenaDestroy: fnDestr,
backend: b,
libPath: path,
hLib: hLib,
fnAMMSwap: fnAMM,
fnOrderBookMatch: fnOrderBook,
fnArenaCreate: fnCreate,
fnArenaDestroy: fnDestr,
}
}
return nil
@@ -391,29 +391,29 @@ func AMMSwap(reserves []LuxAmmReservePair, amounts []uint64) ([]uint64, error) {
return outs, nil
}
// CLOBArena is the opaque handle to a device-resident BookArena. The
// OrderBookArena is the opaque handle to a device-resident BookArena. The
// pointer crosses the cgo boundary as void* — Go never dereferences
// it, only passes it to ArenaDestroy or CLOBMatch.
// it, only passes it to ArenaDestroy or OrderBookMatch.
//
// ArenaCreate hands ownership to the Go caller; ArenaDestroy releases
// it. The arena lives across many CLOBMatch calls — that's the
// it. The arena lives across many OrderBookMatch calls — that's the
// entire reason it's device-resident: the BookArena holds the
// resting bids/asks for a single book, and successive CLOBMatch
// resting bids/asks for a single book, and successive OrderBookMatch
// calls on the same book MUST share state.
type CLOBArena struct {
type OrderBookArena struct {
ptr unsafe.Pointer
}
// ArenaCreate allocates a fresh device-resident BookArena (zero
// initialized). The returned handle is owned by the caller; call
// ArenaDestroy to free it. Concurrent ArenaCreate is safe.
func ArenaCreate() (*CLOBArena, error) {
func ArenaCreate() (*OrderBookArena, error) {
h, err := activeGPU()
if err != nil {
return nil, err
}
var raw unsafe.Pointer
rc := C.call_dex_clob_arena_create(h.fnArenaCreate, &raw)
rc := C.call_dex_orderbook_arena_create(h.fnArenaCreate, &raw)
if rc != 0 {
return nil, fmt.Errorf("dexvm.ArenaCreate: %s plugin returned rc=%d",
h.backend, int(rc))
@@ -422,13 +422,13 @@ func ArenaCreate() (*CLOBArena, error) {
return nil, fmt.Errorf("dexvm.ArenaCreate: %s plugin returned NULL arena",
h.backend)
}
return &CLOBArena{ptr: raw}, nil
return &OrderBookArena{ptr: raw}, nil
}
// ArenaDestroy releases the device-resident BookArena. Calling it on
// a nil arena is a no-op (matches the C-side symmetry). Calling it
// twice on the same arena is UB at the plugin level — don't.
func ArenaDestroy(a *CLOBArena) error {
func ArenaDestroy(a *OrderBookArena) error {
if a == nil || a.ptr == nil {
return nil
}
@@ -436,7 +436,7 @@ func ArenaDestroy(a *CLOBArena) error {
if err != nil {
return err
}
rc := C.call_dex_clob_arena_destroy(h.fnArenaDestroy, a.ptr)
rc := C.call_dex_orderbook_arena_destroy(h.fnArenaDestroy, a.ptr)
a.ptr = nil
if rc != 0 {
return fmt.Errorf("dexvm.ArenaDestroy: %s plugin returned rc=%d",
@@ -445,7 +445,7 @@ func ArenaDestroy(a *CLOBArena) error {
return nil
}
// CLOBMatch runs one matcher step against the arena. calldata is the
// OrderBookMatch runs one matcher step against the arena. calldata is the
// 117-byte EVM precompile 0x100 input — see backend.go for the byte
// layout. Output is a fixed-size 68-byte buffer (filled + avg_price +
// num_fills) plus a num_fills uint32 mirror for callers that want it
@@ -454,17 +454,17 @@ func ArenaDestroy(a *CLOBArena) error {
// Single matcher pass per call; concurrency is across book_ids — each
// book gets its own arena, and independent arenas can be matched in
// parallel on different goroutines without serialization.
func CLOBMatch(a *CLOBArena, calldata []byte) (out [LuxCLOBOutLen]byte, numFills uint32, err error) {
func OrderBookMatch(a *OrderBookArena, calldata []byte) (out [OrderBookOutLen]byte, numFills uint32, err error) {
h, gerr := activeGPU()
if gerr != nil {
return out, 0, gerr
}
if a == nil || a.ptr == nil {
return out, 0, fmt.Errorf("dexvm.CLOBMatch: arena is nil")
return out, 0, fmt.Errorf("dexvm.OrderBookMatch: arena is nil")
}
if len(calldata) != LuxCLOBCalldataLen {
return out, 0, fmt.Errorf("dexvm.CLOBMatch: calldata len=%d, want %d",
len(calldata), LuxCLOBCalldataLen)
if len(calldata) != OrderBookCalldataLen {
return out, 0, fmt.Errorf("dexvm.OrderBookMatch: calldata len=%d, want %d",
len(calldata), OrderBookCalldataLen)
}
var pinner runtime.Pinner
@@ -473,8 +473,8 @@ func CLOBMatch(a *CLOBArena, calldata []byte) (out [LuxCLOBOutLen]byte, numFills
pinner.Pin(&out[0])
var nf C.uint32_t
rc := C.call_dex_clob_match(
h.fnCLOBMatch,
rc := C.call_dex_orderbook_match(
h.fnOrderBookMatch,
a.ptr,
(*C.uint8_t)(unsafe.Pointer(&calldata[0])),
(*C.uint8_t)(unsafe.Pointer(&out[0])),
@@ -483,7 +483,7 @@ func CLOBMatch(a *CLOBArena, calldata []byte) (out [LuxCLOBOutLen]byte, numFills
runtime.KeepAlive(calldata)
runtime.KeepAlive(out)
if rc != 0 {
return out, 0, fmt.Errorf("dexvm.CLOBMatch: %s plugin returned rc=%d",
return out, 0, fmt.Errorf("dexvm.OrderBookMatch: %s plugin returned rc=%d",
h.backend, int(rc))
}
return out, uint32(nf), nil
@@ -19,10 +19,10 @@ func AutoBackend() GPUBackend { return GPUBackendNone }
// GPUPluginPath returns "" under !cgo — no plugin was loaded.
func GPUPluginPath() string { return "" }
// CLOBArena is a zero-sized opaque type so the package surface
// OrderBookArena is a zero-sized opaque type so the package surface
// compiles uniformly under both build tags. The nocgo build never
// returns a non-nil arena from ArenaCreate.
type CLOBArena struct{}
type OrderBookArena struct{}
// AMMSwap returns ErrGPUNotAvailable under !cgo.
func AMMSwap(reserves []LuxAmmReservePair, amounts []uint64) ([]uint64, error) {
@@ -32,21 +32,21 @@ func AMMSwap(reserves []LuxAmmReservePair, amounts []uint64) ([]uint64, error) {
}
// ArenaCreate returns ErrGPUNotAvailable under !cgo.
func ArenaCreate() (*CLOBArena, error) {
func ArenaCreate() (*OrderBookArena, error) {
return nil, ErrGPUNotAvailable
}
// ArenaDestroy returns ErrGPUNotAvailable under !cgo. A nil arena is
// still a no-op for symmetry with the cgo build.
func ArenaDestroy(a *CLOBArena) error {
func ArenaDestroy(a *OrderBookArena) error {
if a == nil {
return nil
}
return ErrGPUNotAvailable
}
// CLOBMatch returns ErrGPUNotAvailable under !cgo.
func CLOBMatch(a *CLOBArena, calldata []byte) (out [LuxCLOBOutLen]byte, numFills uint32, err error) {
// OrderBookMatch returns ErrGPUNotAvailable under !cgo.
func OrderBookMatch(a *OrderBookArena, calldata []byte) (out [OrderBookOutLen]byte, numFills uint32, err error) {
_ = a
_ = calldata
return out, 0, ErrGPUNotAvailable
@@ -117,4 +117,3 @@ func TestGPUAMMSwapEmpty(t *testing.T) {
t.Fatalf("AMMSwap(nil, nil) returned len=%d, want 0", len(outs))
}
}
@@ -3,24 +3,24 @@
//go:build cgo
// dexvm_clob_parity_test.go — GPU-vs-CPU byte-parity KAT for the
// device-resident CLOB matcher.
// dexvm_orderBook_parity_test.go — GPU-vs-CPU byte-parity KAT for the
// device-resident OrderBook matcher.
//
// This is the load-bearing proof for the "full native GPU DEX, crypto in
// GPU memory" claim: the GPU CLOB path (CLOBMatch in dexvm_gpu.go, which
// dlsym's lux_<backend>_dex_clob_match and runs the matcher against a
// GPU memory" claim: the GPU OrderBook path (OrderBookMatch in dexvm_gpu.go, which
// dlsym's lux_<backend>_dex_orderbook_match and runs the matcher against a
// device-resident BookArena) MUST produce a 68-byte output that is
// byte-identical to the canonical CPU oracle for the same order sequence.
//
// The CPU oracle here is a self-contained Go port of the canonical C++
// oracle at
// ~/work/lux-private/gpu-kernels/tools/kat/dex_cpu_oracle.hpp
// ::clob_match_step_cpu
// ::orderBook_match_step_cpu
// which is itself the single source of truth that every GPU backend
// (cuda/hip/metal/vulkan/webgpu) and the C++ dex_match_cpu.cpp are held
// to. The Metal kernel
// ~/work/lux-private/gpu-kernels/ops/dex/metal/clob_match.metal
// ::clob_match_step
// ~/work/lux-private/gpu-kernels/ops/dex/metal/orderBook_match.metal
// ::orderBook_match_step
// implements the identical algorithm (same crossing rules, FIFO front-pop,
// price-sorted insertion: bids high→low, asks low→high; VWAP = 512/256
// shift-subtract long division). This test proves the Go bridge → plugin
@@ -30,7 +30,7 @@
//
// export LUX_GPU_PLUGIN_DIR=/Users/z/work/luxcpp/gpu/build/metal_backend
// export SDKROOT=$(xcrun --show-sdk-path)
// GOWORK=off go test ./dexvm/ -run TestCLOBGPUvsCPUByteParity -v
// GOWORK=off go test ./dexvm/ -run TestOrderBookGPUvsCPUByteParity -v
//
// When no plugin is on disk (AutoBackend()==None), the test SKIPs with the
// reason — the parity assertion needs a real GPU plugin; the CPU oracle is
@@ -46,7 +46,7 @@ import (
)
// =============================================================================
// Go CPU oracle — byte-equal port of dex_cpu_oracle.hpp::clob_match_step_cpu.
// Go CPU oracle — byte-equal port of dex_cpu_oracle.hpp::orderBook_match_step_cpu.
// uint256 is a 32-byte big-endian array, exactly the kernel-side layout.
// =============================================================================
@@ -223,25 +223,25 @@ func u256VWAP(notionalHi, notionalLo, total *u256) u256 {
// Fixed-cap 1024 levels per side, price-sorted (bids high→low, asks low→high).
type bookArena struct {
nBids, nAsks int
bidPrice [LuxCLOBMaxLevels]u256
bidQty [LuxCLOBMaxLevels]u256
askPrice [LuxCLOBMaxLevels]u256
askQty [LuxCLOBMaxLevels]u256
bidPrice [OrderBookMaxLevels]u256
bidQty [OrderBookMaxLevels]u256
askPrice [OrderBookMaxLevels]u256
askQty [OrderBookMaxLevels]u256
}
type clobOutput struct {
type orderBookOutput struct {
filled u256
avgPrice u256
numFills uint32
}
// clobMatchStepCPU is the byte-equal Go port of clob_match_step_cpu.
func clobMatchStepCPU(a *bookArena, side uint8, price, qty u256) clobOutput {
// orderBookMatchStepCPU is the byte-equal Go port of orderBook_match_step_cpu.
func orderBookMatchStepCPU(a *bookArena, side uint8, price, qty u256) orderBookOutput {
remaining := qty
var filled, notionalHi, notionalLo u256
numFills := uint32(0)
matchLoop := func(oppPrice, oppQty *[LuxCLOBMaxLevels]u256, nOpp *int, incomingIsBid bool) {
matchLoop := func(oppPrice, oppQty *[OrderBookMaxLevels]u256, nOpp *int, incomingIsBid bool) {
for *nOpp > 0 && !u256IsZero(&remaining) {
topPrice := oppPrice[0]
topQty := oppQty[0]
@@ -278,7 +278,7 @@ func clobMatchStepCPU(a *bookArena, side uint8, price, qty u256) clobOutput {
if side == 0 {
matchLoop(&a.askPrice, &a.askQty, &a.nAsks, true)
if !u256IsZero(&remaining) && a.nBids < LuxCLOBMaxLevels {
if !u256IsZero(&remaining) && a.nBids < OrderBookMaxLevels {
pos := a.nBids
for i := 0; i < a.nBids; i++ {
if u256Cmp(&a.bidPrice[i], &price) < 0 {
@@ -296,7 +296,7 @@ func clobMatchStepCPU(a *bookArena, side uint8, price, qty u256) clobOutput {
}
} else {
matchLoop(&a.bidPrice, &a.bidQty, &a.nBids, false)
if !u256IsZero(&remaining) && a.nAsks < LuxCLOBMaxLevels {
if !u256IsZero(&remaining) && a.nAsks < OrderBookMaxLevels {
pos := a.nAsks
for i := 0; i < a.nAsks; i++ {
if u256Cmp(&price, &a.askPrice[i]) < 0 {
@@ -314,37 +314,37 @@ func clobMatchStepCPU(a *bookArena, side uint8, price, qty u256) clobOutput {
}
}
return clobOutput{
return orderBookOutput{
filled: filled,
avgPrice: u256VWAP(&notionalHi, &notionalLo, &filled),
numFills: numFills,
}
}
// encodeCLOBOutput packs a clobOutput into the 68-byte wire format the GPU
// encodeOrderBookOutput packs a orderBookOutput into the 68-byte wire format the GPU
// kernel emits: filled(32 BE) | avg_price(32 BE) | num_fills(4 BE).
func encodeCLOBOutput(o clobOutput) [LuxCLOBOutLen]byte {
var out [LuxCLOBOutLen]byte
func encodeOrderBookOutput(o orderBookOutput) [OrderBookOutLen]byte {
var out [OrderBookOutLen]byte
copy(out[0:32], o.filled[:])
copy(out[32:64], o.avgPrice[:])
binary.BigEndian.PutUint32(out[64:68], o.numFills)
return out
}
// encodeCLOBCalldata builds the 117-byte EVM 0x100 calldata:
// encodeOrderBookCalldata builds the 117-byte EVM 0x100 calldata:
// side(1) | price(32 BE) | qty(32 BE) | user(20) | book_id(32).
func encodeCLOBCalldata(side uint8, price, qty u256, user [20]byte, bookID [32]byte) []byte {
cd := make([]byte, LuxCLOBCalldataLen)
func encodeOrderBookCalldata(side uint8, price, qty u256, user [20]byte, bookID [32]byte) []byte {
cd := make([]byte, OrderBookCalldataLen)
cd[0] = side
copy(cd[1:33], price[:])
copy(cd[33:65], qty[:])
copy(cd[65:85], user[:])
copy(cd[LuxCLOBBookIDOffset:LuxCLOBBookIDOffset+LuxCLOBBookIDLen], bookID[:])
copy(cd[OrderBookBookIDOffset:OrderBookBookIDOffset+OrderBookBookIDLen], bookID[:])
return cd
}
// clobStep is one order in a parity sequence.
type clobStep struct {
// orderBookStep is one order in a parity sequence.
type orderBookStep struct {
name string
side uint8 // 0 = bid, 1 = ask
price uint64
@@ -355,14 +355,14 @@ type clobStep struct {
// Self-check of the Go oracle — runs on every host (no GPU needed).
// =============================================================================
// TestCLOBOracleSelfConsistent verifies the Go oracle against hand-computed
// TestOrderBookOracleSelfConsistent verifies the Go oracle against hand-computed
// values, so a CPU-only host still proves the oracle is internally correct
// before it is used as the parity reference.
func TestCLOBOracleSelfConsistent(t *testing.T) {
func TestOrderBookOracleSelfConsistent(t *testing.T) {
var a bookArena
// 1) Rest an ask: 100 @ price 10. No cross (empty book).
out := clobMatchStepCPU(&a, 1, u256FromU64(10), u256FromU64(100))
out := orderBookMatchStepCPU(&a, 1, u256FromU64(10), u256FromU64(100))
if out.numFills != 0 || !u256IsZero(&out.filled) {
t.Fatalf("rest ask: expected no fill, got numFills=%d filled=%x", out.numFills, out.filled)
}
@@ -371,7 +371,7 @@ func TestCLOBOracleSelfConsistent(t *testing.T) {
}
// 2) Incoming bid 60 @ limit 12 crosses the ask. Fill 60 @ 10.
out = clobMatchStepCPU(&a, 0, u256FromU64(12), u256FromU64(60))
out = orderBookMatchStepCPU(&a, 0, u256FromU64(12), u256FromU64(60))
if out.numFills != 1 {
t.Fatalf("cross bid: numFills=%d, want 1", out.numFills)
}
@@ -392,7 +392,7 @@ func TestCLOBOracleSelfConsistent(t *testing.T) {
// 3) Big bid 200 @ limit 15: consumes the 40 remaining ask, residual 160
// rests as a bid. VWAP = 40*10/40 = 10.
out = clobMatchStepCPU(&a, 0, u256FromU64(15), u256FromU64(200))
out = orderBookMatchStepCPU(&a, 0, u256FromU64(15), u256FromU64(200))
if out.numFills != 1 {
t.Fatalf("drain ask: numFills=%d, want 1", out.numFills)
}
@@ -414,8 +414,8 @@ func TestCLOBOracleSelfConsistent(t *testing.T) {
// paritySequence is a fixed multi-order book that exercises: resting both
// sides, partial fills, full-level drain (front-pop), multi-level sweep, and
// non-trivial VWAP (averaging across two price levels).
func paritySequence() []clobStep {
return []clobStep{
func paritySequence() []orderBookStep {
return []orderBookStep{
{"rest ask 100@10", 1, 10, 100},
{"rest ask 50@11", 1, 11, 50},
{"rest ask 30@12", 1, 12, 30},
@@ -439,7 +439,7 @@ func paritySequence() []clobStep {
// device-resident arena, and asserts every 68-byte output is byte-identical
// to the Go CPU oracle stepping a parallel host-side arena. Returns the
// concatenated GPU outputs so the caller can prove determinism across arenas.
func runParity(t *testing.T, seq []clobStep) [][LuxCLOBOutLen]byte {
func runParity(t *testing.T, seq []orderBookStep) [][OrderBookOutLen]byte {
t.Helper()
arena, err := ArenaCreate()
@@ -458,20 +458,20 @@ func runParity(t *testing.T, seq []clobStep) [][LuxCLOBOutLen]byte {
var bookID [32]byte
bookID[31] = 1 // arbitrary stable book id
outs := make([][LuxCLOBOutLen]byte, 0, len(seq))
outs := make([][OrderBookOutLen]byte, 0, len(seq))
for _, s := range seq {
price := u256FromU64(s.price)
qty := u256FromU64(s.qty)
calldata := encodeCLOBCalldata(s.side, price, qty, user, bookID)
gpuOut, gpuNF, merr := CLOBMatch(arena, calldata)
calldata := encodeOrderBookCalldata(s.side, price, qty, user, bookID)
gpuOut, gpuNF, merr := OrderBookMatch(arena, calldata)
if merr != nil {
t.Fatalf("CLOBMatch[%s]: %v", s.name, merr)
t.Fatalf("OrderBookMatch[%s]: %v", s.name, merr)
}
// CPU oracle steps its parallel arena.
want := clobMatchStepCPU(&cpu, s.side, price, qty)
wantBytes := encodeCLOBOutput(want)
want := orderBookMatchStepCPU(&cpu, s.side, price, qty)
wantBytes := encodeOrderBookOutput(want)
if !bytes.Equal(gpuOut[:], wantBytes[:]) {
t.Errorf("BYTE PARITY MISMATCH at step %q:\n GPU : %x\n CPU : %x",
@@ -489,17 +489,17 @@ func runParity(t *testing.T, seq []clobStep) [][LuxCLOBOutLen]byte {
return outs
}
// TestCLOBGPUvsCPUByteParity is the headline proof: the device-resident GPU
// TestOrderBookGPUvsCPUByteParity is the headline proof: the device-resident GPU
// matcher returns byte-identical output to the canonical CPU oracle for a
// fixed multi-order book. Also proves arena lifecycle + determinism by
// replaying the same sequence on a second fresh arena and asserting identical
// GPU output across both runs.
func TestCLOBGPUvsCPUByteParity(t *testing.T) {
func TestOrderBookGPUvsCPUByteParity(t *testing.T) {
backend := AutoBackend()
if backend == GPUBackendNone {
t.Skipf("no GPU plugin loaded — set LUX_GPU_PLUGIN_DIR to a dir containing "+
"libluxgpu_backend_*.{dylib,so} (e.g. .../metal_backend). probe order: %v. "+
"CPU oracle is still self-checked by TestCLOBOracleSelfConsistent.",
"CPU oracle is still self-checked by TestOrderBookOracleSelfConsistent.",
probeOrder())
}
t.Logf("GPU backend=%s plugin=%s", backend, GPUPluginPath())
@@ -524,10 +524,10 @@ func TestCLOBGPUvsCPUByteParity(t *testing.T) {
}
}
// TestCLOBArenaLifecycle exercises create → (no match) → destroy and the
// TestOrderBookArenaLifecycle exercises create → (no match) → destroy and the
// double-destroy no-op contract, independent of any matching. Skips without
// a plugin.
func TestCLOBArenaLifecycle(t *testing.T) {
func TestOrderBookArenaLifecycle(t *testing.T) {
if AutoBackend() == GPUBackendNone {
t.Skipf("no GPU plugin loaded — set LUX_GPU_PLUGIN_DIR (probe order: %v)", probeOrder())
}
@@ -547,8 +547,8 @@ func TestCLOBArenaLifecycle(t *testing.T) {
t.Fatalf("ArenaDestroy (second, should be no-op): %v", err)
}
// Matching against a destroyed arena must error, not crash.
calldata := encodeCLOBCalldata(0, u256FromU64(1), u256FromU64(1), [20]byte{}, [32]byte{})
if _, _, merr := CLOBMatch(arena, calldata); merr == nil {
t.Fatal("CLOBMatch on destroyed arena: expected error, got nil")
calldata := encodeOrderBookCalldata(0, u256FromU64(1), u256FromU64(1), [20]byte{}, [32]byte{})
if _, _, merr := OrderBookMatch(arena, calldata); merr == nil {
t.Fatal("OrderBookMatch on destroyed arena: expected error, got nil")
}
}
+11 -11
View File
@@ -8,19 +8,19 @@
//
// 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
// 2. CPU/C++ : orderBookMatchStepCPU — 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,
// 3. GPU : OrderBookMatch 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
// so parity is asserted at the shared, model-independent abstraction every OrderBook
// 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
// TestOrderBookGPUvsCPUByteParity. This test adds the third leg — the lx.OrderBook
// pure-Go matcher — and ties all three together.
//
// Run (Metal, this Mac):
@@ -65,7 +65,7 @@ func u256ToU64(t *testing.T, v u256) uint64 {
// 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) {
func goOrderbookStep(ob *lx.OrderBook, s orderBookStep, 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 {
@@ -86,7 +86,7 @@ func goOrderbookStep(ob *lx.OrderBook, s clobStep, seq int64) (filled, vwap uint
}
// Capture the trade-log tail BEFORE matching so we slice exactly the trades
// this order produced. AddOrder matches on insert (continuous CLOB) and
// this order produced. AddOrder matches on insert (continuous OrderBook) and
// appends each fill to ob.Trades.
start := len(ob.Trades)
if ob.AddOrder(order) == 0 && order.Status == lx.Rejected {
@@ -123,7 +123,7 @@ func TestThreeModeMatchParity(t *testing.T) {
// --- 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.
// semantic the CPU oracle (orderBookMatchStepCPU) and the GPU kernel implement.
ob := lx.NewOrderBook("PARITY")
ob.EnableImmediateMatching = true
goOut := make([][2]uint64, len(seq))
@@ -136,7 +136,7 @@ func TestThreeModeMatchParity(t *testing.T) {
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))
out := orderBookMatchStepCPU(&cpu, s.side, u256FromU64(s.price), u256FromU64(s.qty))
cpuOut[i] = [2]uint64{u256ToU64(t, out.filled), u256ToU64(t, out.avgPrice)}
}
@@ -157,10 +157,10 @@ func TestThreeModeMatchParity(t *testing.T) {
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)
cd := encodeOrderBookCalldata(s.side, u256FromU64(s.price), u256FromU64(s.qty), user, bookID)
out, _, merr := OrderBookMatch(arena, cd)
if merr != nil {
t.Fatalf("CLOBMatch[%s]: %v", s.name, merr)
t.Fatalf("OrderBookMatch[%s]: %v", s.name, merr)
}
var filled, avg u256
copy(filled[:], out[0:32])
+16 -15
View File
@@ -2,7 +2,7 @@
// See the file LICENSE for licensing terms.
// Package lxgpu is the dlopen bridge to the gpu-kernels DEX plugin and the
// byte-parity proof harness for the native CLOB/AMM matchers.
// byte-parity proof harness for the native OrderBook/AMM matchers.
//
// It is the SINGLE home for the GPU dispatch + its KAT (known-answer test)
// against the deterministic CPU oracle. The CPU oracle is the byte-parity
@@ -68,25 +68,26 @@ var ErrGPUNotAvailable = errors.New("lxgpu: GPU plugin not available")
// LuxAmmReservePair mirrors include/lux/gpu/dex.h::LuxAmmReservePair.
// 16 bytes, packed (no padding), little-endian uint64 fields.
// MUST match:
// the GPU plugin install tree include/lux/gpu/dex.h::LuxAmmReservePair
// ~/work/lux/dex/pkg/lx/amm.go::ReservePair
// the GPU plugin install tree ops/dex/{cuda,metal,wgsl}/amm_xyk*
//
// the GPU plugin install tree include/lux/gpu/dex.h::LuxAmmReservePair
// ~/work/lux/dex/pkg/lx/amm.go::ReservePair
// the GPU plugin install tree ops/dex/{cuda,metal,wgsl}/amm_xyk*
type LuxAmmReservePair struct {
ReserveX uint64
ReserveY uint64
}
// CLOB ABI byte sizes from include/lux/gpu/dex.h.
// OrderBook ABI byte sizes from include/lux/gpu/dex.h.
const (
// LuxCLOBCalldataLen is the on-wire calldata for EVM precompile 0x100:
// OrderBookCalldataLen is the on-wire calldata for EVM precompile 0x100:
// side(1) | price(32 BE) | qty(32 BE) | user(20) | book_id(32) = 117.
LuxCLOBCalldataLen = 117
// LuxCLOBOutLen is the output: filled(32 BE) | avg_price(32 BE) | num_fills(4 BE) = 68.
LuxCLOBOutLen = 68
// LuxCLOBMaxLevels caps the per-side resting-level depth (bid_*/ask_* arrays).
LuxCLOBMaxLevels = 1024
// LuxCLOBBookIDOffset is where book_id starts inside calldata.
LuxCLOBBookIDOffset = 85
// LuxCLOBBookIDLen is the book_id width.
LuxCLOBBookIDLen = 32
OrderBookCalldataLen = 117
// OrderBookOutLen is the output: filled(32 BE) | avg_price(32 BE) | num_fills(4 BE) = 68.
OrderBookOutLen = 68
// OrderBookMaxLevels caps the per-side resting-level depth (bid_*/ask_* arrays).
OrderBookMaxLevels = 1024
// OrderBookBookIDOffset is where book_id starts inside calldata.
OrderBookBookIDOffset = 85
// OrderBookBookIDLen is the book_id width.
OrderBookBookIDLen = 32
)
+1 -1
View File
@@ -241,7 +241,7 @@ var errRecover = errors.New("rfq: secp256k1 recovery failed")
// over the SAME crypto.Ecrecover primitive the order path (pkg/lx/signed_order.go)
// and the D-Chain tx gate (pkg/dchain/auth.go) use. A venue message is its own
// domain (the domain* tags), so this recover is a leaf helper rather than a borrow
// of pkg/lx's tx-auth wrapper — which would couple this pure relay to the GPU CLOB
// of pkg/lx's tx-auth wrapper — which would couple this pure relay to the GPU DEX
// matcher. secp256k1 is the only scheme venue messages use.
func recoverAddr(digest [32]byte, sig []byte) (common.Address, error) {
if len(sig) != 65 || sig[64] > 1 { // ecrecover accepts only v ∈ {0,1}
+1 -1
View File
@@ -184,7 +184,7 @@ func (api *TradingAPI) isVenueExecutable(name string) bool {
}
func isV4Venue(name string) bool {
return name == VenueTypeV4Native || name == "v4_native_clob" || strings.HasPrefix(name, "lux_")
return name == VenueTypeV4Native || name == "v4_native_dex" || strings.HasPrefix(name, "lux_")
}
func isV2Venue(name string) bool {
+1 -1
View File
@@ -1165,7 +1165,7 @@ func TestVenueTypeClassification(t *testing.T) {
isV4 bool
}{
{"v4_native", false, false, true},
{"v4_native_clob", false, false, true},
{"v4_native_dex", false, false, true},
{"lux_amm", false, false, true},
{"uniswap_v2", true, false, false},
{"sushiswap", true, false, false},
+25 -25
View File
@@ -6,29 +6,29 @@ import (
"math/big"
)
// NativeCLOB is the V4 CLOB precompile address.
const NativeCLOB = "0x0000000000000000000000000000000000009020"
// NativeOrderBook is the V4 DEX precompile address.
const NativeOrderBook = "0x0000000000000000000000000000000000009020"
// quoteExactInputSingle(address,address,uint256,uint24)
var selectorNativeQuote = mustDecodeHex("30d07f21")
// getBestQuote(address,address,uint256,bool)
var selectorCLOBQuote = mustDecodeHex("a2e62045")
var selectorDEXQuote = mustDecodeHex("a2e62045")
// NativeDEXVenue quotes from the Lux V4 DEX precompiles via eth_call.
// Calls PoolManager (0x9010) for AMM quotes and optionally CLOB (0x9020).
// Calls PoolManager (0x9010) for AMM quotes and optionally DEX (0x9020).
type NativeDEXVenue struct {
evm *EVMClient
name string
feeBPS uint32
useCLOB bool
evm *EVMClient
name string
feeBPS uint32
useDEX bool
}
// NativeDEXConfig configures a NativeDEXVenue.
type NativeDEXConfig struct {
RPCURL string
FeeBPS uint32
UseCLOB bool
RPCURL string
FeeBPS uint32
UseDEX bool
}
func NewNativeDEXVenue(cfg NativeDEXConfig) *NativeDEXVenue {
@@ -37,10 +37,10 @@ func NewNativeDEXVenue(cfg NativeDEXConfig) *NativeDEXVenue {
feeBPS = 30
}
return &NativeDEXVenue{
evm: NewEVMClient(cfg.RPCURL),
name: "v4_native",
feeBPS: feeBPS,
useCLOB: cfg.UseCLOB,
evm: NewEVMClient(cfg.RPCURL),
name: "v4_native",
feeBPS: feeBPS,
useDEX: cfg.UseDEX,
}
}
@@ -90,12 +90,12 @@ func (v *NativeDEXVenue) Quote(ctx context.Context, req QuoteRequest) (*VenueQuo
Executable: true,
}
if v.useCLOB {
clobQuote, err := v.queryCLOB(ctx, req, amountIn)
if err == nil && clobQuote != nil {
clobOut, _ := new(big.Int).SetString(clobQuote.AmountOut, 10)
if clobOut != nil && clobOut.Cmp(amountOut) > 0 {
return clobQuote, nil
if v.useDEX {
dexQuote, err := v.queryDEX(ctx, req, amountIn)
if err == nil && dexQuote != nil {
dexOut, _ := new(big.Int).SetString(dexQuote.AmountOut, 10)
if dexOut != nil && dexOut.Cmp(amountOut) > 0 {
return dexQuote, nil
}
}
}
@@ -103,7 +103,7 @@ func (v *NativeDEXVenue) Quote(ctx context.Context, req QuoteRequest) (*VenueQuo
return quote, nil
}
func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req QuoteRequest, amountIn *big.Int) (*VenueQuote, error) {
func (v *NativeDEXVenue) queryDEX(ctx context.Context, req QuoteRequest, amountIn *big.Int) (*VenueQuote, error) {
inAddr, _ := decodeAddress(req.TokenIn)
outAddr, _ := decodeAddress(req.TokenOut)
@@ -111,13 +111,13 @@ func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req QuoteRequest, amount
buyWord[31] = 1
calldata := make([]byte, 0, 132)
calldata = append(calldata, selectorCLOBQuote...)
calldata = append(calldata, selectorDEXQuote...)
calldata = append(calldata, padAddress(inAddr)...)
calldata = append(calldata, padAddress(outAddr)...)
calldata = append(calldata, padUint256(amountIn)...)
calldata = append(calldata, buyWord[:]...)
result, err := v.evm.CallContract(ctx, NativeCLOB, calldata)
result, err := v.evm.CallContract(ctx, NativeOrderBook, calldata)
if err != nil {
return nil, err
}
@@ -131,7 +131,7 @@ func (v *NativeDEXVenue) queryCLOB(ctx context.Context, req QuoteRequest, amount
}
return &VenueQuote{
Venue: v.name + "_clob",
Venue: v.name + "_dex",
AmountOut: amountOut.String(),
Fee: "0",
GasEstimate: "200000",
+19 -19
View File
@@ -2,14 +2,14 @@
// See the file LICENSE for licensing terms.
// Package venue is the protocol-neutral seam between a wire front-end and THE
// matcher. It defines ONE interface — the four operations of the FROZEN clob_*
// matcher. It defines ONE interface — the four operations of the FROZEN dex_*
// ZAP surface (pkg/zapwire) — and ONE production implementation that dials the
// venue's clob_* endpoint. Every protocol front-end (FIX in pkg/fix, the WS
// venue's dex_* endpoint. Every protocol front-end (FIX in pkg/fix, the WS
// venue handler in pkg/api, the 0x9010 precompile in lux/precompile/dex)
// translates its dialect into these calls and renders the consensus-computed
// result back; none of them owns a matcher. This is the decomplection: the
// matcher and book live in ONE place (pkg/dchain), reached over ONE surface
// (clob_*), and a "path" is purely a wire translation onto this Venue.
// (dex_*), and a "path" is purely a wire translation onto this Venue.
package venue
import (
@@ -21,17 +21,17 @@ import (
)
// Venue is the protocol-neutral seam between a wire front-end (FIX in pkg/fix, WS
// in pkg/api) and THE matcher. It is the SAME four operations the FROZEN clob_*
// in pkg/api) and THE matcher. It is the SAME four operations the FROZEN dex_*
// ZAP surface exposes (pkg/zapwire) — ensure-market, place a resting limit order,
// cancel a resting order, submit a marketable order and get its fills. A front-end
// translates its protocol into these calls; it does NOT match. There is exactly
// ONE matcher (the D-Chain CLOB) and exactly ONE network surface onto it (clob_*);
// ONE matcher (the D-Chain DEX) and exactly ONE network surface onto it (dex_*);
// Venue is that surface expressed as a Go interface so a front-end can be unit
// tested against a fake and run in production against the real ZAP client.
//
// Side/poolID semantics are the zapwire ones: side 0=buy, 1=sell; poolID is the
// 32-byte market identity. price/size are float64 on the price grid (the same
// units clob_place/clob_submit carry); user is the <=16-byte account handle whose
// units dex_place/dex_submit carry); user is the <=16-byte account handle whose
// ledger the order locks against and settles into.
type Venue interface {
// EnsureMarket idempotently creates the book for poolID.
@@ -49,17 +49,17 @@ type Venue interface {
// the matcher would not rest, or a cancel of an unknown order).
var ErrRejected = fmt.Errorf("venue: rejected")
// zapVenue is the production Venue: a clob_* ZAP client over github.com/luxfi/rpc,
// dialing the SAME rpc.Listen socket dchain.VM.RegisterCLOB serves and the SAME
// zapVenue is the production Venue: a dex_* ZAP client over github.com/luxfi/rpc,
// dialing the SAME rpc.Listen socket dchain.VM.RegisterDEX serves and the SAME
// socket the 0x9010 precompile (engine_zap) and the chains/dexvm proxy dial. It
// holds no matcher state — every call is a frozen zapwire frame to the venue.
type zapVenue struct {
conn *rpc.ZAPConn
}
// DialVenue opens a ZAP client to the venue's clob_* endpoint (e.g. the addr
// DialVenue opens a ZAP client to the venue's dex_* endpoint (e.g. the addr
// returned by rpc.Listen / the node runtime). The returned Venue speaks the
// frozen clob_* frames; Close releases the connection.
// frozen dex_* frames; Close releases the connection.
func DialVenue(ctx context.Context, addr string) (Venue, error) {
conn, err := rpc.ZAPDial(ctx, addr)
if err != nil {
@@ -78,10 +78,10 @@ func (v *zapVenue) Close() error { return v.conn.Close() }
func (v *zapVenue) EnsureMarket(ctx context.Context, poolID [32]byte) error {
resp, err := v.conn.Call(ctx, zapwire.MethodEnsureMarket, zapwire.EncodeEnsureMarket(poolID))
if err != nil {
return fmt.Errorf("fix: clob_ensure_market: %w", err)
return fmt.Errorf("fix: dex_ensure_market: %w", err)
}
if _, status, derr := zapwire.DecodeAck(resp); derr != nil {
return fmt.Errorf("fix: clob_ensure_market ack: %w", derr)
return fmt.Errorf("fix: dex_ensure_market ack: %w", derr)
} else if status == zapwire.StatusRejected {
return fmt.Errorf("%w: ensure_market", ErrRejected)
}
@@ -91,11 +91,11 @@ func (v *zapVenue) EnsureMarket(ctx context.Context, poolID [32]byte) error {
func (v *zapVenue) Place(ctx context.Context, poolID [32]byte, side uint8, price, size float64, user string) (uint64, error) {
resp, err := v.conn.Call(ctx, zapwire.MethodPlace, zapwire.EncodePlace(poolID, side, price, size, user))
if err != nil {
return 0, fmt.Errorf("fix: clob_place: %w", err)
return 0, fmt.Errorf("fix: dex_place: %w", err)
}
orderID, status, derr := zapwire.DecodeAck(resp)
if derr != nil {
return 0, fmt.Errorf("fix: clob_place ack: %w", derr)
return 0, fmt.Errorf("fix: dex_place ack: %w", derr)
}
if status == zapwire.StatusRejected {
return 0, fmt.Errorf("%w: place", ErrRejected)
@@ -106,11 +106,11 @@ func (v *zapVenue) Place(ctx context.Context, poolID [32]byte, side uint8, price
func (v *zapVenue) Cancel(ctx context.Context, poolID [32]byte, orderID uint64) error {
resp, err := v.conn.Call(ctx, zapwire.MethodCancel, zapwire.EncodeCancel(poolID, orderID))
if err != nil {
return fmt.Errorf("fix: clob_cancel: %w", err)
return fmt.Errorf("fix: dex_cancel: %w", err)
}
_, status, derr := zapwire.DecodeAck(resp)
if derr != nil {
return fmt.Errorf("fix: clob_cancel ack: %w", derr)
return fmt.Errorf("fix: dex_cancel ack: %w", derr)
}
if status != zapwire.StatusCanceled {
return fmt.Errorf("%w: cancel order %d", ErrRejected, orderID)
@@ -121,11 +121,11 @@ func (v *zapVenue) Cancel(ctx context.Context, poolID [32]byte, orderID uint64)
func (v *zapVenue) Submit(ctx context.Context, poolID [32]byte, side uint8, isMarket bool, limit, size float64, user string) ([]zapwire.Fill, error) {
resp, err := v.conn.Call(ctx, zapwire.MethodSubmit, zapwire.EncodeSubmit(poolID, side, isMarket, limit, size, user))
if err != nil {
return nil, fmt.Errorf("fix: clob_submit: %w", err)
return nil, fmt.Errorf("fix: dex_submit: %w", err)
}
fills, err := zapwire.DecodeFills(resp)
if err != nil {
return nil, fmt.Errorf("fix: clob_submit fills decode: %w", err)
return nil, fmt.Errorf("fix: dex_submit fills decode: %w", err)
}
return fills, nil
}
@@ -147,7 +147,7 @@ func SymbolToPoolID(symbol string) [32]byte {
// UserHandle truncates a sender/account identity to the <=16-byte on-wire user
// handle the venue ledger keys balances by. Two distinct senders that share a
// 16-byte prefix would collide; production binds a checked account id, but for the
// CLOB user field the 16-byte handle is the canonical width (zapwire.UserSize).
// DEX user field the 16-byte handle is the canonical width (zapwire.UserSize).
func UserHandle(s string) string {
if len(s) > zapwire.UserSize {
return s[:zapwire.UserSize]
+6 -6
View File
@@ -13,11 +13,11 @@ import (
"github.com/luxfi/rpc"
)
// fakeCLOB stands up a real rpc.Listen server with canned clob_* handlers, so the
// fakeDEX stands up a real rpc.Listen server with canned dex_* handlers, so the
// production zapVenue is exercised over a REAL ZAP socket (the same transport
// production uses) without standing up the cgo matcher. It proves zapVenue's frame
// encode + ack/fill decode + error mapping, end to end over the wire.
type fakeCLOB struct {
type fakeDEX struct {
server rpc.Server
cancel context.CancelFunc
placeID uint64
@@ -25,13 +25,13 @@ type fakeCLOB struct {
cancelOK bool
}
func startFakeCLOB(t *testing.T) (*fakeCLOB, string) {
func startFakeDEX(t *testing.T) (*fakeDEX, string) {
t.Helper()
srv, err := rpc.Listen("127.0.0.1:0")
if err != nil {
t.Fatalf("rpc.Listen: %v", err)
}
f := &fakeCLOB{server: srv, placeID: 42, cancelOK: true}
f := &fakeDEX{server: srv, placeID: 42, cancelOK: true}
must := func(method string, h rpc.RawHandler) {
if err := srv.RegisterRaw(method, h); err != nil {
@@ -62,7 +62,7 @@ func startFakeCLOB(t *testing.T) (*fakeCLOB, string) {
}
func TestZAPVenueRoundTrip(t *testing.T) {
f, addr := startFakeCLOB(t)
f, addr := startFakeDEX(t)
f.fills = []zapwire.Fill{{Price: 100, Size: 3, TakerSide: zapwire.SideBuy}}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -100,7 +100,7 @@ func TestZAPVenueRoundTrip(t *testing.T) {
// TestZAPVenueCancelRejectedMapsToErrRejected proves a non-canceled cancel ack is
// surfaced as ErrRejected, not a silent success.
func TestZAPVenueCancelRejectedMapsToErrRejected(t *testing.T) {
f, addr := startFakeCLOB(t)
f, addr := startFakeDEX(t)
f.cancelOK = false
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

Some files were not shown because too many files have changed in this diff Show More