Files
zeekay c0f265e47e evm: wire dex-zap-endpoint -> SetBackend(NewZAPEngine) so 0x9010 routes to the venue D-Chain
config.go: DexZapEndpoint (node-local + version-pinned consensus-safety invariant). vm.go: when
set, SetBackend(NewZAPEngine(endpoint,2s)) BEFORE parseGenesis; INERT when empty (public default
= embedded engine, unchanged). Resolves against published luxfi/[email protected], no replace.
Builds CGO on+off; config decode test added.
2026-06-17 17:26:12 -07:00

428 lines
18 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package parallel
// DEX-fill settlement on the Block-STM engine.
//
// A matched DEX fill settles to a balance mutation: debit the seller, credit
// the buyer, touch the market. That mutation is exactly the unit Block-STM
// parallelises -- a piece of work with an explicit read/write set. This file
// adapts DEX fills onto the SAME speculate -> conflict-detect -> commit engine
// the EVM block path uses (blockstm.go), reusing the shared detectConflicts
// kernel (the predicate the GPU conflict_detect kernel enforces byte-for-byte).
//
// Why this is the right seam (not a second engine):
// - The EVM path approximates a tx's read/write set from To()/coinbase.
// A DEX fill carries its EXACT footprint: {maker account, taker account,
// market}. So the settlement path is MORE precise, not a reimplementation.
// - At 10k markets, distinct fills touch disjoint accounts -> detectConflicts
// finds few edges -> near-linear parallel settlement. The few cross-account
// conflicts (one account in two fills) re-execute in deterministic index
// order, identical to the GPU edge order.
//
// Boundary: this package settles Fill values; it does NOT match orders and does
// NOT depend on lux/dex. The d-chain matcher (dex/pkg/dchain) produces the
// ordered Fill slice and supplies the FillApplyFunc that mutates its state.
// That hook is documented in DChainSettlementHook below.
import (
"fmt"
"runtime"
"sync"
"github.com/luxfi/evm/core/state"
"github.com/luxfi/geth/common"
)
// Fill is one matched DEX fill to settle. It is a value: the accounts and
// market it touches (its conflict footprint) plus the index that fixes its
// deterministic commit order (the matcher's sequence number within the batch).
//
// Apply performs the actual balance mutation against the supplied state. It is
// injected so this package never needs to know asset/ledger internals -- the
// same inversion TxApplyFunc uses for the EVM path. Apply MUST be a pure
// function of the state it is given (no shared mutable capture) so speculative
// execution on isolated copies is sound.
type Fill struct {
// Maker and Taker are the two accounts whose balances move. For a spot
// trade these are the resting-order owner and the incoming-order owner.
Maker common.Address
Taker common.Address
// Market identifies the order book (e.g. the pool/pair address). Two fills
// in the same market that adjust shared market-level state (fee accumulator,
// last-price oracle slot) conflict; fills in different markets do not.
Market common.Address
// Index is the fill's position in the deterministic settlement order
// (matcher sequence). Commit and re-execution honour ascending Index.
Index int
// Apply settles this fill against the state. Return an error to abort the
// batch (e.g. insufficient balance that the matcher should have caught).
Apply FillApplyFunc
}
// FillApplyFunc settles a single fill against a state copy. The d-chain matcher
// supplies this; it debits/credits the two accounts and updates market state.
type FillApplyFunc func(statedb *state.StateDB, f Fill) error
// SettlementExecutor settles batches of DEX fills. On a single state it applies
// in deterministic order with cheap conflict detection (SettleBatch); across
// independent state shards it settles concurrently (SettleSharded) -- the latter
// is where DEX-settlement parallelism actually lives (geth StateDB cannot be
// mutated concurrently even for disjoint accounts; see SettleBatch).
type SettlementExecutor struct {
workers int
}
// NewSettlementExecutor creates a settlement executor. Pass 0 for workers to
// use runtime.NumCPU().
func NewSettlementExecutor(workers int) *SettlementExecutor {
if workers <= 0 {
workers = runtime.NumCPU()
}
return &SettlementExecutor{workers: workers}
}
// fillRWSet builds the exact read/write footprint of a fill: both accounts and
// the market are read and written (balances and market state change). This is
// the precise footprint -- no false positives from coarse approximation, so
// detectConflicts produces the minimal, correct edge set.
func fillRWSet(f Fill) txReadWriteSet {
rw := txReadWriteSet{
reads: make(map[common.Hash]common.Hash, 3),
writes: make(map[common.Hash]common.Hash, 3),
}
for _, a := range [...]common.Address{f.Maker, f.Taker, f.Market} {
slot := common.BytesToHash(a.Bytes())
rw.reads[slot] = slot
rw.writes[slot] = slot
}
return rw
}
// SettleBatch settles fills against a single state in deterministic index order
// and returns the conflict count (for observability). It does NOT copy state per
// fill: a DEX fill is a ~3-account balance mutation (sub-microsecond), so the
// optimistic copy-the-world-per-item model the EVM block path uses (where each
// tx is heavy enough to amortise a StateDB.Copy) is catastrophically wrong for
// fine-grained fills -- measured 311x SLOWER than sequential on this host.
//
// MEASURED FINDING (M1 Max, 10k fills): per-fill StateDB.Copy() => 750 fills/s
// (70 GB allocated); sequential apply on the shared state => 233k fills/s. The
// conflict-detection kernel itself is cheap (2.2M fills/s) -- so the right shape
// is "detect cheaply, then settle without per-item world-copies".
//
// Single-state parallelism is impossible here: geth's StateDB.stateObjects is a
// plain map mutated even on reads (getOrNewStateObject), so concurrent goroutines
// touching DISJOINT accounts still race the map and panic. The parallelism for
// DEX settlement therefore lives ONE level up -- across independent state shards
// (SettleSharded), which is also the LAN / multi-node story. On one state, the
// correct, fastest path is the deterministic sequential apply this method does.
//
// fills MUST be ordered by their matcher sequence; the order is preserved so the
// committed state is identical for every validator.
func (e *SettlementExecutor) SettleBatch(base *state.StateDB, fills []Fill) (int, error) {
n := len(fills)
if n == 0 {
return 0, nil
}
// Conflict detection over the precise fill footprints. This is the
// consensus-relevant artefact (the same edge set the GPU conflict_detect
// kernel produces, KAT byte-equal) and the input to SettleSharded's
// cross-shard handling. Cheap: O(Σ|rwSet|), measured 2.2M fills/s.
_, conflicts := detectConflicts(rwSetsForFills(fills), nil)
// Deterministic sequential apply on the shared state -- no per-fill copy.
for i := 0; i < n; i++ {
if err := fills[i].Apply(base, fills[i]); err != nil {
return conflicts, fmt.Errorf("settle fill %d (market %s): %w",
i, fills[i].Market.Hex(), err)
}
}
DefaultMetrics.BlocksProcessed.Add(1)
DefaultMetrics.TxsProcessed.Add(int64(n))
DefaultMetrics.TxsReExecuted.Add(int64(conflicts))
return conflicts, nil
}
// ShardResult reports a sharded settlement outcome.
type ShardResult struct {
Shards int // number of intra-shard states settled concurrently
IntraShard int // fills settled inside a single shard (parallel)
CrossShard int // fills touching >1 shard (settled serially after)
Conflicts int // total conflict edges across all fills (consensus artefact)
CrossShardCfl int // conflicts among the cross-shard fills
}
// shardOf maps an address to one of `shards` partitions (FNV-1a, deterministic
// and identical on every validator). Used to assign a MARKET to a shard.
func shardOf(a common.Address, shards int) int {
var h uint64 = 1469598103934665603
for _, b := range a {
h ^= uint64(b)
h *= 1099511628211
}
return int(h % uint64(shards))
}
// fillShard assigns a fill to a shard BY MARKET. This is the correct DEX
// partition: a fill belongs to exactly one market, so sharding by market makes
// every fill intra-shard by construction -- the same per-book-arena model the
// matcher uses ("books never share state on the hot path"). The only state that
// crosses a shard boundary is an ACCOUNT that trades in markets assigned to
// different shards (a maker active across markets); those collisions are the
// cross-shard merge cost, NOT the common case.
//
// Sharding by account instead would make a 2-party trade straddle two shards
// almost always (two independent accounts rarely co-locate) -- measured ~100%
// cross-shard at 4+ account-shards. Sharding by market is the fix.
func fillShard(f Fill, shards int) int {
return shardOf(f.Market, shards)
}
// SettleSharded settles a fill batch by partitioning accounts across N
// independent state shards and settling each shard CONCURRENTLY. This is the
// real parallelism for DEX settlement: each shard is its own *state.StateDB (no
// shared map), so the goroutines never race. It is also the in-process model of
// the LAN multi-node settlement -- a node == a shard, the merge == cross-shard
// reconciliation.
//
// Contract:
// - shardStates[i] is an independent state holding the accounts that trade
// ONLY in markets assigned to shard i; the caller seeds each such account
// into that shard, and cross-shard accounts into base (see seedSharded).
// - base is the authoritative state for fills touching a cross-shard account
// (an account that trades in markets on >1 shard) and the merge target.
// After this returns, the canonical post-batch state = the union of the
// shard states + the cross-shard fills applied to base, exactly as a single
// sequential settlement would produce (TestSettleShardedEqualsSequential).
//
// Partition: fills are sharded BY MARKET (fillShard), so a fill is always
// intra-shard for its market. The hazard is an ACCOUNT trading across markets
// that landed on different shards -- two shard goroutines would mutate it
// concurrently. SettleSharded detects those cross-shard accounts up front and
// routes every fill touching one to the serial base tail, so the concurrent
// shard phase only ever touches shard-local accounts -- race-free WITHOUT locks.
//
// Determinism: a shard's fills apply in ascending Index; shards are disjoint by
// construction so their relative order is irrelevant; the cross-shard tail
// applies last in ascending Index. Every validator runs the same shardOf, so
// the partition and the final state are identical everywhere.
func (e *SettlementExecutor) SettleSharded(
base *state.StateDB,
shardStates []*state.StateDB,
fills []Fill,
) (ShardResult, error) {
shards := len(shardStates)
res := ShardResult{Shards: shards}
if shards == 0 {
return res, fmt.Errorf("SettleSharded: no shard states")
}
// Whole-batch conflict count for observability (consensus artefact).
_, res.Conflicts = detectConflicts(rwSetsForFills(fills), nil)
// Pass 1: find accounts that appear under markets assigned to >1 shard.
// acctShard[acct] = the single shard it's seen on, or -1 once it spans two.
acctShard := make(map[common.Address]int, len(fills)*2)
note := func(a common.Address, s int) {
if prev, ok := acctShard[a]; !ok {
acctShard[a] = s
} else if prev != s && prev != -1 {
acctShard[a] = -1 // now cross-shard
}
}
for _, f := range fills {
s := fillShard(f, shards)
note(f.Maker, s)
note(f.Taker, s)
// Market is unique to its shard by definition; no need to track.
}
isCross := func(a common.Address) bool { return acctShard[a] == -1 }
// Pass 2: route. A fill goes to the serial tail iff either trading account
// is cross-shard; otherwise to its market's shard (race-free, shard-local).
perShard := make([][]Fill, shards)
var cross []Fill
for _, f := range fills {
if isCross(f.Maker) || isCross(f.Taker) {
cross = append(cross, f)
res.CrossShard++
} else {
s := fillShard(f, shards)
perShard[s] = append(perShard[s], f)
res.IntraShard++
}
}
// Settle each shard concurrently on its own state. No shared mutable state
// is touched across goroutines, so this is race-free by construction.
errs := make([]error, shards)
sem := make(chan struct{}, e.workers)
var wg sync.WaitGroup
for s := 0; s < shards; s++ {
if len(perShard[s]) == 0 {
continue
}
wg.Add(1)
sem <- struct{}{}
go func(shard int) {
defer wg.Done()
defer func() { <-sem }()
for _, f := range perShard[shard] {
if err := f.Apply(shardStates[shard], f); err != nil {
errs[shard] = fmt.Errorf("shard %d fill %d: %w", shard, f.Index, err)
return
}
}
}(s)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return res, err
}
}
// Cross-shard fills: apply on base in ascending Index (the merge). These are
// the boundary-spanning settlements -- rare when accounts cluster, but always
// correct. Their conflict count is reported for the LAN-merge cost model.
if len(cross) > 0 {
_, res.CrossShardCfl = detectConflicts(rwSetsForFills(cross), nil)
for i := range cross {
if err := cross[i].Apply(base, cross[i]); err != nil {
return res, fmt.Errorf("cross-shard fill %d: %w", cross[i].Index, err)
}
}
}
DefaultMetrics.BlocksProcessed.Add(1)
DefaultMetrics.TxsProcessed.Add(int64(len(fills)))
DefaultMetrics.TxsReExecuted.Add(int64(res.Conflicts))
return res, nil
}
// conflictEdgesOf returns the FULL conflict edge list over read/write sets in
// canonical (lo, hi) ascending order -- byte-identical to what the GPU
// conflict_detect kernel emits (gpu-kernels ops/cevm/.../conflict_detect, which
// flags EVERY conflicting upper-triangle pair, KAT byte-equal to the CPU
// oracle). The predicate is the same three-way intersection the GPU enforces:
//
// pair (lo, hi) is an edge iff
// W_lo ∩ R_hi ≠ ∅ OR W_lo ∩ W_hi ≠ ∅ OR R_lo ∩ W_hi ≠ ∅
//
// Note this differs from the detectConflicts re-execution MASK: the mask only
// needs "fill i conflicts with SOME earlier fill" (one bit per fill), whereas
// this edge list is the consensus artefact -- the exact set the GPU produces
// and every validator must agree on. For a slot written by fills 0,1,2 the GPU
// emits all of (0,1),(0,2),(1,2); this reproduces that.
//
// It builds per-fill an inverted index of which earlier fills wrote each slot,
// so the cost is Σ(edges) rather than the O(N²) the GPU pays with a thread per
// pair -- same output, CPU-appropriate shape. (At low conflict rates Σ(edges)
// is tiny; the GPU's value is the dense/worst-case batch.)
func conflictEdgesOf(rwSets []txReadWriteSet) [][2]int {
n := len(rwSets)
// Per slot, the earlier fills that wrote / read it (ascending index).
writers := make(map[common.Hash][]int)
readers := make(map[common.Hash][]int)
var edges [][2]int
for i := 0; i < n; i++ {
seen := make(map[int]struct{})
mark := func(idxs []int) {
for _, j := range idxs {
seen[j] = struct{}{}
}
}
// hi=i's READS hit earlier WRITES -> W_lo ∩ R_hi
for slot := range rwSets[i].reads {
mark(writers[slot])
}
// hi=i's WRITES hit earlier WRITES -> W_lo ∩ W_hi
// hi=i's WRITES hit earlier READS -> R_lo ∩ W_hi
for slot := range rwSets[i].writes {
mark(writers[slot])
mark(readers[slot])
}
if len(seen) > 0 {
los := make([]int, 0, len(seen))
for j := range seen {
los = append(los, j)
}
sortInts(los)
for _, j := range los {
edges = append(edges, [2]int{j, i})
}
}
// Register i for later fills (after computing its edges -- upper triangle).
for slot := range rwSets[i].writes {
writers[slot] = append(writers[slot], i)
}
for slot := range rwSets[i].reads {
readers[slot] = append(readers[slot], i)
}
}
return edges
}
// ConflictEdges returns the GPU-equivalent conflict edge list for a fill batch
// (canonical (lo,hi) ascending). Exposed so the d-chain side can assert
// CPU==GPU by diffing this against the GPU op_cevm_conflict_detect output over
// the same footprints, and to drive a sharded merge.
func ConflictEdges(fills []Fill) [][2]int {
return conflictEdgesOf(rwSetsForFills(fills))
}
func rwSetsForFills(fills []Fill) []txReadWriteSet {
out := make([]txReadWriteSet, len(fills))
for i := range fills {
out[i] = fillRWSet(fills[i])
}
return out
}
// sortInts is a tiny ascending insertion sort (edge fan-in per fill is small).
func sortInts(a []int) {
for i := 1; i < len(a); i++ {
for j := i; j > 0 && a[j-1] > a[j]; j-- {
a[j-1], a[j] = a[j], a[j-1]
}
}
}
// DChainSettlementHook documents the boundary between this package (settlement
// on the reused Block-STM engine) and dex/pkg/dchain (the matcher + ledger).
// This is the documented adapter seam: the d-chain VM owns the other side and
// is out of scope for this track (another track owns dex/pkg/dchain).
//
// On the d-chain side, Block.Verify already matches orders against a versiondb
// overlay (see dchain-vm-design). To settle the resulting fills in parallel:
//
// 1. After matching, build []Fill from the matched trades:
// fills[k] = Fill{
// Maker: trade.MakerOwner,
// Taker: trade.TakerOwner,
// Market: poolAddr,
// Index: k, // matcher sequence == deterministic order
// Apply: func(sdb *state.StateDB, f Fill) error {
// // debit/credit both legs in the d-chain ledger overlay,
// // update market fee/oracle slot; fixed-point (Q64.64),
// // never float -- the determinism rule from dchain-vm-design
// },
// }
// 2. Settle: conflicts, err := NewSettlementExecutor(0).SettleBatch(overlay, fills)
// 3. Optionally assert CPU==GPU edges at scale:
// cpuEdges := ConflictEdges(fills)
// gpuEdges := <gpu-kernels op_cevm_conflict_detect over the same rwSets>
// require cpuEdges == gpuEdges // the consensus-relevant invariant
// 4. Block.Accept commits the overlay -> zapdb atomically (unchanged).
//
// The hook is deliberately a doc, not code: wiring it lives in dex/pkg/dchain,
// which this track must not modify.
const DChainSettlementHook = "see settlement.go doc: dex/pkg/dchain builds []Fill from matched trades and calls SettlementExecutor.SettleBatch on its state overlay"