371 Commits
Author SHA1 Message Date
zeekay 670aec5261 deps: pin luxfi/[email protected] (re-tagged v0.8.10 had inconsistent content across hosts) v1.9.0 v1.7.3 2026-06-26 12:56:10 -07:00
zeekay c9daf10e22 fix(lx): delete fake benchmarks + false perf claims; use real measured numbers
Delete 3 AI-slop benchmarks from pkg/lx/critical_path_bench_test.go that measured no real work and reported fabricated throughput:
- BenchmarkMLXAcceleration: b.Skip + no-op slice index, then fabricated orders/sec
- BenchmarkQuantumSignatures (+verifyQuantumSignature): verify == len(sig)==512 && len(pub)==256
- BenchmarkConsensusFinalization (+hashBlock/verifyBlock): byte copy + len(hash)==32
Real benchmarks (CriticalOrderMatching, TradeExecution, RiskCheck, EndToEndLatency, ...) untouched; no imports left unused.

Replace fake '434,782,609 orders/sec (GPU/MLX) - 2ns/order - zero-alloc' claims in README.md and LLM.md (traced to archive/_lp108-2026-05-04/mlx_engine.go, a 'pure Go simulation' hardcoding OrdersPerSecond: 1000000.0) with numbers measured first-hand on Apple M1 Max:
- C++: 11.88M orders/s (10 threads), 169 ns avg match (p50 125 / p99 292), 5.91M single-thread, 49 ns cancel
- Go pkg/lx: 2.2M orders/s, 381 ns/order, 1-2 allocs/op (NOT zero-alloc)
- DEX matching stays on CPU; the real GPU win is FHE Metal NTT (23x speedup, N=4096 batch=128).
2026-06-26 12:55:14 -07:00
zeekay 8f9e335393 deps: re-record re-tagged luxfi/[email protected] go.sum (clean dexd build) 2026-06-26 12:54:14 -07:00
zeekay 21301212f9 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.
2026-06-26 12:24:34 -07:00
zeekay afcc765854 Merge main-pub into main v1.7.2 2026-06-26 00:53:15 -07:00
zeekay 265873e025 deps: bump luxfi/zap v0.8.10→v0.8.11 (clean tag; v0.8.10 was re-tagged → go.sum mismatch on fresh builders) 2026-06-25 23:22:58 -07:00
zeekay ad3129ec7a dex: drop redundant 'core' — pkg/dexcore → pkg/dex (package + dex_ error prefixes)
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).
2026-06-25 23:15:44 -07:00
zeekay b0bea31b18 build(dex): add CPU venue Dockerfile (CGO_ENABLED=0 pure-Go matcher)
Dockerfile.dvenue-cpu builds cmd/dvenue for linux/amd64 with no CUDA/GPU,
selecting pkg/lx's pure-Go CPU matcher via the !cgo build tag; serves the
frozen clob_* surface over ZAP with the consensus sealer loop. Complements
the GPU Dockerfile.dvenue (CUDA, arm64/spark-only) with a venue image that
runs on any amd64 node — the deployed devnet dchain-venue binary.

Verified: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./cmd/dvenue links
the pure-Go path (amm_nogpu / orderbook_cuda_stub) clean on this base.
v1.7.1 mirror/main-pub-3392393
2026-06-26 04:39:40 +00:00
zeekay f4bcdbca39 feat(rfq): non-custodial cross-chain swap coordinator (pkg/rfq)
The venue V of the HTLC atomic-swap design: a pure relay that serves a
maker-signed quote book and sequences a swap session between taker U (the
secret-holder) and a CEX-backed maker M, validating each typed signed
transition — role bindings (HTLC_A recipient=U/refund=M, HTLC_B
recipient=M/refund=U), the cross-leg hashlock, and the timeout ordering
T_B > T_A + Δ (maker locks first/shorter; spec Δ table: Lux<->BTC 2h,
EVM<->EVM 5m).

Non-custody is STRUCTURAL: the Coordinator holds no private key of any
asset, is the recipient/refund of no HTLC, and has no fund-moving path —
TestNonCustody reflects over it and fails if any field is a key type. After
the hashlock commit V is irrelevant to safety; its crashing or lying can
only deny service or relay a stale quote.

