A chain's pools say what a pair trades at and nothing about what is listed on
it: an AMM has no list. Our own chains have an indexer that keeps one, and
every other chain had nothing — so an interface switched to Ethereum asked for
nothing at all and drew an empty table, which is exactly what a reader has been
looking at. The hosted API would answer and refuses without a contract.
CoinGecko publishes both halves openly: a per-chain token list with names,
decimals and logos, and a price by contract address with the day's change and
volume. No key, no account; a key only raises the rate limit. Measured on the
open endpoints: 5,821 tokens on Ethereum, 4,214 on BNB, 2,679 on Base, every
one with a logo.
The lists are large and change slowly, so they are held for six hours; a price
is a reading of a market and is held for a minute. Both are held in the gateway
rather than in a browser, so a thousand readers are one request to a service
that asks nothing of us. A token it does not price is absent from the answer
rather than present at zero — a screen can draw a dash, and it cannot un-draw a
wrong number. One batch refused is not every price unknown.
Measured through the running binary: /v1/trade/tokens?chainId=1 answers WETH,
USDC, 1INCH, AAVE with logos; a search for USDC returns USD Coin alone; and the
price still comes from our own pool read — WETH at 2474.5369 via uniswap_v3
against USDC — with this provider supplying only what the pool cannot.
Co-authored-by: Hanzo Dev <[email protected]>
/v1/trade/swap asked the hosted provider registry for a price. This deployment
registers none on purpose, so it answered 500 while /v1/trade/quote priced the
same pair from the pools a line away — and then encoded whatever came back for
the PoolManager precompile at 0x9010 regardless of where the price came from. A
router on Ethereum quoted and a precompile on our chain addressed, in one
response. 0x9010 answers 0x on 96369, measured with the venue's own quoter
selector, so the calldata named a contract that is not there.
Venue has a Swap method now. Each one builds against the contract it read: the
V3 arm against the SwapRouter02 beside its quoter, the V2 arm against the same
Router02 that answered getAmountsOut. A venue that prices a market it cannot
settle returns nothing, and the surface says so naming it.
The selector was wrong in the way that spends money. calldata_lxrouter.go sent
04e45aaf under a signature it called ExactInputSingle(address,address,uint256,
uint256,uint160,bytes32); that hashes to f48f227e. 04e45aaf is SwapRouter02's
exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) — a
seven-field tuple. A router handed the six-field form reads amountIn where the
fee tier belongs and amountOutMinimum where the recipient belongs. Those
builders were reachable only from their own tests; the padding and decoding
survive in abi.go.
Proved end to end: the gateway's own calldata replayed through eth_call against
96369's real SwapRouter02, from the account holding the allowance, returns
997001483075056712 — the number the quote gave. The tier travels on
route[0].fee because a V3 quote asks four tiers and keeps one, and a swap that
does not name the same pool executes at a different price. amountOutMinimum is
99.5% of the quote by default, because a router handed the quoted amount as its
floor reverts on the movement the caller's own trade causes.
Permit2 is at one address everywhere it exists and does not exist everywhere:
9152 bytes on Ethereum, nothing on 96369. Both paths check first, so our chain
gets a sentence instead of calldata addressed to an empty account.
Nine more paths are gone because they could not work here. Five answered 500
"no providers available" — tokens, pools, pool/, positions, stats, price,
prices — because enumerating a chain is an index read and this reads one pool
by address, right now. api-explore.lux.cloud already answers all of it: 16
pools, $39.9M of TVL, real symbols and decimals for 96369. /v1/route is the
same read in a hat and returned {"routes":[]} forever, which a caller reads as
"no path exists" rather than "I cannot answer". The order paths were an
in-memory map behind two replicas, so an order placed on one pod was a 404 on
the other about half the time and every order died on restart — the D-Chain
venue is the order book and it is deployed beside this. The position builders
all addressed 0x9010.
Eight paths remain and all eight were exercised against the live chain.
Co-authored-by: Hanzo Dev <[email protected]>
Three things claimed to be this repo's trading API and no two agreed.
pkg/trading carried its own Venue interface, its own V4/native/V2/V3 and broker
venues, its own constant-product math and its own eth_call client — a copy of
what pkg/gateway already had — and nothing mounted it: grep for the import
outside that directory returns empty. api/openapi.yaml documented it anyway, so
the one published contract in the repo described the one package no binary
served. Meanwhile the binary answered at /v1/* and again at /trading/* in
Uniswap's shapes: two contracts for one API, reachable by both names, free to
drift. Deleted, both of them, rather than picked between and left beside.
Everything answers under /v1/trade now. api.lux.cloud carries the chain at
/v1/chain, the C-chain at the root and other services besides, so /v1/quote was
a generic name claimed on a shared host; one prefix is one namespace with one
owner, and the edge rule that publishes it collapses from a nine-clause
allowlist to a prefix. Server.routes() is the surface written once, and
TestTheDocumentIsTheSurface reads api/openapi.yaml and holds the two to each
other in both directions.
/v1/trade/venues is new. It is the question a screen asks before it asks
anything else — which chains this deployment can price, and from what — grouped
by chain, because a venue is a contract on a chain and the same name means a
different pool on each. Without it a client learns a chain is unquotable by
quoting on it and reading an error.
The allowance is read rather than assumed. approval/check and permit2/check
answered allowance 0 and needsApproval on every request, under a comment saying
the gateway had no way to reach a chain; it has one per chain, one field away —
the same endpoint the pool is quoted from. Measured against 96369: LUSD to the
position manager reads 6000000000000000000000000, which is what eth_call
returns for that pair, and the approve tx now rides along only when it is
needed. Handed back beside "you already approved" it is a transaction somebody
sends, and the user pays gas for a state change that changes nothing before
every swap. Permit2 reads the expiration too, because a grant that has run out
is not a grant.
Nothing here invents a number any more. /v1/history/prices and /v1/history/tvl
returned a random walk seeded from hardcoded base prices — LUX at 2.47, WBTC at
96420, 12.5M of TVL on a chain whose pools were seeded hours ago — and both
were routed to the public internet, where a chart drawn from them is
indistinguishable from a chart of the market. History belongs to the indexer,
which reads the chain. /v1/tokens/search answered 501 on a TODO, a route lying
about the surface in the other direction. Gone, with a test that keeps them
gone.
Co-authored-by: Hanzo Dev <[email protected]>
The hosted API answers ACCESS_DENIED without a contract, which made every
market outside our own chain unquotable and turned a pair with no pool into an
error about somebody else's quota. A pool answers anyone. So the gateway reads
contracts: our chain through the V4 precompiles, with V2 beside them when a
router is deployed, and every other chain through the V2 router and the V3
quoter Uniswap published there. The hosted provider is registered only when a
key is given, because a provider that always refuses is worse than none.
V3 was the missing arm and it is where the liquidity is. The gateway could read
a V2 router and our precompiles and nothing in between, so most markets on most
chains were invisible. The venue asks each of the four fee tiers and keeps the
best — which tier holds a pair is a fact about that pair — and a tier with no
pool reverts, which is the ordinary answer to three of the four rather than a
failure to report.
Venues are per chain now. One router for all of them could not tell which chain
it was answering about, so the shape only ever fit a single-chain deployment.
Each chain names its RPC, its router and its quoter, and any of them can be
overridden by name.
Quotes are cached for ten seconds. A pool moves when somebody trades in it, so
the window is short by nature; what it saves is the second reader of the same
pair in the same second, and every eth_call saved is one a public endpoint is
entitled to refuse. Measured: 1.26s cold, 22ms warm.
And a route names the venue that answered. The router's own routing label
describes its strategy, so a V3 pool on Ethereum came back marked V4_NATIVE —
our precompile, on somebody else's chain. That is the one thing a route must
never misreport, since it is what a reader checks to see where their order goes.
Measured live, no key: 1 WETH is 2494.500698 USDC on Ethereum via uniswap_v3,
2494.591730 on Base.
Co-authored-by: Hanzo Dev <[email protected]>
pkg/gateway has carried the whole thing for a while — quote, swap, approval,
orders, pools, prices, tokens, stats, history — with a Uniswap provider that
speaks the shapes at trade-api.gateway.uniswap.org, and nothing ever started
it. `dexd run -http` mounts the D-Chain venue's own handlers under /v1/dex,
which is a different surface for a different reader, so an interface asking for
a quote reached nothing on any chain. It has an entrypoint now.
Two things it did wrong the moment it ran, both the same mistake in different
places. The router asked every provider about every chain, so a quote for a Lux
market went to Uniswap and came back ACCESS_DENIED — somebody else's API error
dressed as a fact about our liquidity. A provider is asked only about the
chains it lists now, and one that lists none is taken to serve any, which is
the shape a single-chain deployment has.
And the venues were attached to the server and never read: `s.venues` was
stored by an option nothing consulted, so the chain we settle ourselves was
quoted by the chain we do not. Our own venues answer first — an upstream
provider that also lists the chain is a second opinion about a market we
settle — and a venue holding no liquidity returns nothing rather than an
error, so falling through to the providers stays the honest next question.
A chain of ours with no provider behind it then says no venue holds the pair,
rather than "no providers available", which sends a reader looking for a
misconfiguration that is not there.
Co-authored-by: Hanzo Dev <[email protected]>
Consensus-correctness fix for the native DEX settlement VM. Root cause of the
launch-blocking forks: order size/price were IEEE-754 float64 on the ZAP wire, so
every validator re-derived the settlement integer via uint64(float)/int64(float*1e8),
whose out-of-range result is implementation-defined per the Go spec and DIFFERS BY
CPU ARCHITECTURE (arm64 saturates to 2^64-1; amd64 wraps to 2^63). Two honest
validators on different archs then derived different reserves/balances/fills from the
SAME ordered block.
F1/F3/F5 — exact-integer size/price end to end:
- pkg/zapwire: Place/Submit/Fill carry price as uint64 fixed-point (×1e8 = PriceInt)
and size as uint64 atomic base units. No float64 on the wire. Removed PutFloat64/
Float64. Decode is exact bytes->uint64 (deterministic on every arch).
- pkg/dchain/execute: decode the exact integers, set the authoritative PriceUnits/
SizeUnits lane; the float Price/Size become derived projections only (never a
source of a consensus value). Reject price==0/size==0/price>maxPriceUnits.
- pkg/dex/persist: the DEXOrder/DEXTrade row stores the EXACT PriceInt (in
DEXPrice.Integer) and EXACT base units (Quantity/Remaining); RowToOrder restores
the integer lane verbatim (identity — no float round-trip) and Filled=Qty-Remaining.
Removed floatToFixedQty/fixedQtyToFloat/floatToFixedPrice (the non-idempotent
round(size*1e8)/Q64.64 encoding that clamped at ~1.845e11 and forked on restart).
- pkg/dex matcher: cross and level-key on the exact integer PriceUnits.
- pkg/dchain/settle: orderLock/floorsToZeroLock take exact integers; removed
sizeToUnits/priceToInt float conversions and the dead restoreIntegerLane.
F2 — commit the custody ledger into the execution root:
- state.ComposeRoot/ExecutionRoot gain a ledgerRoot term (Merkle over balance:/
locked:/orderasset: rows). Validators can no longer finalize agreeing on the book
while holding different money.
F4 — release the ceil-lock/floor-charge residual on full fill:
- decrementMakerReserves now closes a fully-filled maker's reserve and returns the
ceil(size*price)-floor residual to the owner (a filled order can never be
cancelled), through the same unlock path a cancel uses. No more permanently
frozen quote.
PROVEN: the two-phase cross-arch harness (arm64 build / amd64 Rosetta verify) now
derives byte-identical execRoots AND byte-identical custody ledgers for the
identical block (uint64(1e20) still differs at the language level — 0xffff.. vs
0x8000.. — but the exact-integer wire freezes the value in the bytes so no
per-validator float->int conversion remains). Red guards flipped to assert the fix;
conservation/byzantine/batch-parity/KAT/e2e all green (go test ./... -count=1),
go vet clean.
OUT OF SCOPE (flagged, NOT fixed here): redteam_unbacked_deposit_test documents a
SEPARATE critical finding (F9) — dex_deposit credits an arbitrary self-signed amount
with no C-side backing proof, exposed on the public RPC. That needs an access-control/
custody-model decision, not a mechanical fix, and is left for a follow-up.
Co-authored-by: Hanzo Dev <[email protected]>
The matching engine (pkg/lx, 45.5K LOC incl. tests) and the deterministic
core layered on it (pkg/dex) are one matcher spread across two packages
(dex imported lx in 12 files; lx imported neither). Fold lx into dex and
drop the "lx" name entirely — pkg/dex is now the single matcher package.
Pure rename/fold, ZERO logic change. Behavior proven identical: the merged
pkg/dex runs 457 top-level tests (431 former-lx + 26 former-dex) and 489
test funcs (468 + 21), all green — matching, batch (BatchMatchEqualsSequential,
run-to-run determinism, tile boundary), GPU/CPU parity, truncation, and CL
swap math all pass unchanged.
What moved:
- git mv all 101 pkg/lx/*.go + 1 .c into pkg/dex/ (package lx -> package dex).
- lx's swap_test.go -> swap_math_test.go (only filename collision with dex's
existing swap_test.go).
- Rewrote imports repo-wide: 54 pkg/lx import lines removed; 42 external
importers (api, client, consensus, dchain, dpdk, lxgpu, marketdata, test/,
tests/) repointed to pkg/dex with lx.X -> dex.X; the 12 in-package dex files
had lx.X -> bare X. 717 selector lines rewritten. Stale pkg/lx comment refs
updated (pkg/lxgpu left intact).
Collisions resolved (2, both package-level vars):
- ErrNoLiquidity: lx's pool-level error (dexError, used only in swap.go, never
referenced via qualifier) renamed ErrPoolNoLiquidity to match the existing
ErrPool* convention; dex's router-level ErrNoLiquidity (public, has errors.Is
tests) kept.
- priceMultiplierBig: identical after fold (both big.NewInt(PriceMultiplier));
deduped by deleting dex's copy.
Not folded — pkg/orderbook: it is NOT the GPU seam (that is orderbook_gpu.go,
which lives in the engine and folded in here). pkg/orderbook is a zero-importer,
placeholder-cgo island whose 13 core domain identifiers (Order, OrderBook, Trade,
OrderType, OrderStatus, PriceLevel, Buy, Sell, Market, Limit, Filled,
PartiallyFilled, NewOrderBook) collide incompatibly with the engine's. Folding
it verbatim breaks the build; folding via rename reintroduces the exact parallel
duplication this fold removes. Left for owner decision (recommend deletion as
dead code) — see refactor notes.
Follow-on (separate repos, not touched): 19 files under luxfi/dex import
pkg/lx externally — lux/precompile/dex, lux/precompile-dex/dex,
lux/consensus/examples/01-simple-bridge, lux/_consensus-v12537/examples,
lux/cex/pkg/engine — repoint to pkg/dex after this lands.
Verified: CGO_ENABLED=0 GOWORK=off go build ./... clean; go vet ./... clean;
go test ./... -count=1 all green (22 ok / 0 fail).
Co-authored-by: Hanzo Dev <[email protected]>
Saves the GPU-venue build work: pkg/lx/orderbook_cuda*→orderbook_gpu*, the
clob→gpu cgo binding rename needed for the GB10 build, and cmd/dexfill (the
real-fill ZAP client). Checkpoint — build-verify pending.
Decentralized exchange; the engine acts like an order book but is decentralized,
so 'Central Limit Order Book' is a misnomer. Branded/service surface → DEX
(RegisterDEX, DEXMethod*, dex_* wire). Matching engine + GPU → OrderBook
(OrderBookMatch/Source/Arena, source_orderbook.go), preserving the AMM-vs-orderbook
distinction. C-ABI → dex_orderbook_* (matches luxcpp). SDKs/clients/docs aligned.
CGO=0 + cgo builds clean; full suite 23 pkgs green (dchain/dex/lx/fix/gateway/zapwire).
Legit financial-term 'Central Limit Order Book' retained in hummingbot taxonomy.
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.
The package lived at dex/pkg/dexcore (package dexcore) with 'dexcore:' error
prefixes, while every sibling is short (api, client, dchain, consensus) and every
other dex error says 'dex:'. The 'core' qualifier is redundant inside the dex
module. Rename pkg/dexcore → pkg/dex (package dex), unify error prefixes
dexcore: → dex:, update the 5 importers (dexseed, dexfillproof). Tests green
(pkg/dex, 1.398s); dvenue + dexvm plugin rebuild; e2e swap re-proven (seed 12
orders + cross → fill @ 12.5125).
dexfillproof compared after.Markets[].PoolID against ids.ID.String() (cb58),
but pkg/dchain/read.go encodes poolId as lowercase hex of the 32-byte id, so
the read-back market-presence assertion never matched and the tool exited 1
even after a real FILL landed. Compare on hex (the canonical wire form). Also
rename the residual clob_ doc/error strings to dex_ — the public surface is
dex_* (the engine's internal clobMethods identifiers stay as-is).
The D-Chain DEX wire methods now use the dex_ namespace (dex_place,
dex_submit, dex_cancel, dex_ensure_market, dex_open_market, dex_deposit,
dex_withdraw, dex_get_markets/book/orders/trades) per the luxd
service-prefix convention (eth_/avm./platform.). On the D-Chain the DEX
IS the CLOB (the AMM lives in C-Chain contracts, not this VM), so the
public wire exposes the chain's purpose, not the internal engine name.
The internal Go identifiers (clobMethods, MethodPlace, the CLOB engine
name) are unchanged — only the on-wire string VALUES move. Covers BOTH
transports via the one clobMethods table (ZAP RegisterCLOB + HTTP
ingest.go httpHandlers) plus read.go GET endpoints. Forwards-only (DEX
not live yet). dexseed + dexfillproof clients aligned.
zapwire + dchain tests green; four-path + venue E2E pass.
The mess was three layers defending a self-inflicted footgun:
(a) cmd/clobverify + cmd/fourpath-live SEED ascii-of-symbol synthetic
markets (poolID = ASCII of "LUX/LUSD"), orders with no orderuser row;
(b) internal/localguard refused those tools off-loopback;
(c) VM.assertOrderUserCoverage BRICKED the chain at boot if a custody
market had a resting order with no orderuser row.
Decomplect-perfect (NOT another guard):
1. DELETE cmd/clobverify + cmd/fourpath-live. Real markets come from
permissionless OpenMarket + real ERC-20s + the maker; the real-market
e2e supersedes them. cmd/dexseed (canonical, structurally synthetic-safe)
stays.
2. DELETE internal/localguard (+ test) — it existed ONLY to fence the
deleted seeders.
3. The settlement-identity invariant already holds BY CONSTRUCTION at order
creation: lockOrderSpend writes orderuser: and putOrderRow writes order:
to the SAME versiondb overlay, committed in ONE atomic Block.Accept
overlay.Commit(); an unfunded place never rests. So a resting custody
order without its orderuser row is UNREPRESENTABLE in committed state ->
the boot scan was provably dead -> DELETE VM.assertOrderUserCoverage.
The single remaining settlement-identity guard is the consensus-time
fail-closed in settleOrderEffects (ErrMissingSettlementUser), which can
never fire on honest committed state.
TDD:
- TestCommittedStateNeverHasOrderWithoutIdentity: drives the real commit
path (rest + reject + fill); committed order: set == orderuser: set.
- TestRestartDoesNotBrickBoot: hand-corrupts state the commit path cannot
produce; boot now SUCCEEDS (no brick); consensus-time guard still fails
closed when a fill touches the corrupt order.
- All prior settlement-identity regression tests still pass.
Result: zero guards, illegal state unrepresentable. Removes the footgun
instead of guarding the footgun we created.
cmd/dexseed seeds REAL markets onto the live in-luxd native D-Chain CLOB
ingestion seam (POST /ext/bc/D/dex/clob_* with FROZEN zapwire frames),
using consensus-native asset identities for every market side.
Identity rule (the property that makes this seeder SAFE against a shared
cluster, unlike the loopback-gated cmd/clobverify): every asset handle is
the dexcore canonical AssetID = DeriveAssetID(networkID, C-chainID, kind,
canonicalRef) — the SAME id the chain's resolver derives — NEVER an
ascii-of-symbol id (the synthetic form, e.g. 0x4c5558 'LUX', that bricked
the chain). A synthetic/malformed ref is refused at frame-build time,
before a single frame is sent; the chain's permissionless resolver then
proves each side is backed by live on-chain code (EXTCODESIZE) before
binding the market.
End-to-end smoke path for one real LUX/<ERC20> market: clob_open_market
(bind real base+quote ids) -> clob_deposit (fund maker base) -> clob_place
(rest maker ask) -> clob_get_markets + clob_get_book (read back & confirm).
Pure, testable core (seed.go): assetSpec.resolveID, poolIDFor (canonical
domain-separated SHA-256 over the derived ids, NON-ascii), depositRefFor,
realMarketFrames. Tests (main_test.go): golden AssetID vectors, OpenMarket
96B + slot equality, synthetic/malformed-ref rejection, deterministic
non-ascii poolID, frozen-frame field round-trips. CGO_ENABLED=0, pure-Go,
luxfi packages only.
clobverify and fourpath-live seed markets with ASCII-of-symbol SYNTHETIC
identities (poolID = symbol bytes; asset ids = ascii of LUX/LUSD). When those
frames reached the live devnet validator set they persisted into the
replication snapshot and BRICKED the native D-Chain VM: dchain.assertOrderUserCoverage
sees a resting order on market 4c55582f4c555344 ("LUX/LUSD") with no orderuser
row at boot and refuses the degraded settlement path.
Add internal/localguard.AssertLocalOnly: each seeding tool now refuses to write
unless (1) the explicit --i-understand-this-seeds-synthetic-test-data flag is
set AND (2) every target (validator base URLs / venue / cchain-rpc) resolves
entirely to loopback. The check runs BEFORE any frame is sent, so the
poison-a-shared-chain path is impossible while local single-process testing is
unchanged. clobverify -read-only is exempt (it never seeds).
IsLoopbackTarget is a pure, fail-closed classifier (URL or host:port; rejects
on parse error, unresolvable name, or any non-loopback resolved IP). Doc
comments at both tool tops mark them LOCAL-ONLY with the brick reason.
Tests: internal/localguard covers (a) non-loopback rejected, (b) loopback
without opt-in rejected, (c) loopback + opt-in allowed, plus k8s-dns /
lux-dev.network / public-ip / 0.0.0.0 classification.
Every d-chain write is deduped by the hash of its frame (seen: key); re-sending
identical deposit/place/submit frames across runs returns the FIRST run's cached
outcome (stale orderId, empty fills) and proves nothing. Thread a wall-clock
nonce into the order SIZE (ask+buy same size so they fully cross) and the deposit
AMOUNT so each invocation is a genuinely new, funded cross. Proven on lux-devnet:
fresh fills submitted to luxd-0/2/4 each committed identically on all 5 validators.
A pure-Go (CGO=0) tool that audits the native D-Chain across a validator set:
submit a crossing order to ONE validator over the in-luxd HTTP ingestion seam
(POST /ext/bc/D/dex/clob_* with frozen zapwire frames), then read the committed
trade log (GET /ext/bc/D/dex/clob_get_trades) from EVERY validator and compare
the trade rows + accepted head root.
Verdict:
- all nodes show the identical fill + identical root -> PROVEN replicated under
the chain's quorum (not just the proposer).
- only the submit-target shows it -> SINGLE-PROPOSER (replication did not reach
followers).
- partial / roots diverge -> incomplete.
Links only pkg/zapwire (the one source of frame truth) + stdlib; decodes the
read responses as open JSON (exercises the v1.5.15 read schema exactly as an
external consumer would). Builds static for in-cluster runs.
The D-Chain DEX VM (pkg/dchain) is the native consensus matcher: it runs
the lx.OrderBook matcher inside luxd consensus (Block.Verify over a
versiondb overlay) and is the chain VM luxd loads from PluginDir at the
dexvm vmID slot — exactly like the EVM plugin. There is no standalone
venue and no ZAP relay in the trading path.
cmd/dchain + runner carried a //go:build dchain && cgo gate from the old
'venue accelerator' framing, but they import ONLY pkg/dchain (pure-Go),
log, and vm/rpc — zero native deps. The gate excluded the plugin from
every CGO=0 build, which is exactly how the node Dockerfile builds chain
VM plugins. Drop the tag so cmd/dchain is the canonical CGO=0 native
plugin (same harness as chains/*/cmd/plugin). The optional GPU AMM
accelerator in pkg/lx stays gated by its own cuda/metal tags and is not
linked by this consensus plugin.
Proof: CGO_ENABLED=0 go build/vet ./cmd/dchain clean; native VM suite
(TestLocalnetVenueE2E, TestFourPath, conservation, ownership) passes.
The D-Chain (dexvm) is its own chain: trading is its consensus-driven state
transition (matcher-at-Verify in pkg/dchain). There is no off-chain keeper
driving 0x9999 — 0x9999 is only the C<->D settlement adapter. Remove the
keeper and its footprint:
- cmd/dexkeeper/ (the off-chain relayer)
- Dockerfile.dexkeeper
- .github/workflows/docker-dexkeeper.yml + docker.yml reference
- k8s/devnet-keeper/ (never deployed)
- go.mod: drop luxfi/chains dep (was pinned at v1.3.16 ONLY for the keeper;
nothing else in dex imports chains — go mod tidy removes it)
Native dchain VM + matcher build pure-Go (CGO_ENABLED=0) and the full
pkg/dchain suite passes (consensus match->Verify->Accept, conservation,
LUX/LUSD + LUX/LETH venue e2e, ownership red-team).
Two corrections to the Phase-B path:
1. CollateralRef bug: the settling RelayOrderTx now binds CollateralRef = ev.IntentID
(the C->D object's UTXOID), NOT importTx.ID(). executeImport keys the escrow by
tx.ImportedInputs[0].UTXOID (== intentID) and settleFromFills looks it up by
RelayOrderTx.CollateralRef; with importTx.ID() the escrow lookup missed (proceeds-only,
no refund, degraded authority bind). Now they agree.
2. getExportedOutput is no longer a stub: it polls dex.getSettlement(collateralRef) (the
new dexvm RPC) until the proposer relays + the settling block accepts and records the
proceeds object's (outputID, amount), then returns them for the Phase-B DS01 settlement.
~60s poll budget; on a zero-fill (no proceeds) it reports the reclaim path.
Builds linux/amd64 static (CGO off); keeper tests pass.
cmd/dexkeeper watches the C-Chain for IntentSubmitted (Phase-A intent: a maker
swap to 0x9999 with DI01 hookData), submits the ImportTx (consume the staged
C->D atomic object, fund the D order) + the settling RelayOrderTx (clob_submit,
collateral-bound) to the dexvm dex.submitTx RPC, then drives the Phase-B
settlement swap (DS01 hookData) that consumes the D->C proceeds object and emits
DEXFill. The keeper holds NO custody authority: value is locked in shared memory
by C before it acts, and the D->C object pays its recorded owner regardless.
- maker.go: BIP44 m/44'/9000'/0'/0/i maker-key derivation, pinned byte-for-byte
against genesis.LoadBIP44WalletKeysFromMnemonic. Uses the STANDARD EVM address
(keccak256(X||Y)[12:]) everywhere — NOT secp256k1.PrivateKey.EVMAddress(),
which hashes the compressed pubkey and yields a non-standard address.
- calldata.go: Phase-A/B 0x9999 calldata via zap/dexsession, intent-id
derivation mirroring precompile/dex DeriveIntentID, IntentSubmitted + DEXFill
decoders (fail-closed at the boundary).
- venue.go: ZAP maker-liquidity seeding, modeled on cmd/fourpath-live.
- keeper.go: watch loop + import/relay orchestration. Settling relay is sent
UNSIGNED with From = escrow owner (the architecturally-correct build).
- Modes: seed | phase-a | watch. Maker key from LUX_MNEMONIC.
Unit tests (10, all passing) cover the pure logic: maker derivation vs the
genesis authority + a known vector, Phase-A/B calldata byte-identity + DI01/DS01
shape, intent-id determinism/injectivity, and synthetic event decode (+ negative
cases). Builds CGO_ENABLED=0 for linux/amd64 (static ELF).
Known gap (documented stub): getExportedOutput — the D->C export discovery needs
a dexvm settlement-coordinate query (dex.getSettlement) or a C-Chain importable-
UTXO view; neither is exposed today. Phase-A path is fully wired.
SubmitMarketable and MatchOrders derived their returned fills by re-slicing
the shared trade log (ob.Trades[startOffset:]) with an offset captured BEFORE
matching. ob.Trades self-truncates at 100k entries (keeps the last 50k), so a
match that crosses that boundary mid-match trims the log and leaves the
pre-match offset pointing past the end -> panic 'slice bounds out of range'.
The log feeds settlement, so fills had to stay exact.
tryMatchImmediateLocked / MatchOrders now capture each fill as it is produced,
independent of ob.Trades, and return that slice. Decomplects 'this match's
fills' (a value the caller needs) from 'the trade log' (a bounded-retention
buffer): correct across the truncation boundary by construction, deterministic
across validators.
Adds orderbook_truncation_test.go: three regression cases that cross the
boundary through the production match path (single submits, one sweeping
submit, legacy MatchOrders) with exact-fill and value-conservation asserts.
They panic on the old code, pass on the fixed code. No sleeps, no stubs.
Also: 0x9999 D-matches/C-settles docs (LLM.md, README, ARCHITECTURE),
venue-canonical naming, fourpath-live proof tool, drop dead snowman-api key
from run-lux-testnet.sh.
Adds the missing SERVER side of the FIX trading path and a unified e2e
proving REAL fills over all four DEX access paths against ONE book.
The decomplection: the D-Chain CLOB (pkg/dchain) is the single matcher,
reached over the single frozen clob_* ZAP surface (pkg/zapwire). Every
access path is a thin wire dialect onto that surface — none owns a
matcher. This commit adds the two missing dialects (FIX, a focused WS
venue handler) and the shared seam they translate through.
New:
- pkg/venue: ONE Venue interface (ensure/place/cancel/submit = the
clob_* ops) + ONE production ZAP-backed impl. The seam FIX, WS, and
the 0x9010 precompile all route through. Dials the SAME rpc.Listen
socket dvenue serves.
- pkg/fix: dependency-free FIX 4.4 codec (BodyLength tag 9 + CheckSum
tag 10 computed AND validated; spec-correct emit/parse) + a TCP
acceptor (Logon/Heartbeat/TestRequest/Logout, monotonic seq,
NewOrderSingle->clob_submit->ExecutionReport, OrderCancelRequest->
clob_cancel). Pure protocol front-end onto Venue; no matcher.
- pkg/api/venue_ws.go: minimal gorilla/websocket /ws handler that
translates a JSON order to Venue.Submit and streams fills back —
the WS dialect onto the SAME venue (kept orthogonal to the existing
TradingEngine product WS server).
- cmd/fix-gateway: runs the acceptor in front of a live venue
(-venue clob_* endpoint), the production form of the FIX path.
- pkg/dchain/fourpath_e2e_test.go: stands up ONE venue + ONE LUX/LUSD
book and drives a real limit-order -> match -> FILL over EACH path
(V4 precompile engine_zap frame | ZAP native | FIX | WS) against the
SAME resting asks, asserting real consensus fills, per-path custody
changes, and GLOBAL conservation (available+locked invariant).
V4 leg: exercised at the engine_zap WIRE boundary — the exact 66-byte
clob_submit frame engine_zap.Swap emits (asserted byte-identical to
zapwire.EncodeSubmit) + the V4 BalanceDelta sign convention derived from
the consensus fills. A live luxd EVM (StateDB replay-idempotency, gas,
PoolManager) is what the full on-chain precompile call additionally
needs; the matcher boundary is proven in-process here.
No deps added; no existing files modified (go.mod/go.sum untouched).
Builds CGO_ENABLED=0; race-clean.
Drop 31 demo/benchmark/experiment one-off cmds whose coverage lives in
package tests (bench-all, dag-network, dclient, dexe2e, e2e-fix-zmq,
fix-benchmark, hybrid-auto, latency-benchmark, memory-*, mlx-memory-analysis,
multi-node, multi-node-test, nats-*, perf-test, stress-test, test-client,
zmq-*, dvenue-cancel, dvenue-custody, venueprobe, empty api-server/server/trader).
Keep production binaries: dvenue, dchain, dex-server, dex-api-server, gateway,
price-feed, monitor, persistent-server, demo (each referenced by a
Dockerfile/Makefile/script/k8s manifest or a documented entrypoint).
Consolidate cmd/dvenue + cmd/dvenue-cpu into one tag-free cmd/dvenue: the two
mains were byte-identical except a build tag and a log label; the GPU-vs-CPU
matcher is already selected inside pkg/lx by its own cgo-tagged files, so the
main needs no tag. CGO_ENABLED alone picks the matcher; one binary builds in
both modes. engine_{cgo,nocgo}.go carry only the engine display label. dvenue
now builds under the default CGO_ENABLED=0 gate (was tag-excluded before).
Dockerfile.dvenue: drop the dvenue-custody build+copy (cmd removed).
Makefile build-tools: point at real tools (price-feed, monitor) instead of the
removed cmd/trader and never-existent cmd/lx-metrics.
pkg/price: fix the three CChainSource tests. They were gated on requireLuxd
(a luxd TCP dial) but CChainSource never dials luxd — fetch() is pure simulate().
The gate skipped only when luxd was down, masking the real defect on hosts where
luxd runs. Rewrite the tests to drive the source's true contract deterministically
via src.poll() (no ticker race) and assert against the symbols the source actually
polls (cchainTokens), plus not-found for untracked symbols. Remove the dead
requireLuxd helper and its net import.
orderuser->full 16-byte user is the only settlement identity; 8-byte UserID is a matcher/STP handle only, never a money key. Missing orderuser fails closed (ErrMissingSettlementUser), no matcher-handle fallback. Ownership invariant harness alongside conservation (catches value-conserving theft). txHash-ref custody idempotency closes vault-drain/strand. Reject zero-lock orders; persist orderuser before book insert; restart asserts coverage.
Adds runIDOR — the live devnet counterpart to the precompile unit test
TestZAPCancelAuthRejectsOthersAcrossMultiExec. A maker rests an ask; a DIFFERENT
account attempts to cancel it with the same tick band + salt; asserts the attacker's
cancel REVERTS, the maker's lock survives, and the maker can still cancel its own
order. Proves the durable cancel-authorization binding (precompile RED H4) closes the
EVM-path cancel revert WITHOUT opening an IDOR.
NEW cmd/dvenue-cpu: //go:build dchain (no cgo) — the FREE pure-Go CPU D-Chain venue daemon
(drives dchain.VM's CPU matcher; builds CGO=0; published as ghcr.io/luxfi/dchain:cpu). The
cgo cmd/dvenue stays the LICENSED GPU venue (//go:build dchain && cgo). Both now open the DB
via github.com/luxfi/database/zapdb (NOT badgerdb directly — per the zapdb-only rule).
CRITICAL: the prior taker-only settlement debited the maker's resting order but NEVER credited
the maker the taker's payment — a value hole on every two-sided fill (old test only audited the
taker leg). FIX: per-account ledger in pkg/dchain (balance:/locked:/asset:/orderasset:; checked-int
lockFromAvailable/unlockToAvailable/spendLocked) — a fill is a pure intra-D-Chain balance move,
wei-exact on BOTH legs. settle.go (orderLock/settleFills/quoteUnitsCeil: lock>=spend), block.go
custody gate (lock-before-match, unfunded→reject) + settleOrderEffects. rebuildBookFromDB now
restoreIntegerLane() — RowToOrder didn't round-trip SizeUnits/RemainingUnits → makers from disk
had a broken integer lane → nil-units fills settle refused. zapwire ADDITIVE custody frames
(clob_deposit/withdraw/open_market); 4 frozen CLOB frames byte-untouched. Conservation tests
(real VM) + live 1337 e2e (deposit→cross→maker-credited→withdraw 1100==1100→restart) green.
pkg/dchain: store original outcome under seen:<txID> and return it verbatim on replay so a
deduped clob_place is distinguishable from a genuine reject (was returning StatusRejected →
reverted the EVM tx that re-executes ~5x). BookDepth read accessor. cmd/dvenue = the dial-able
venue D-Chain ZAP listener (badgerdb + vm.Initialize + rpc.Listen + RegisterCLOB + production
sealer) the 0x9010 precompile + dexvm proxy point at; cmd/dclient = native-ZAP e2e driver.
zapdb v1.10.0→v1.10.1 (cloud-free, cgo blocker resolved on the v1 lineage). Tests green.
Document dex-api-server's actual architecture: it is the single-process,
in-memory-orderbook daemon. There is no consensus binding — the orderbook
finalises orders synchronously inside OrderBook.AddOrder, so a single-
validator self-loop on top would only add latency. Multi-node consensus
lives in cmd/dag-network (build tag zmqtest), not here.
Add cmd/dex-api-server/standalone_test.go behind the integration build
tag. It builds the daemon, places a limit order via /api/order, and
confirms the orderbook is queryable end-to-end.
Run: go test -tags=integration ./cmd/dex-api-server/... -count=1 -timeout 60s
Output: PASS — TestStandalonePlaceOrderRoundTrip (1.89s)
Architectural note: the audit asked for a Quasar self-loop here. Honest
assessment in the README — that loop would be redundant in a single
process. The real refactor (one binary, --peers flag, both modes) is a
separate piece of work that requires clean interfaces between the two
orderbook implementations.
Gateway now requires --rpc/RPC_URL and --chain-id/CHAIN_ID.
No hardcoded C-Chain or Zoo endpoints. Precompile addresses
fixed to LX-series (0x9010-0x9060). White-label ready.
Apply consistent naming conventions throughout:
- Standardize product name to "LX" in documentation
- Update README files and API documentation
- Apply linter fixes to all source files
- Update Hummingbot connector documentation
Q-Chain quantum finality verification is now integrated directly into
the Oracle instead of being a CLI flag toggle. This provides:
- oracle.SetVerifier(v) to attach Q-Chain verifier
- oracle.VerifiedPrice(sym) returns price with finality status
- Verifier is auto-started and auto-stopped with Oracle
- No flag needed - verification is automatic when verifier attached
- Use -no-verify flag to explicitly disable (opt-out vs opt-in)
The VerifiedData type wraps price Data with finality proof, making
quantum finality a first-class concern of the price aggregation system.
Clean architecture following Go principles:
- factory.go: Pure Go implementation (default, no CGO)
- factory_cgo.go: C++ optimized version when CGO=1
- orderbook.go: Standalone Go orderbook implementation
- cpp_orderbook.go: C++ bindings for high performance
Also includes:
- Vault execution integration with orderbook
- dex-api-server command for REST/WebSocket API
- E2E test scripts for API validation
- Documentation site scaffolding
- Add missing OrderBook interface implementations
- Fix go vet warnings (unreachable code, redundant newline)
- Add build tags for long-running tests
- Create proper Go and C++ orderbook implementations
- Fix clearinghouse test skipping properly
- Format all Go code with gofmt
- Ensure all packages build successfully
- All tests pass with -short flag