Composes pkg/htlc for the shared hashlock/preimage and authenticates every
message through crypto.Ecrecover directly (recoverAddr) with its own venue
domain tags — a leaf, so the relay never pulls the GPU CLOB matcher. Builds
and passes go test -race off a non-GPU box.
v1.7.0
2026-06-26 04:26:54 +00:00
zeekay 9c9106017e feat(htlc): cross-chain HTLC leg builders (pkg/htlc)
The single place a Lux cross-chain atomic swap is constructed across all
three legs, composed from one shared primitive:

- secret.go  — Secret [32]byte, Hashlock() = sha256(secret), constant-time
  VerifyPreimage. THE source of the SHA-256 hashlock every leg shares (one
  32-byte preimage unlocks BTC, the LP-90A0 swap precompile, and the EVM
  LXSwapHTLC.sol); keccak is explicitly excluded.
- bitcoin.go — the fixed P2WSH HTLC script (OP_IF SHA256/EQUALVERIFY/CHECKSIG
  // OP_ELSE CLTV/DROP/CHECKSIG), P2WSHAddress, and claim/refund witnesses
  signed with real BIP143 segwit-v0 sighash via btcd txscript (no hand-rolled
  sighash; low-S RFC6979 sigs).
- evm.go     — lock/claim/refund calldata; selectors DERIVED from the canonical
  ABI signatures and asserted equal to the precompile's 0x4da2c728 /
  0x84cc9dfb / 0x7249fbb6.

Tests (-race) include real txscript.Engine execution proofs: claim spends
with the correct preimage and fails with a wrong one; refund spends at/after
locktime and fails before / with CLTV disabled. btcd pinned v0.24.2 (v0.26.0
split chaincfg/wire/txscript into /v2 modules and breaks the build).
2026-06-26 04:26:54 +00:00
zeekay 035b30818a release: v1.6.1 — matcher self-trade prevention (conservation) v1.6.1 2026-06-26 01:18:15 +00:00
zeekay dd228b211c lx: self-trade prevention in SubmitMarketable via canonical owner fold (fixes conservation seed 4242)
A rested maker carries an 8-byte-truncated User (from persist.go userToUint64);
a fresh taker carries the full 16-byte identity. The marketable matcher's raw
User== self-trade check missed the same-account self-cross, desyncing the int/
float quantity lanes -> maker reserve drained + orderuser row deleted -> later
cross stranded settlement -> conservation broke. Add selfTradeKey() (one fold,
used by both place + marketable paths) and skip-and-cancel the self-maker leg.
settle.go arithmetic untouched. seed 4242 re-included + conserves.
2026-06-26 01:18:15 +00:00
zeekay 14d6d21321 release: v1.6.0 — signed-order authorization boundary (non-custodial settlement hardening) v1.6.0 2026-06-26 00:52:52 +00:00
zeekay b30f276a90 dchain: enforce signed-order authorization on every fund-moving tx (closes audit C1/H2/H3)
Every state-mutating D-Chain tx now carries a signature + per-account nonce,
verified in consensus before any ledger mutation: recovered signer must equal
the asserted 16-byte user, and the digest binds tx type + nonce + full body
(so the withdraw ref and cancel orderId are signed). secp256k1 + ML-DSA-65,
fail-closed. Previously any caller of the venue RPC could move anyone's funds.
Conservation, identity-width, and ownership invariants preserved; the
signed-order primitive that existed without a caller is now the auth boundary.
Tests across ingest/read/zapingest/e2e adapted to sign; no checks weakened.
2026-06-26 00:52:52 +00:00
zeekay 38485adee3 Merge branch 'test/dexvm-purity-lock' into main-pub 2026-06-25 16:37:02 -07:00
zeekay 4d5038a402 deps: bump crypto v1.19.26 + consensus v1.25.29 (go-live cascade)
Aligns the dex leaf onto the canonical crypto/consensus go-live tags. dexcore
(permissionless AssetResolver) + dchain (dex_get_* read RPC, pure block.ChainVM)
tests green (GOWORK=off). No source change — dep alignment.
v1.5.20
2026-06-25 16:08:34 -07:00
zeekay 6c088ef199 test(dchain): fix CGO-on test build + sync canonical zap go.sum
poolIDForSymbol was defined only in localnet_e2e_test.go (//go:build !cgo) but
read_test.go (no build tag) used it, so the package test binary failed to
compile under CGO_ENABLED=1 (undefined: poolIDForSymbol). Move the helper to an
untagged poolid_helper_test.go — it is pure-Go (a byte copy) and belongs in both
test binaries. Both CGO_ENABLED=1 (read/purity) and CGO_ENABLED=0 (venue/4-path/
ingest/conservation) suites now compile and pass.

go.sum: luxfi/zap v0.8.10 content hash updated to the canonical registry hash
(h1:QKNTAsenkke+...) after an upstream re-tag; 'go mod verify' = all modules
verified. Regenerated, not bypassed.
2026-06-25 15:10:07 -07:00
zeekay f7c9c91055 test(dchain): broaden purity denylist — catch generic gossip/broadcast/p2p verbs 2026-06-25 14:51:42 -07:00
zeekay 5daa702f25 test(dchain): purity lock — dexvm must stay a pure block.ChainVM
Structural regression lock for the consensus write-path fix (luxfi/consensus
fix/fetch-on-unknown-vote). The wedge was a SHARED-engine bug, not a dexvm bug:
the dexvm has — and must keep — ZERO networking/gossip primitives. It parses,
verifies, accepts, and matches blocks under consensus but NEVER propagates them;
propagation, vote distribution, and ancestor fetch live in luxfi/consensus.

TestDChainVM_IsPureBlockChainVM walks the package's own non-test .go files and
fails (with file:line) if any references AppGossip, SendAppGossip, PushQuery,
PullQuery, SendChits, common.Sender, or RequestAncestors. Verified to FAIL on an
injected sentinel and PASS on the real tree (12 source files, zero hits).

CGO_ENABLED=0 go test ./pkg/dchain/... green.
2026-06-25 14:07:29 -07:00
zeekay 6c28480234 fix(dexfillproof): match poolId as hex (read.go encoding), purge stale clob_ strings
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).
2026-06-25 11:47:43 -07:00
zeekay 8c46d75c5e test(dchain): use neutral seed in conservation property test v1.5.18 2026-06-24 08:59:22 -07:00
zeekay 84e8968b40 dex: rename wire methods clob_* -> dex_* (public DEX namespace)
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.
v1.5.19
2026-06-23 17:46:09 -07:00
zeekay 812ea50c0a refactor(dchain): drop dead ctx param from startZAPIngest
The serve goroutine deliberately uses context.Background() (its lifetime is
the VM's, cancelled by Shutdown via zapIngestCancel, not the short-lived
Initialize ctx), so the ctx parameter was never used. Remove it — no slop.
Functionally identical to v1.5.17; the param carried no behavior.
2026-06-23 15:54:50 -07:00
zeekay 53e0c44402 feat(dchain): ZAP socket is the canonical in-luxd CLOB ingestion (transport-decision)
The in-luxd native D-Chain plugin served HTTP only (the dev left the ZAP
socket to standalone cmd/dvenue: handler.go said 'the in-luxd plugin does
NOT use this path'). Owner transport decision: the ZAP socket is the
CANONICAL native ingestion in-luxd (the HFT path — binary wire, streaming,
co-located on the pod), HTTP stays as the compat surface (web/exchange-api/
debug). Both fold over the ONE clobMethods core; transport is orthogonal to
consensus, so it is safe to serve both — an order from either transport hits
the identical submitTx -> mempool.Add -> Verify-match path, and validators
replay from accepted block BYTES, never the wire.

zapingest.go: the VM owns its transports. Initialize parses init.Config for
an optional zapIngestAddr and, when set, opens rpc.Listen co-located in the
plugin process bound to clobMethods (reusing RegisterCLOB, the existing
socket seam) — served concurrently, closed on Shutdown. Enabled by DATA (the
chain config the operator/genesis sets), NOT a build tag or forked entry
point: one binary, one core, the transport set is a per-environment knob.
Empty addr => HTTP-compat-only (in-process tests + cmd/dvenue, which runs its
own socket, are unaffected). Malformed config fails fast at Initialize.

TDD (all !cgo):
 - TestZAPIngestCrossingOrdersMatchInBlock: an order over the VM's OWN
   config-started socket (no manual RegisterCLOB) -> consensus -> committed
   fill.
 - TestZAPIngestSharesOneCoreWithHTTP: maker rests over ZAP, taker crosses
   over HTTP against the SAME book -> proves one core, two transports.
 - TestParseConfig / TestNoZAPIngestWhenUnset / TestZAPIngestStopsOnShutdown:
   config contract (forwards-only), no-op path, clean idempotent teardown.

Additive: RegisterCLOB unchanged; CreateHandlers/ingest.go unchanged; the
runner (cmd/dchain) unchanged — the socket is a VM concern driven by config.
v1.5.17
2026-06-23 15:24:27 -07:00
zeekay 02e5254ca8 refactor(dex): decomplect the synthetic-seed/guard footgun (3 layers -> 0)
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.
2026-06-23 15:18:32 -07:00
zeekay 93fa24e705 dex(cmd/dexseed): canonical REAL-market seeder over the native D-Chain seam
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.
2026-06-23 15:09:01 -07:00
zeekay bcf4d0a4f5 dex: refuse synthetic-seed tools against non-loopback targets
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.
2026-06-23 14:42:13 -07:00
zeekay 8a985c4752 dexcore(dex): commit the canonical AssetID identity primitive (value-path compiles)
HIGH-1/HIGH-2 (integrity): assetid.go was UNTRACKED — it defines AssetKind, the
{EVMNative,ERC20,UTXO} consts, EVMNativeMarker, DeriveAssetID, and CanonicalRefFor:
the foundational real-asset identity primitive the whole value path keys on. A fresh
worktree at the prior dexcore commit failed to build (undefined: AssetKind ×5 in
place.go/resolver.go). Commit it (+ assetid_test.go) so pkg/dexcore COMPILES and the
value-path branch is reproducible.

assetid.go derives the consensus-native 32-byte AssetID from where an asset really
lives — (networkID, sourceChainID, kind, canonicalRef) — via a length-prefixed,
domain-separated ("lux:dex:asset:v1") SHA-256 fold, byte-identical to
chains/dexvm/registry.DeriveAssetID, so a swap-derived id (this home) and a registered
id (the registry) name the SAME asset by the SAME id, by construction.

MED-1 (golden KAT): assetid_test.go pins AssetIDGoldenVectors — fixed
(networkID, chainID, kind, ref) → expected 32-byte id for all three kinds (incl UTXO).
The mirror test in chains/dexvm/registry asserts the IDENTICAL bytes, locking the
byte-identity the resolve↔register equivalence depends on; a drift in either home's
preimage discipline fails CI in both. Domain-separation test confirms ERC20 vs
EVM_NATIVE never collide and a zero-address ERC-20 / empty source chain is refused.

Verified: CGO_ENABLED=0 go build ./pkg/dexcore clean; full dexcore suite green.
v1.5.16
2026-06-22 22:50:48 -07:00
zeekay 19c18b3cc9 dexcore(dex): permissionless AssetResolver — resolve canonical identity, not allowlist
The value-path admission port becomes PERMISSIONLESS + PUBLIC per the owner's clean
model: an asset trades IFF its canonical identity verifies on-chain, with NO
manifest-membership requirement.

- resolver.go: AssetResolver.ResolveEnabled -> ResolveAsset (proves canonical identity
  permissionlessly; refuses only malformed/wrong-network/explicitly-disabled/synthetic,
  never 'not pre-registered'). RequireEnabledAsset -> RequireResolvedAsset.
  ErrAssetNotAdmitted -> ErrAssetNotResolved (identity-resolve refusal). The EXTCODESIZE
  OnChainAssetVerifier is now documented as the AUTHORITATIVE reality check (a synthetic
  asset has no code -> ErrAssetNotOnChain). Fail-closed errors (no resolver/verifier)
  unchanged.
- place.go OpenMarketChecked: same two orthogonal steps (resolve identity -> verify
  on-chain reality), now permissionless: any REAL (base, quote) pair opens; only
  malformed/synthetic/disabled refused. Value keying (left-pad) unchanged.
- resolver_test.go: replace the membership-allowlist fakeResolver with a permissionless
  resolver. New positive proof (fails without the change): a REAL unregistered ERC-20
  opens a market. Synthetic base now refused by the reality gate (ErrAssetNotOnChain),
  preserving the safety property via the authoritative check, not an allowlist. Disabled
  (kill switch) and malformed refused at resolve; nil resolver/verifier fail closed.

DeriveAssetID untouched (byte-identical). Conservation keying untouched.
2026-06-22 21:16:06 -07:00
zeekay f41e6f5274 fix(dexcore): AMM source credits taker available — symmetric with CLOB
The CLOB source's SettleFills credits the taker's dexcore available with the proceeds;
the AMM source did not (only SpendLocked + journal record), leaving the two sources
asymmetric on the D-surface. A caller draining the taker's transient by the journal
amount then over-drained for an AMM leg (the proceeds were never in available). Credit
the taker's available in the AMM source too, so after ANY swap the taker's available
holds exactly the journal-recorded proceeds. Conservation holds across (ledger + pool
reserves): input left the taker's locked into the pool base reserve; output left the
pool quote reserve into the taker's available — both legs balance to the unit.
2026-06-22 17:52:49 -07:00
zeekay 98d4f676db fix(dexcore): widen AccountID 16->32 bytes — injective for 20-byte EVM addresses
The cEVM home's taker is a 20-byte EVM address; the prior 16-byte AccountID would
fold a 20-byte address lossily, so two distinct addresses sharing a leading 16-byte
prefix could collide as one dexcore account and drain each other's resting-order
proceeds (cross-user drain on the account axis). Widen AccountID to the full 32-byte
injective settlement identity both homes map into without a lossy fold: the cEVM
left-pads the 20-byte address (accountFromAddress), dchain left-pads its 16-byte wire
user. 32 bytes is birthday-safe at 2^128 and injective for every real identity.

matcherHint renders the full 32 account bytes as the matcher's User/UserID string —
the hint is ONLY a matching collision handle, never authoritative for value (settlement
resolves the taker from the explicit AccountID and the maker from the orderuser: row).
Replaces the trimTrailingZeros round-trip (no longer needed: settlement never reads the
hint back).
2026-06-22 17:46:49 -07:00
zeekay fa586680b5 feat(dexcore): shared deterministic core — ledger, settlement, smart-order-router
The caller-agnostic core both DEX homes call (the standalone D-Chain VM over a
versiondb overlay, and the in-process 0x9999 cEVM precompile over 0x9999 storage),
so there is ONE settlement algorithm and ONE router in ONE place:

- ledger.go: custody schema + conserving balance primitives (available/locked) over
  a generic Store, full-injective AccountID(16)/AssetID(32) keys.
- settle.go: integer-exact fill conservation + fail-closed maker identity
  (orderuser: index, ErrMissingSettlementUser, never the 8-byte handle).
- router.go: the on-chain smart-order-router — CLOB-first, AMM-fallthrough,
  best-execution across V4+AMM with split; taker-authenticated MEV floor. Source
  abstraction leaves room for an RFQ/external-CEX source LATER without reshaping.
  RouteClass{PublicCLOB,FHEPrivate} reserves the staged private-pool route.
- journal.go: the JOINT JOURNAL — C-surface deltas recorded for the caller to mirror
  onto EVM balances; D-surface writes go straight to the Store. Both-or-neither.
- source_clob.go / source_amm.go: the two liquidity sources (in-process matcher;
  in-cEVM V2/V3 constant-product, 128-bit exact).
- place.go: maker resting-order place/cancel (custody-gated, fail-closed cancel).
- book.go / root.go: book schema + rebuild; RFC-6962 DEX execution root.

REAL assets only (assetID = left-pad of the 20-byte token address); no synthetic
ticker ids anywhere. 16 tests pass race-clean: conservation, fail-closed ownership
(handle-collision non-value-bearing), deterministic replay across independent
stores, two-layer MEV floor, CLOB/AMM routing + split, BUY-requires-limit.
2026-06-22 17:06:50 -07:00
zeekay f544b3d071 chore: use Lux-native naming for the DEX core/EVM-adapter model
Reword comparison docs, comments, and design prose to Lux's own terms
(native DEX core, EVM adapter / 0x9999 V4 PoolManager, native HFT surface,
one consensus-finalized C+D transition). Rename the test broker-venue label
and oracle source weight to neutral names. Behavior-preserving: no logic,
ABI, or wire format changes.
2026-06-22 16:07:21 -07:00
zeekay fada4dcfc2 fix(clobverify): unique per-run frames (dodge content-addressed dedup)
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.
2026-06-22 15:27:11 -07:00
zeekay 03b804dd3d feat(clobverify): cross-validator native-D-Chain replication probe
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.
2026-06-22 15:02:08 -07:00
zeekay f60386336e feat(dchain): committed-state read RPC (clob_get_trades/orders/markets/book)
The D-Chain VM exposed only write methods (clob_place/submit/cancel/...) over
its HTTP surface; there was no way to read committed chain state, so a fill
proven on the proposer could not be verified as replicated across validators,
and native fills (trade: rows) could not feed markets-display.

Add a READ surface (read.go) orthogonal to the write surface (handler.go):

  - clob_get_trades?since=<h>&limit=<n>  committed trade: log (markets feed +
                                          cross-validator replication probe)
  - clob_get_orders?market=<hex>          resting order: rows for a market
  - clob_get_markets                      every market: id/symbol/assets/summary
  - clob_get_book?market=<hex>&depth=<n>  aggregated bid/ask ladder + totals

Reads are JSON, GET, served via CreateHandlers on the SAME /ext/bc/<D>/dex/
route group as the writes. They read committed state directly under vm.mu —
NO mempool, NO block, NO consensus round — so a read can never change a fill or
balance (proven: TestRead_ZeroConsensusImpact). Every response carries the
accepted-block head (height, lastAccepted, root) so a cross-validator diff is
anchored to an exact block: two nodes at the same height+root MUST return
identical trades.

Trades/markets read durable rows (the committed truth, identical on every node
at a height); orders/book read the in-RAM book (a deterministic fold of the
committed order: rows). The 80-byte trade row carries no poolID (frozen GPU
ABI), so clob_get_trades is the GLOBAL height-ordered log — exactly right for a
single-market devnet (the verification case) and for an indexer that scopes
fills to a market by the order ids it tracks.

State helpers (state.go): readTrades (height-ordered durable scan), listMarkets.
lx: export DEXPrice.Float() + FixedQtyToFloat (one conversion; the internal
fixedPriceToFloat/fixedQtyToFloat now delegate).

TDD (read_test.go, all green, race-clean):
  - TradesAfterCommittedFill: a matched cross is queryable with right px/size/side
  - MarketsAndOrdersAndBook: the three other endpoints reflect committed state
  - ZeroConsensusImpact: 25 reads advance neither height nor mempool
  - CrossValidatorIdenticalTrades: two INDEPENDENT VMs (separate DBs) that
    Accept the SAME block bytes return BYTE-IDENTICAL trades JSON + same root —
    the in-process model of the live 5-validator replication question
  - GenesisEmpty / BadParams: clean empty answers; boundary validation (400/405)

Removed the temporary zzz_fillrepro_test.go (its header said DELETE after;
superseded by read_test.go's committed-fill coverage).
v1.5.15
2026-06-22 14:55:38 -07:00
zeekay bb8c0d0ea1 deps: bump luxfi/database v1.20.3 -> v1.20.4 (prefixdb prefix-iteration fix)
database v1.20.4 fixes prefixdb NewIteratorWithStartAndPrefix synthesizing a
pre-prefix start for a nil-start prefix scan, which returned ZERO rows over a
prefixdb-wrapped store (every plugin VM's chain DB via vm/rpc) when sub-prefix
keys sorted before the target. This stranded the native D-Chain: rebuildBookFromDB
iterates order:<pool> alongside asset:/balance:/locked:/meta: rows, folded an
EMPTY book, and every taker cross produced 0 fills despite durably-committed
asks. With the bump the order book refolds correctly and crosses settle.
v1.5.14
2026-06-22 14:12:03 -07:00
zeekay 27de7256d5 fix(dchain): index processing blocks so plugin-transport Accept resolves them
The luxd plugin transport (vm/rpc) resolves Block.Accept by ID via VM.GetBlock —
unlike Block.Verify, which carries the full block bytes (ParseBlock). The dexvm's
GetBlock only indexed ACCEPTED blocks, so GetBlock(builtID) for a block the engine
had just built returned ErrNotFound; the engine's self-finalize Accept
(finalizeOwnProposal) became a silent no-op, the clob_* submitTx waiter
(handler.go) hung forever, and the consensus forwarder wedged inside the hung
Accept so NO further D-Chain blocks were ever built. The EVM survives the same
ID-only Accept because geth resolves built-but-unaccepted blocks from its block
cache; the dexvm had no equivalent.

Add a processingBlocks index (built/verified-but-unaccepted blocks): BuildBlock
and ParseBlock record into it (ParseBlock returns the stable instance so Verify's
stashed overlay is the one Accept commits), GetBlock consults it after
acceptedBlocks, and Accept/Reject prune it. Smallest change that makes the
plugin Accept(ID) round-trip resolve the same instance built/verified — no
transport, engine, or matcher change.

TDD: TestVMAcceptViaGetBlock drives the exact plugin path BuildBlock ->
GetBlock(id) -> Accept (proven to FAIL without the lookup: 'not found, the
production stall'); TestVMParseVerifyAcceptByID covers the follower
ParseBlock -> Verify -> GetBlock(id) -> Accept. Full pkg/dchain suite green
CGO=0. Every prior test accepted the *Block from BuildBlock directly, skipping
the ID-resolve path the live node uses.
v1.5.13
2026-06-22 12:28:05 -07:00
zeekay 57bbf394b8 fix(dchain): persist head block so GetBlock(lastAccepted) survives restart
The block store is the in-RAM acceptedBlocks map; only bootstrapGenesis and
Accept populate it. On restart loadHead restored the head META (lastAccepted id,
height, root) but NOT the block object, so once the chain advanced past genesis
the engine's GetBlock(lastAccepted) — called immediately after Initialize —
returned ErrNotFound and luxd failed VM init with 'zap initialize: remote error:
get last accepted block: not found', taking the D-Chain down on the first
validator to restart.

Persist the head block's canonical bytes (metaHeadBlock) in the SAME atomic
overlay/batch as the head meta (Accept) and in bootstrapGenesis. loadHead now
reconstructs the head block via parseBlock (id/height asserted to match the head
pointer) and puts it back into acceptedBlocks. A legacy DB with no persisted head
block recovers genesis from the deterministic genesisBlock() (DRY with
bootstrapGenesis) and errors loudly for a non-genesis head rather than silently
resetting the chain.

TDD: TestVMRestartGetBlockHead advances two blocks, restarts (fresh VM, same DB),
and asserts GetBlock(head) returns the block — the exact engine call that failed;
proven to FAIL without the acceptedBlocks repopulation ('not found'), pass with.
TestVMRestartLegacyGenesisDB covers the no-metaHeadBlock genesis recovery. Full
pkg/dchain suite green CGO=0.
v1.5.12
2026-06-22 11:40:51 -07:00
zeekay 80e1018781 feat(dchain): expose native CLOB order ingestion over luxd HTTP router
The in-luxd native D-Chain VM (pkg/dchain) bootstrapped consensus but never
exposed order ingestion: VM.CreateHandlers returned an empty map, so luxd
mounted nothing and /ext/bc/D/* 404'd. RegisterCLOB (the clob_* -> submitTx ->
mempool -> consensus -> Verify-match path) was only reachable over the ZAP
socket transport, which only cmd/dvenue (the abandoned standalone venue) wires.

Wire ingestion the idiomatic luxd way: CreateHandlers now returns one HTTP
handler per CLOB method (/dex/clob_place, /dex/clob_submit, ...), each adapting
the EXACT same rpc.RawHandler RegisterCLOB binds (one method->handler table,
two transports — vm.clobMethods). luxd mounts each as an exact route under
/ext/bc/<chainID> (and the D alias) through the node's own router. An order
POSTed to /ext/bc/D/dex/clob_submit flows submitTx -> mempool.Add -> consensus
PendingTxs -> BuildBlock -> Block.Verify (the matcher) -> Block.Accept, and the
response is the CONSENSUS-COMPUTED outcome. No external matcher, no relay, no
side port, no node image change — the chain is the matcher.

TDD: TestIngestCrossingOrdersMatchInBlock proves two crossing orders submitted
over the HTTP handler produce a fill committed as D-Chain state (trade: row,
height advances). CGO=0; full pkg/dchain suite green; plugin cross-builds
linux/amd64.
v1.5.11
2026-06-22 11:03:15 -07:00
zeekay 59c9a6c347 dchain: native VM plugin builds CGO=0 (drop phantom dchain+cgo gate)
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.
v1.5.10
2026-06-22 03:38:47 -07:00
zeekay 2414b34cba chore: remove dexkeeper (abandon off-chain keeper/venue model)
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).
2026-06-22 03:13:24 -07:00
zeekay 7eff95d6b3 ci(dexkeeper): self-contained inline Docker workflow (luxfi/node pattern)
The hanzoai/.github reusable docker-build.yml is unreachable cross-org from
luxfi/* (GitHub 'workflow was not found' at validation -> 0s fail; the real
reason dex's docker.yml never ran). Give dexkeeper its own inline workflow on
the lux-build ARC pool, mirroring luxfi/node's proven Docker workflow exactly
(PAT resolve -> docker/build-push-action with secret id=ghtok). Align
Dockerfile.dexkeeper to consume id=ghtok. Remove the dexkeeper matrix entry
from the (cross-org-broken) reusable docker.yml.
v1.5.9
2026-06-22 02:55:22 -07:00
zeekay 4afe6b7472 ci(docker): add id-token: write to caller permissions
The reusable docker-build.yml jobs declare id-token: write; a caller whose
top-level permissions are narrower is rejected by GitHub at workflow validation
(the 0s 'workflow file issue' failure — present since the workflow was added).
Every working caller of this reusable workflow grants id-token: write. This is
the actual parse-stage fix (the runner-input + secret fixes were also needed for
the build to RUN, but this is why it never started).
2026-06-22 02:51:53 -07:00
zeekay 57ab21687d ci(docker): fix runner-* JSON inputs + consume gh_token BuildKit secret
The docker.yml runner-amd64/arm64/deploy overrides were bare strings
(luxfi-amd64, …); the reusable workflow fromJson()-decodes them, so they
produced a 0s 'workflow file issue' failure (broken since 2026-06-19). Drop
the overrides -> canonical [self-hosted,linux,<arch>] defaults (the lux arcd
pool's labels; org runner-group + arcd allowlist isolate). Dockerfile.dexkeeper
now consumes the org PAT via --mount=type=secret,id=gh_token (the secret the
reusable workflow actually supplies) for the private luxfi/* module fetch,
ARG GITHUB_TOKEN kept as a local-build fallback.
v1.5.8
2026-06-22 02:50:25 -07:00
zeekay 56af519430 deps: pin zap v0.8.10 + chains v1.3.16 + genesis v1.13.14 (dexkeeper release)
The dexkeeper imports zap v0.8.10 (dexsession calldata) + chains v1.3.16 (dexvm
dex.submitTx/getSettlement RPC). genesis -> v1.13.14 (node's pin; exports
MnemonicEnvVar since v1.13.10 — v1.13.8 lacked it, a break the go.work masked).
CI-mode (GOWORK=off) build + cmd/dexkeeper tests pass.
v1.5.7
2026-06-22 02:46:20 -07:00
zeekay 99c7a8c158 merge: dexkeeper — off-chain keeper driving the 0x9999 two-phase atomic DEX seam
cmd/dexkeeper watches the C-Chain 0x9999 IntentSubmitted event, submits the dexvm
ImportTx + settling RelayOrderTx (dex.submitTx), polls dex.getSettlement for the
D->C proceeds object, then has the maker sign+send the C-side Phase-B settle that
emits DEXFill. Maker key derived in-process from KMS-synced LUX_MNEMONIC at
m/44'/9000'/0'/0/i. Relay sent UNSIGNED (authority binds to escrow owner).
Dockerfile.dexkeeper (CGO=0) + docker.yml matrix entry (ghcr.io/luxfi/dexkeeper)
+ k8s/devnet-keeper manifests. Tests pass.
2026-06-22 02:44:46 -07:00
zeekay 85ce5f482c dexkeeper: CI image build + devnet deploy manifests (KMSSecret + Deployment + drive-swap Job)
- Dockerfile.dexkeeper: pure-Go (CGO off) static keeper image; documents the
  zap>=v0.8.10 + chains-RPC-seam tag prereqs.
- docker.yml: add ghcr.io/luxfi/dexkeeper to the build matrix.
- k8s/devnet-keeper/kmssecret-mnemonic.yaml: SANCTIONED maker-key path — a KMSSecret
  (operator-reconciled) that syncs the devnet mnemonic from lux KMS (lux-infra/devnet,
  /staking key=mnemonic, the proven luxd-staking-kms-sync scope) into LUX_MNEMONIC. No
  plaintext handled out-of-band. Server-dry-run validated against the live CRD.
- deployment.yaml: the keeper in watch mode against devnet C/D RPC + dchain-venue:9099,
  maker key from the synced secret, single replica (proposer-relays-once).
- job-drive-first-swap.yaml: one-shot seed + Phase-A swap to kick the first DEXFill.
- README.md: the full deploy sequence + verification (DEXFill eth_getLogs, subgraph, UI).

DEVNET ONLY. All manifests dry-run clean.
2026-06-22 02:30:40 -07:00
zeekay e71230aca7 dexkeeper: wire export discovery to dex.getSettlement + fix collateralRef
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.
2026-06-22 02:26:06 -07:00
zeekay 4134148ea1 feat(dexkeeper): off-chain keeper driving the 0x9999 two-phase atomic DEX seam
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.
2026-06-22 02:17:38 -07:00