A node killed while admin_importChain was accepting a round could not start
its C-Chain again: "required historical state unavailable (reexec=2)". The
import wrote the acceptor tip for the whole round first and then committed the
round's tries one block at a time, so a kill in between left a tip on disk
above the accepted pointer, naming blocks whose state was never written.
reprocessState re-executes from the acceptor tip, which it takes to be at or
below the block it recovers, and searches back from there only 2x the commit
interval for a state to start from.
The import now accepts each block the way the acceptor does, through the one
acceptDurably both call: tip, then trie, a block at a time. And reprocessState
moves an acceptor tip that is above the accepted block back to it, which is
what recovers a database an earlier release left that way.
TestImportArchive_KilledAnywhereRestarts kills an import at every write it
makes, reopens from the accepted pointer the VM would read, resumes, and
restarts once more. On the previous code, at commit interval 1, the kill after
write 11 of 48 does not start again.
Refs #152
Co-authored-by: Hanzo Dev <[email protected]>
admin_importChain and --import-chain-data validate blocks with InsertChain
and then move the accepted tip with SetLastAcceptedBlockDirect. Neither step
does what the acceptor does for a block consensus accepts: index its
transactions and hand its trie to the state manager. An archive node therefore
committed only the roots the import force-committed at round ends and indexed
only each round's last block. After a restart every other height's state was a
missing trie node, eth_getTransactionByHash found one transaction per round,
and with commit-interval 1 the startup head repair could not re-execute the tip
from the height before it, so the C-Chain did not start.
AcceptImported does the acceptor's durable work for each inserted block, in
order, through acceptState, which the acceptor now calls too.
TestImportArchive_HistorySurvivesRestart imports in rounds shorter than the
chain, reopens it on the same database, and checks every height's state and
every transaction's lookup. On the previous code it fails at height 1, and with
commit-interval 1 the chain does not reopen.
A database an earlier release imported keeps the gaps; only a fresh import
under this one fills them.
Fixes#152
Co-authored-by: Hanzo Dev <[email protected]>
The C++ and Rust EVMs are separate luxd plugins. The host spawns every VM with
VM_TRANSPORT=zap (node/vms/rpcchainvm/factory.go) and the cevm binary is
exercised that way cross-process today. Choosing an EVM is choosing which plugin
binary runs -- not a switch inside the Go EVM.
This tree carried a second mechanism for the same choice: an in-process cgo
bridge that linked C++ into the Go binary behind -tags cevm/revm, with a
per-transaction executor registry, an evm_setBackend RPC to flip between
backends at runtime, a resident-store config knob, and a startup log naming the
chosen backend. Its executor returned nil, nil; its tagged build did not compile
(cevm_resident_store.go referenced symbols cevmbridge never defined); and no
build produced a binary that used it. 3,961 lines, gone.
What stays is what the Go EVM actually has: the BlockExecutor seam and
Block-STM behind it (blockstm.go), and the GPU accelerator seam with its
darwin/cgo bridge into luxfi/gpu (gpu_bridge.go). The state processor consults
both and falls through to sequential execution when neither is registered.
Note for the record: nothing in the shipped binary registers a BlockExecutor,
so blocks run sequentially on the Go interpreter. The removed startup log said
"Block-STM across all cores"; it was not describing this binary. Wiring
Block-STM into the production path changes consensus execution and is a
separate decision, made deliberately, with a parity gate.
Co-authored-by: Hanzo Dev <[email protected]>
constants.BlackholeAddr is a plain [20]byte, so constants no longer imports
geth. The fourteen sites here all want a geth common.Address, and each file
already imports geth/common, so each takes the free conversion itself.
Only one site was a compile error: the .Bytes() method call in
rewardmanager.DisableFeeRewards. Everywhere else [20]byte is assignable to
common.Address, so the change was invisible to the compiler, and the two
require.Equal assertions in rewardmanager would have compiled and then failed
at run time, because reflect.DeepEqual compares types before bytes. Those two
are wrapped for that reason, not for style.
The two failures in core, TestHeadPin_Across4096Boundary_NoLeak and the
invalid-chainID-0 panic in TestCreateThenDeletePostByzantium, reproduce
identically with these lines reverted and are unrelated.
Co-authored-by: Hanzo Dev <[email protected]>
Two defects, both fatal, both on the C<->D atomic settlement path.
1. Accept handed the versiondb batch to sm.Apply unconditionally. The
C-Chain EVM is an out-of-process plugin, so its SharedMemory is the ZAP
client, and that client refuses any batch — a database.Batch cannot
cross a process boundary. Every block staging an atomic op would have
failed Accept, which is fatal. Commit the versiondb first, then Apply
with no batch. Without a shared batch exactly-once is unreachable; the
choice is at-most-once or at-least-once, and replay is unsound here (a
duplicate put is fatal, and after the peer consumes the object it
re-creates it). A skipped op is survivable: C's settlement replay guard
is EVM state, not the Remove, and a skipped intent Put is refunded by
reclaimIntent. The residual single-node cost is stated in the code.
2. The settle path read the object from shared memory DURING EVM
execution while the consuming Remove landed at Accept. A node
re-executing that block — bootstrapping, state-syncing, re-tracing —
found the object gone, computed a reverted receipt where the network
computed a successful one, and died on the receipt root. The seam
settled live and wedged every node that tried to sync past it.
Execution now binds to the object bytes the transaction carries and
declares the consumption in its logs; this adds the block-level rule
that proves the declaration against shared memory at Verify, on both
the consensus path and the producer's own pre-proposal verify, so a
forged object costs a block rather than forking a receipt root. The
check is gated on the bootstrap frontier, which the shared-memory layer
requires of any chain consuming cross-chain state.
Co-authored-by: Hanzo Dev <[email protected]>
The fee split braided two independent concerns into one line: how much of a
fee is kept (a protocol constant, floor(fee/2)) and where the kept part goes
(compiled-in extras.FeeRewardVault = 0x0100..02). The second half made the
destination ungovernable and silently bypassed the RewardManager precompile
the moment FeeSplit activated, so turning the burn on would have DISABLED the
one on-chain knob the chain already has.
Split the concerns:
ratio -> stays a right shift. Fixed, auditable, not settable by any
transaction. Changing it remains a release plus a coordinated
fork, deliberately: monetary policy should not move at the
speed of a multisig.
destination -> the configured coinbase, i.e. BlockChain.GetCoinbaseAt, i.e.
the RewardManager precompile's stored address. Already part of
header validity (dummy.verifyCoinbase), so it is verifiable
from the header and movable by one admin transaction.
creditTxFee collapses to one credit site; extras.FeeRewardVault is deleted.
Also adds the ordering gate this created a need for: ChainConfig.Verify now
refuses a config that schedules feeSplitTimestamp while rewardManagerConfig is
not enabled at that time (and allowFeeRecipients is off). Without RewardManager
GetCoinbaseAt falls back to the keyless blackhole, so such a config would burn
half of every fee and strand the other half forever -- the C-Chain already has
~3867 LUX stuck at that address. mainnet/cchain.json currently ships exactly
that shape (feeSplitTimestamp 1785715200, no rewardManagerConfig in any of the
49 precompileUpgrades); the node will now refuse to start on it instead.
This is a no-op on every live chain: mainnet 96369's running config has no
feeSplitTimestamp at all (eth_getChainConfig), the vault balance is 0, and the
branch has never executed. The correction is free today and costs a fork later.
Tests:
core: destination follows the configured coinbase; a redirect mid-chain moves
the whole reward stream while the burn is unchanged; supply conservation
on executed blocks; genesis refuses the ungoverned shape.
dummy: a proposer naming any other coinbase is rejected with ErrInvalidCoinbase,
so crediting the header coinbase is not a validator-capture hole.
extras: the verbatim upgrade.json fragment activating rewardManagerConfig and
feeManagerConfig with the DAO Safe as adminAddresses parses through the
registry, and feeManager's governable surface contains no ratio.
Co-authored-by: Hanzo Dev <[email protected]>
The cevmgpu overlay linked build-phase5b (Apr/May, cevm project 0.2.0) GPU
archives against Jul build-mpt (0.5.2) state libs — nm-clean but era-skewed
ABI layout. Repoint the four GPU .a from build-phase5b to build-mpt-gpu: a
Metal-ON build (LUX_CEVM_ENABLE_METAL=ON) co-built from the SAME current cevm
source tree AND the SAME conan deps as build-mpt (it references build-mpt's
own generators/conan_toolchain.cmake; the GPU backend luxgpu_core_static is
compiled in-tree at project version 0.5.2 — the version build-mpt reports).
GPU host archives and state libs are now the same era by construction, closing
the cross-build ABI-layout risk. Verified on darwin/arm64: `go build -tags
"cevm cevmgpu"` links all four archives from build-mpt-gpu (confirmed via the
full linker command line + a move-aside falsification that fails citing the
build-mpt-gpu paths, then rebuilds green deterministically); the default
`-tags cevm` build still links the build-mpt state libs unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
FRONT 1 of the parallel GPU effort. New file bridge_cevm_gpu.go
(//go:build cgo && cevm && cevmgpu) is a pure LDFLAGS overlay, additive to
bridge_cevm.go: under -tags "cevm cevmgpu" both compile and cgo concatenates
their #cgo LDFLAGS, so the CPU state libs (build-mpt) link alongside the GPU
host archives (build-phase5b Metal-ON build of the same cevm source tree):
libevm-gpu.a + libevm-metal-hosts.a + libevm-kernel-metal.a + libevm-gpu-state.a,
plus -framework Metal -framework Foundation.
Symbol audit (nm): every evm:: core/state/precompile symbol the GPU archives
reference is satisfied by build-mpt's state libs; evm::gpu::* symbols resolve
among the four archives; only Metal/Foundation framework symbols come from
outside (no MetalPerformanceShaders, no luxgpu referenced). Device-free anchors
GPUABIVersion/GPUBackendName pull the GPU dispatch objects into the link.
Verified (darwin/arm64):
go build -tags "cevm cevmgpu" ./plugin/ -> links, 69,125,858 bytes,
Metal+Foundation+CoreFoundation linked, 88 evm::gpu symbols in binary
(EvmKernelHost::create, BlockStmGpuMetal::execute_block, execute_block).
go build -tags cevm ./plugin/ -> links, 68,978,754 bytes (CPU-only,
EvmKernelHost::create count 0 — default cevm stays CPU-only, unchanged).
go build ./plugin/ -> links, 68,701,170 bytes (stub path).
SCOPE: BUILD + LINK only. Metal shaders compile at runtime and
MTLCreateSystemDefaultDevice() returns nil headless, so GPU DISPATCH is
CI-gated on a Metal-capable interactive runner — not claimed here.
Co-authored-by: Hanzo Dev <[email protected]>
Wires StateLoadFromLazy into the resident applier so the C-Chain, on restart,
resumes cevm from a persisted checkpoint (bounded-RAM lazy load) instead of dumping
the full Go state into cevm — the "own disk-backed state" evolution the seed-cap
comment names. Removes the post-restart declined-too-large seed dump.
- core/parallel/cevm_resident_store.go: SetCevmResidentStore(db) (the VM registers a
durable KV; nil = off, unchanged behavior). tryLazyLoadCheckpoint pages the account
trie from disk via cevmbridge.StateLoadFromLazy and sets the resident to the
checkpoint height; persistCheckpoint writes StatePersist records + the height at a
bounded cadence (default 4096 blocks).
- cevm_shadow.go: getResident tries the checkpoint before a fresh handle;
persistCheckpoint runs after a block's cevm root is VERIFIED against the header.
- plugin/evm: CevmResidentStore config flag (default false) → registers a "cevmstate"-
prefixed sub-DB before chain init.
- cevmbridge/bridge_stub.go: StateLoadFromLazy no-op for the non-cevm build.
Safe by construction + triple-gated: nothing runs unless built -tags cevm, the VM
registered a store, AND a checkpoint exists; and the applier still verifies EVERY
cevm root against the header, so a stale/wrong checkpoint only forces a resync (a
Go-dump seed next block), never a wrong commit. nil store (the default) is the prior
in-memory, seed-from-Go behavior byte-for-byte.
Proof (cevm_resident_store_cevm_test.go, //go:build cevm): seed + apply a block,
persistCheckpoint, simulate restart (free handles, retain store), getResident
LAZY-LOADS the checkpoint (height + root recovered, no Go dump), then the next block
applies to a byte-identical FULL root vs a full-reseed reference. Full parallel cevm
suite green; default (non-cevm) plugin build unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
Wires cevm's C-ABI lazy load into the Go bridge so a node applies blocks over
persisted state without materializing all of it. StateLoadFromLazy builds a
store-backed cevm StateDB (via C.cevm_state_load_lazy) whose account trie is a
from_store_root ref at the persisted root; reads and writes fault account leaves
and storage sub-tries in from zapdb on demand, so RAM is bounded to the block's
touched working set. StateApplyBlock on the handle produces the FULL state root
paged.
Lifetime: unlike a full reseed (which touches the store only during the load),
a lazy handle faults through the store on every later apply, so its cgo.Handle
AND its cevm_kv_store (C-heap allocated) are retained in lazyResources and freed
by StateFree after cevm_state_free has stopped the C++ side faulting through them.
Proof (store_lazy_cevm_test.go, //go:build cevm): seed + apply 2 blocks, persist
to a REAL on-disk zapdb, restart (free handle + close db), reopen, then load the
SAME state two ways — StateLoadFrom (full reseed) and StateLoadFromLazy — and apply
an identical next block to both. Both recover the committed root; the paged apply
root is byte-identical to the full-reseed apply. The full cevmbridge cevm suite
(incl. the existing restart-survival test) stays green.
Co-authored-by: Hanzo Dev <[email protected]>
Wire cevm's new persist/load C-ABI to a geth-style database.Database (in
production luxd's own zapdb), and prove restart-survival.
- store_cevm.go: StatePersist(handle, db) / StateLoadFrom(db) -> handle.
A cevm_kv_store whose get/put point at Go trampolines is built in C
(cevmbridge_make_store), so cevm (C++/cgo) calls db.Get/Put back through
THIS Go runtime — no second Go runtime is embedded (the two-runtime
constraint). The ctx is an opaque cgo.Handle token cast uintptr->void* in
C (no uintptr<->unsafe.Pointer in Go).
- store_callbacks_cevm.go: the //export get/put trampolines (kept separate —
//export forbids C definitions in its file's preamble) resolving the ctx
back to a *kvStore over a Get/Put backend.
- bridge_stub.go: !cevm no-op StatePersist/StateLoadFrom keep `go build ./...`
green without the tag.
- store_cevm_test.go (//go:build cevm): RESTART-SURVIVAL PROOF. Seed + apply
2 blocks against a resident cevm StateDB, StatePersist to a REAL zapdb
(zapdb.New on t.TempDir()), record the root; then StateFree the handle AND
Close the zapdb (full restart), reopen the zapdb from disk, StateLoadFrom
into a NEW handle, and assert StateRoot(new) == pre-restart root
byte-identical. Then apply the NEXT block on the reloaded handle and assert
it matches the stateless (geth-golden) reference.
Result: "persisted 300 accounts to zapdb, reloaded, root
0x5e9db5d6...794d74f == pre-restart root"; next block after reload matches.
All existing gates stay green: cevmbridge golden + resident-across-blocks,
core/parallel conformance 9/9, resident 256-block, shadow 256-block e2e.
MVP scope (honest): persists account state; the resident MPT is rebuilt on
load (root-identical), NOT yet bounded-RAM load-on-demand.
Co-authored-by: Hanzo Dev <[email protected]>
The cevm gated applier (cevm_shadow.go) dumped the WHOLE Go pre-state
every block and rebuilt a fresh cevm StateDB for each — capped at
maxShadowAccounts, which WAS the declined-too-large decline at scale.
Wire the persistent cevm StateDB behind a per-chain handle instead:
- bridge_cevm.go: StateCreate/StateSeed/StateApplyBlock/StateRoot/StateFree
wrappers over the new cevm_state_* C-ABI; marshalling factored into
shared marshal*/unmarshal* helpers (DRY with ProcessBlock). Stubs in
bridge_stub.go keep the default (non-cevm) build a no-op.
- cevm_shadow.go: a residentState per chain (keyed by chain ID). The FIRST
block seeds cevm once from the Go pre-state dump; SUBSEQUENT in-sync
blocks call StateApplyBlock with NO dump and compare cevm's resident root
to header.Root. On AGREE the four-way apply gate writes cevm's post-state
delta into the Go statedb (unchanged) and the resident advances to height
N. On DISAGREE/DECLINE/ERROR the resident is flagged out-of-sync and
re-seeded from the (correct) Go state on the next block. New counters
ResidentApplied / ResidentSeeds / ResidentReseeds. A wrong resident root
can never commit — the root check + four-way gate decline it.
Proof (CGO_ENABLED=1 -tags cevm):
- TestCevmResidentVerifiesRealBlocks: over the same 256 real InsertChain
blocks, cevm's RESIDENT root == Go header.Root at ALL 256 heights,
applied via the resident handle, with ResidentSeeds == 1 (not 256) and
0 resyncs — the declined-too-large killer: 1 full-state dump vs 256 blocks.
- TestStateApplyBlockGoldenRoots + TestStateResidentAcrossBlocksMatchesStateless
(cevmbridge): the resident C-ABI is byte-identical to the geth golden
roots and to a stateless reseed across blocks.
- Existing TestProcessBlockGoldenRoots (stateless), the 256-block
TestCevmShadowVerifiesRealBlocks, and the 9/9 applier conformance all
stay green.
Caveat: this makes the SHADOW/applier resident (no per-block dump). cevm as
the SOLE mainnet engine additionally needs zero declines + disk-backed state
+ node ownership — not claimed here.
Co-authored-by: Hanzo Dev <[email protected]>
The cevm gated applier declined EIP-2930 access-list txs as DISAGREE: the
access list never crossed the bridge, so cevm charged only the 21000 base
intrinsic where the Go EVM charges 21000 + 2400/addr + 1900/key. On a
fee-bearing block that 4300-gas shortfall under-credits the coinbase and
diverges cevm's keccak-MPT root from header.Root, so the four-way gate
(correctly) routed the block to the Go EVM.
Carry the access list end-to-end:
- cevmbridge.Tx gains AccessList []AccessTuple (nil for legacy txs); new
AccessTuple{Address, StorageKeys}.
- bridge_cevm.go marshals it across cgo into the appended cevm_pb_tx.access_list
(tuple array + flattened key buffers allocated in C memory — cTxs is Go
memory passed to C and cgo forbids nested Go pointers — freed on return).
- cevm_shadow.go populates it from tx.AccessList().
Pairs with luxcpp/cevm's process_block surcharge + EIP-2929 warm-set. The
09-access-list-tx conformance scenario flips DISAGREE -> APPLIED
(Agree:1 Applied:1), lifting the applier to 9/9 diverse tx-types. No
regression: the ABIVersion==2 guard, TestProcessBlockGoldenRoots, and the
256-block TestCevmShadowVerifiesRealBlocks (256/256 applied) stay green, and
the default (non-cevm) build is unchanged (additive shared type + stub path).
Co-authored-by: Hanzo Dev <[email protected]>
Adds core/parallel/cevm_applier_conformance_test.go (//go:build cevm) — a
permanent conformance gate that measures cevm's REAL per-tx-type apply/decline
coverage as the gated APPLIER (cevm_shadow.go). For each of 9 diverse scenarios
it builds a fresh preimages-ON chain, generates ONE block exercising exactly one
tx-type (CALL-target contracts pre-deployed in genesis so the measured block is
a single call), resets the shadow counters, runs chain.InsertChain (the applier
path), and records the ShadowStats delta.
Measured today (CGO_ENABLED=1 go test -tags cevm ./core/parallel -run Conformance):
APPLIED (5): value-transfer, call-sstore, call-log1, call-oog, multi(transfer+call)
DECLINED (4): contract-create, call-revert, precompile-identity, access-list-tx
every decline is DISAGREE (cevm's independent keccak-MPT root != Go header.Root);
none hit reverted-mismatch / errored / declined-*. InsertChain OK on all 9
(the Go EVM always applies regardless of cevm's verdict).
Headline: cevm applier covers 5/9 diverse tx-types. New findings vs the doc's
"safe subset" claim: LOG1 and out-of-gas calls DO apply; contract CREATE and
top-level REVERT do not. This table is the punch-list for widening cevm parity.
Measurement only — does NOT modify cevm_shadow.go or the bridge. Tag-gated, so
the default no-tags build is unchanged (build+vet green; -run Conformance finds
no test without -tags cevm).
Co-authored-by: Hanzo Dev <[email protected]>
Apples-to-apples benchmark of the cevm (C++ EVM) cgo ProcessBlock path vs
the Go EVM (luxfi/geth core/state) raw state transition, on the golden block
of N value transfers that both engines prove to the same keccak-MPT root
0x57b7250733ad7f7a...020099.
- TestBenchRootParity: validity gate. At N=1000 cevm==go==geth golden; at
N=10000 cevm==go mutual. If roots diverge the comparison is invalid.
- BenchmarkCevmProcessBlock: whole cgo ProcessBlock (seed C++ state + full
validated EVM block + real keccak-MPT commit + result marshalling).
- BenchmarkGoStateApply: Go state transition only (Sub/Add/SetNonce +
IntermediateRoot(true)); seeding excluded. Narrowest Go baseline.
- BenchmarkGoStateSeedApply: Go seed+apply+root, the pure-function analogue
of ProcessBlock.
Measured (Apple M1 Max, best-of-5, -tags cevm): at N=10k cevm ProcessBlock
~24.5ms / ~8585 Mgas/s vs Go-apply ~71.2ms and Go-seed+apply ~87.6ms — cevm
~2.9x / ~3.6x faster while doing strictly more work (full EVM + validation +
gas + cgo round-trip vs state-mutation-only).
Co-authored-by: Hanzo Dev <[email protected]>
The cevm C-ABI is now v2 (post-state delta export); the golden-roots test
guard still expected v1 and Fatalf'd before the byte-exact root check. Roots
unchanged across v1->v2. TestProcessBlockGoldenRoots now green (N=1/100/1000
byte-match geth).
Co-authored-by: Hanzo Dev <[email protected]>
cevm now ACTUALLY EXECUTES blocks (applies post-state) in luxfi/evm's block
executor, not just shadow-verifies the root — safely gated so it can never
diverge consensus.
Bridge (cevmbridge): ProcessBlock now surfaces the ABI-v2 post-state DELTA and
logs the C-ABI already exports — PostAccounts (address, absolute nonce/balance/
code, Deleted, CodeChanged), PostStorage (address, key, value), and Logs
(address, topics, data, tx_index) — alongside the root + per-tx results.
Executor (cevm_shadow.go): on the AGREE path (cevm root == header.Root) the
executor PROMOTES to applier under an APPLY-THEN-VERIFY-ELSE-DECLINE gate that
is consensus-safe BY CONSTRUCTION:
- reconstruct receipts from cevm's per-tx {status, cumulative gas, gasUsed,
logs} + txs (Type, TxHash, ContractAddress via crypto.CreateAddress for
CREATE, per-receipt Bloom) mirroring core/state_processor.go applyTransaction;
- replay cevm's post-state delta onto an ISOLATED statedb.Copy() and compute
its IntermediateRoot(IsEIP158);
- keep the writeback ONLY IF all FOUR quantities block_validator.ValidateState
checks byte-match the header: state root, aggregate bloom, receipt-trie hash
(DeriveSha), and cumulative gas. Then replay the identical delta onto the
LIVE statedb and return the receipts — the Go EVM does NOT re-run the block;
- on ANY mismatch, or an unrepresentable block (blob tx, cevm-rejected tx,
tx-result count skew, missing preimage, too-large, disagree, finalize-gap),
return (nil, nil) with the LIVE statedb UNTOUCHED so the Go EVM applies.
The trial runs on a deep Copy() rather than Snapshot()/RevertToSnapshot()
because IntermediateRoot -> Finalise -> clearJournalAndRefund invalidates the
journal, making a post-IntermediateRoot revert unreliable; verifying on a copy
leaves the live statedb pristine on every decline (nothing to revert). A wrong
or incomplete delta can therefore never reach the live state.
New honest counters: Applied (four-way gate passed, cevm EXECUTED the block) and
RevertedMismatch (agreed on root but the gated writeback/gate failed → declined
to Go). Invariant: Applied + RevertedMismatch == Agree.
Proven (-tags cevm, build-mpt CPU): the e2e test drives 256 real InsertChain
blocks (768 transfers) through Process -> ExecuteBlock and reports
Applied:256 RevertedMismatch:0 Disagree:0 — cevm executed every block in the
proven safe subset and InsertChain succeeded (ValidateState's four-way check
passed on the written-back state). Whole ./core suite stays GREEN: 9794 blocks
observed, all safely declining (preimages off => cannot seed cevm) and falling
through to Go, no regression. ./core/parallel suite green.
Co-authored-by: Hanzo Dev <[email protected]>
Wires the existing cevm (C++ EVM) byte-exact shadow verifier so it observes
EVERY block of the real ./core suite, and reports the true tally — without
weakening the gate or ever letting cevm apply state.
- core/main_cevm_test.go (//go:build cevm): a cevm-only TestMain that runs the
whole ./core suite then prints the aggregate parallel.CevmShadowStats(). Because
state_processor.go:124 routes every block through
parallel.DefaultExecutor().ExecuteBlock — which under -tags cevm is the
cevmShadowExecutor — this is the honest whole-suite count. main_test.go's goleak
TestMain is gated //go:build !cevm so exactly one TestMain exists per build.
- core/parallel/cevm_shadow_registration_test.go (//go:build cevm): asserts
DefaultExecutor() is the cevmShadowExecutor (not the no-op fallback) — the
precondition for the whole claim.
- core/parallel/cevm_shadow_e2e_test.go: scaled the real-StateProcessor agreement
proof from 3 to 256 genuine InsertChain blocks (768 transfers). cevm's
independent real-MPT process_block root byte-matched the Go EVM header.Root on
ALL 256/256 blocks (0 disagree, 0 finalize-gap).
- Four precompile test files (genesis_dexsettle_reproduce, precompile_alwayson,
precompile_statedb_subbalance, precompile_env_call) gated //go:build !cevm: they
alone pull precompile/registry -> luxfi/precompile/dex -> luxfi/dex/pkg/dex whose
cgo file #includes the absent dex_gpu.h, breaking the CGO_ENABLED=1 build the
bridge needs. They are precompile tests, not block-execution tests, and still run
in every non-cevm build — an orthogonal blocker isolated, not a cevm regression.
HONEST RESULTS (CGO_ENABLED=1 -tags cevm):
- Whole ./core suite: 78 PASS / 0 FAIL / 32 SKIP. cevm observed all 9794 blocks;
it declined every one as no-preimage (the production default runs preimages OFF,
and the shadow needs trie-key preimages to recover the 20-byte addresses that
seed cevm) — 0 disagree, 0 errored, 0 false roots. The Go EVM applied every
block; cevm never touched state.
- Real-block agreement (preimages ON, the config the shadow requires), driven
through core.BlockChain.InsertChain -> StateProcessor.Process -> the shadow:
256/256 blocks byte-exact agreement (768 transfers).
Default (!cevm) build unchanged: ./core and ./core/parallel green in both CGO
modes; the 10 gated precompile tests present in the default build.
Co-authored-by: Hanzo Dev <[email protected]>
Route-gated activation of the Lux C++ EVM (cevm) through the BlockExecutor
seam (SEAM 1): parallel.RegisterExecutor makes core/state_processor.go run
ExecuteBlock on every block. Because the CONSENSUS root is taken from the Go
statedb.IntermediateRoot() (block_validator.go), cevm-as-applier would need
host-callback state write-back (deferred); so this lands cevm as a pure
SHADOW VERIFIER — it recomputes the block's keccak-MPT root independently and
confirms it matches the Go EVM's header.Root, then ALWAYS returns (nil,nil)
so the Go path applies the block. Root-only checking sidesteps both known
cevm gaps (event logs, state write-back).
core/parallel/cevmbridge: cgo seam to cevm's extern "C" cevm_process_block
(luxcpp/cevm build-mpt CPU libs, real MPT commit — NOT the placeholder-root
gpu path). Two variants share one API: bridge_cevm.go (//go:build cevm) links
the verified build-mpt + Conan link line and calls the real process_block;
bridge_stub.go (//go:build !cevm) is pure Go and declines, so `go build ./...`
stays green on any machine and the C++ path is opt-in via -tags cevm (the
existing backend_cevm.go convention). Balance is LE limbs; value/gasPrice/ctx
256-bit fields are BE — matching go_process_block.h exactly.
core/parallel/cevm_shadow.go: the BlockExecutor. Snapshots the pre-block state
read-only via DumpToCollector (address<-trie-key preimage, nonce, balance,
code, storage), builds the cevm tx array (sender via block signer, effective
gas price matching evm/core/state_transition.go's full-fee-to-coinbase model),
runs cevmbridge.ProcessBlock, and cross-checks the root against header.Root.
Atomic counters tally agree / disagree / finalize-gap (withdrawals move
header.Root; cevm runs no engine.Finalize — a state no-op on Lux otherwise) /
declined-too-large / declined-no-preimage / declined-tx / errored, read via
CevmShadowStats. Panics in the snapshot/bridge are swallowed and counted: the
verifier can never destabilise consensus. Registration is gated on
cevmbridge.Enabled so default and -tags parallel builds are byte-unchanged.
parallel.go: initialize txExecutors at declaration instead of in init().
Backend files register from their own init() (backend_cevm.go under -tags
cevm), and Go orders init() funcs alphabetically by filename — backend_cevm.go
precedes parallel.go, so the old init()-time map creation raced too late and
panicked on a nil map under -tags cevm. Var initializers run before all
init() funcs, fixing the order deterministically.
Proven:
- cevmbridge golden roots through the Go marshalling byte-match luxfi/geth:
N=1 0xb9d849f9…805e9, N=100 0x2028713f…7cf0f, N=1000 0x57b72507…20099.
- e2e (TestCevmShadowVerifiesRealBlocks, -tags cevm): 3 real blocks through
core.BlockChain.InsertChain -> StateProcessor.Process -> shadow ExecuteBlock;
cevm's real-MPT root byte-matched the Go EVM header.Root on all 3
(Processed:3 Agree:3 Disagree:0, zero declines/errors).
- default (stub) core/parallel suite unchanged and green.
Co-authored-by: Hanzo Dev <[email protected]>
Range by index + take the slice element address instead of copying types.Block
(which carries an atomic.Pointer cache). go vet ./core/ is now clean.
Co-authored-by: Hanzo Dev <[email protected]>
creditTxFee runs once per tx. Bench (darwin/arm64, -benchtime=2s):
RawBaseline (pre-split, bare AddBalance) : 168.6 ns/op 4 allocs
Legacy (shipped dormant path) : 162.9 ns/op 4 allocs <- within noise
Split (active vault credit + burn) : 175.5 ns/op 5 allocs
The GetExtra+IsFeeSplit branch adds no measurable overhead when dormant; the
split path costs ~7% + 1 alloc only when the DAO activates it.
Co-authored-by: Hanzo Dev <[email protected]>
FinalizeRewind (core/rewind.go) rolls the EVM accepted tip DOWN to a
consensus-finalized height — offline recovery for the proposervm/EVM height
inconsistency (mainnet incident 1085xxx: inner EVM ran ahead of finality after
RLP import->produce). Fail-closed: asserts head==lastAccepted==target with
state present and canonical-above removed; INVALIDATES (never repoints) the
flat snapshot so it rebuilds from targets trie (repointing would serve the
discarded tips balances fleet-wide). Zero-re-exec target via
HighestCommittedStateAtOrBelow.
rlp_seam_red_test.go: RED proof of the import->produce seam (the exact
regenesis flow) — produced blocks chain contiguously onto the imported RLP tip
with no gap/divergence. Passes on real data (lux-mainnet 1083347..1083873
contiguous; zoo 12301 blocks). rewind_test reproduces the recovery at unit
scale. All pass CGO off.
(cmd/repair-cchain, the offline CLI wrapper, is unfinished WIP and NOT included
— its capability lives here in FinalizeRewind.)
Co-authored-by: Hanzo Dev <[email protected]>
Single fee-disbursement seam in state_transition.go delegates to creditTxFee
(core/fee_split.go): full-fee-to-coinbase legacy path by default, or a
deterministic 50%-true-burn + 50%-to-FeeRewardVault (0x0100..0002) split once
the standalone FeeSplitTimestamp fork is active. Gated OFF everywhere
(FeeSplitTimestamp nil) so the LIVE path stays RewardManager -> DAO Gov Safe;
this is the ready in-protocol-split option the DAO can enable later.
Proven (CGO off): fee_split_test (split math, conservation, odd-wei, determinism,
legacy), fee_split_block_test (end-to-end burn on accepted blocks),
reward_manager_routing_test (100% -> DAO Safe + live on-chain setRewardAddress
redirect). params/extras suite green incl. the 3 mainnet boot-compat guards.
Co-authored-by: Hanzo Dev <[email protected]>
Red finding: pathdb Reference returns 'not supported'; a fatal pin would wedge
Accept/startup on that backend. Warn + continue = pre-fix behavior at worst.
Co-authored-by: Hanzo Dev <[email protected]>
The C-Chain froze at an idle tip with "missing trie node" for the head's
state root: coreth's cappedMemoryTrieWriter keeps only StateHistory(32)
roots referenced via the tipBuffer and commits to disk every
CommitInterval(4096); when the chain sits idle at a non-commit height the
head root can be dereferenced/capped before it is persisted, so the next
BuildBlock cannot materialize the parent state and the chain wedges. The
prior insert-time Reference (writeBlockAndSetHead) is refcount-BALANCED and
is consumed as the block ages through the tipBuffer, so it does not protect
an idle head — with duplicate empty-block roots the tipBuffer Dereference
lands on the live head.
Fix: hold a dedicated, UNBALANCED GC reference on the accepted head's state
root, transferred head-to-head (Reference the new head, then Dereference the
previous head) so exactly one live head pin exists at all times on
lastAccepted.Root(). Established in Accept, in the two direct setters, and on
the loaded head at startup, so the invariant "accepted head state root is
never GC-eligible" holds across idle windows, duplicate roots, cold caches,
small StateHistory, and restart. No-op on pathdb (Reference/Dereference are
no-ops there); active only for the hashdb dirty cache where the wedge occurs.
Restart-then-reexecute remains valid RECOVERY, but is no longer the fix: the
head can no longer be GC'd while idle. DRY via one pinAcceptedHead helper.
Co-authored-by: Hanzo Dev <[email protected]>
hashdb.Update references storage-trie roots but never the STATE root, so every processed
root has parents==0; the StateHistory-deep tipBuffer Dereferences roots as they age out and
on an idle chain that Dereference lands on the live head and evicts it — at a non-CommitInterval
height the head was never flushed, yielding 'missing trie node' at tip → block production
stalls + the accepted tip is lost on restart (compounds the consensus catch-up split).
Fix: one Reference(block.Root(), common.Hash{}) in writeBlockWithState BEFORE InsertTrie
(hash-scheme only), refcount-balanced against the matching AcceptTrie/RejectTrie eviction.
Touches ONLY in-memory GC pinning — never a state root, block hash, gas value, or on-disk byte.
PROVEN (blue abeb6333): execution-IDENTICAL (golden roots identical RED-unpatched vs
GREEN-patched — cannot change mainnet history); RED reproduces at StateHistory=32 idle blocks,
GREEN passes after 96 idle; no-leak bounded over 4100 roots + across the 4096 boundary; refcount
balanced. head_pin test green (StateAvailableAfterIdle, NoLeakOverLongIdle, ExecutionIdentical,
Across4096Boundary_NoLeak, RestartAtNonBoundary).
Co-authored-by: Hanzo Dev <[email protected]>
Three deterministic test reds, all test-only (no production source change):
- plugin/evm/message: regenerate 6 LE golden vectors (utils LP-023 BE→LE
codec migration @v1.2.0; oracle-verified, not tautological) + fix stale
big-endian doc comments in codec.go.
- plugin/evm/validators/state: convert TestParseValidator fixtures BE→LE
(same codec migration) + doc comments.
- core/txpool/legacypool: decouple testTxPoolConfig from the production
DefaultConfig (which was deliberately bumped in fdc840067 for throughput);
pin canonical geth test limits. NOT a regression — production tx-acceptance
is correct; the test baseline was self-contradictory (per-account min >
global max). Also fixes a 600s package timeout → 20s.
Co-authored-by: Hanzo Dev <[email protected]>
The AlwaysOn DEX settlement precompile (0x9999) had its EXTCODESIZE marker
baked into EVERY chain's genesis state unconditionally (parent==nil in
ApplyPrecompileActivations), mutating the genesis state root. This changed the
genesis hash of every chain genesised before the DEX activation, breaking RLP
re-import of the real archived Lux mainnet (0x3f4fa2a0), Lux testnet
(0x1c5fe377) and Zoo mainnet (0x7c548af4) — all re-genesised Nov 2024, before
the Dec-2025 activation.
Fix: an AlwaysOn system precompile carries a protocol ActivationTime (Dec 2025
for 0x9999, set by the registry bridge — a fork-like constant, not per-net
config). Its genesis marker is installed ONLY IF ActivationTime <= the genesis
timestamp; otherwise it activates later, on the block transition that crosses
ActivationTime during history replay. Dispatch (GetExtrasRules) is gated on the
same timestamp so a codeless address never dispatches. A chain born before
activation reproduces its ORIGINAL genesis byte-for-byte; a fresh chain born
at/after activation still gets 0x9999 at genesis.
Verified against the real RLP block-0 of all three chains (byte-identical
reproduction) plus a fresh post-activation chain. Corrects two pre-existing
tests that had hardcoded the buggy with-0x9999 hashes as "canonical".
Co-authored-by: Hanzo Dev <[email protected]>
The fresh-multi-node block-production halt is NOT an accept-time commit gap (the
accept-path guard added earlier never fires — state IS present at accept). The
loss is POST-accept: the reorg/reject churn equivocation produces on a fresh
chain dereferences an accepted block's trie AFTER it becomes the head, before the
next BuildBlock reads it, wedging the chain with 'missing trie node' / 'failed to
create new current environment'. Verified live: fresh v1.101.1 devnet re-genesis
halted at block 8 with the head-root missing on all 5 nodes and the accept guard
at 0 firings.
Fix: BlockChain.MaterializeState rebuilds the pruned state range on demand from
the nearest ancestor whose state is present; the miner's createCurrentEnvironment
calls it on a StateAt miss so block production continues instead of halting. No-op
on a healthy chain. reprocessBlock's ValidateState re-checks each rebuilt root
against its header. Keeps the accept-path guard as defence-in-depth.
Co-authored-by: Hanzo Dev <[email protected]>
Under equivocation on a fresh chain, the consensus engine can finalize (via the
α-of-K cert / catch-up path) a block that this node verified only as a transient
non-canonical sibling — whose state trie was then dereferenced when that sibling
was rejected — WITHOUT re-verifying it on the finalize path. The node's head then
points at a block whose state root is absent from the trie database, so the next
BuildBlock fails 'failed to create new current environment: missing trie node'
and the chain wedges. This is the fresh re-genesised testnet/devnet block-2 halt
(node v1.32.4 / evm v1.99.52); mainnet is unaffected (its genesis lacks the fresh
precompile suite and its mature operation does not equivocate at the tip).
Fix: in BlockChain.Accept, if the accepted block's state root is not present, re-
execute it on the parent (== last accepted, whose state is present by construction)
before it becomes the head. reprocessBlock's ValidateState re-checks the computed
root against the header, so a non-deterministic transition surfaces loudly rather
than silently. On the common (non-equivocation) path this is a cheap HasState no-op.
The WARN log fires ONLY on the commit-gap, so it doubles as confirming instrumentation.
Co-authored-by: Hanzo Dev <[email protected]>
Enable-everything builder surface (public permissionless launch). The geth-layer
strict-PQ profile (LuxStrictPQ) refused ecrecover / p256Verify / sha256 /
ripemd160 / blake2f / bls12381 / kzg under vm.config.PQ — but ecrecover
(secp256k1) is THE Ethereum signature path every dapp depends on, and the rest
are verify-only / hash, key-safe. Refusing them breaks the builder surface.
The chain now installs NO forbidding pq.Profile: chainConfig.PQ stays nil,
which pq.RefuseUnder(nil) treats as permissive. The generic geth mechanism
(chainConfig.PQ + runPrecompile.RefuseUnder + legacypool nil-check) is kept for
any chain that wants it; Lux installs nothing.
This completes the decomplect started in luxfi/precompile: consensus PQ and the
EVM precompile profile were complected through config.PQ / StrictPQTimestamp.
After precompile dropped contract.RefuseUnderStrictPQ from every verify-only
precompile, the whole strict-PQ projection here was dead. Removed:
- plugin/evm/pq_profile.go (LuxStrictPQ)
- plugin/evm/vm.go: the `if vm.config.PQ { chainConfig.PQ = LuxStrictPQ() }`
install + the unconditional StrictPQTimestamp=0 genesis pin
- plugin/evm/config: the config.PQ bool field (no code writer; was JSON-opt-in)
- params/extras/network_upgrades.go: StrictPQTimestamp field + apply + IsStrictPQ
- precompile/registry/bridge.go + core/precompile_overrider.go: the dead
IsStrictPQ adapters (only existed to feed the deleted StrictPQReporter)
- tests: pq_test.go, strict_pq_activation_test.go, and the two StrictPQ
assertions (+ orphaned helper) in precompile_upgrade_rollout_test.go
Lux's PQ security is enforced at consensus / identity (quasar/p3q), not by
refusing an EVM verifier a dapp asked for. The live lux-mainnet upgrade.json
never actually set strictPQTimestamp, so this matches production.
Build: full module green. Tests: params/extras, plugin/evm/config,
precompile/registry pass.
GenerateChain forcibly uses a hash-scheme trie db to read parent state, but the
snapshot suite runs the chain under both hash and path schemes. With an empty
genesis (pre genesis-active) parent.Root() is the empty root -- readable in any
scheme -- so block generation worked. With the genesis-active 0x9999 marker the
genesis state is non-empty, and a path-scheme chain commits its genesis nodes in
path format, invisible to GenerateChain's hash-scheme reader. state.New then
errored and GenerateChain returned 0 blocks (the error was discarded), so prepare
built an empty chain and restart recovered to genesis (head 0) instead of C4.
Generate blocks on a dedicated hash-scheme genDb via GenerateChainWithGenesis
(the pattern used by the rest of core's tests); the scheme-agnostic block
structures are re-executed into the chain's own db by InsertChain. The gapped and
wipe-crash variants source their extra blocks from genDb too.
The recovery engine is unchanged and verified correct for a non-empty genesis
under both hash and path schemes: on restart driven by lastAcceptedHash=C4 the
head, header and snapshot disk layer all land at C4 and block[4] is present.
genesis-active 0x9999 puts the precompile marker in every genesis, so the genesis
hash changes (correct for a first-run, always-on network): TestSkipPostMergeFields
GenesisHash 0x3f4f->0x5609, TestGenesisBlockForTesting 0x517c->0x4e97. Also fix
pre-existing TestUpstreamParamsValues drift: Lux raises MaxCodeSize to 64KB (65536)
and MaxInitCodeSize to 2x (131072) vs Ethereum's 24576 — the test asserted stale
Ethereum values. params + core-genesis now GREEN.
REMAINING (not merged): the snapshot-recovery tests (TestRestartWithNewSnapshot ...)
fail — a geth snapshot-engine limitation with a non-empty genesis (block[N] of the
snapshot disk layer goes missing on restart). genesis-active EXPOSES it (marker makes
genesis non-empty) but does not cause it; it's a foundational geth-engine fix.
First-run network, no legacy, no pre-activation history to protect, so the
DexSettleActivationTime dated-fork gate is pure complexity. Delete the concept of
an activation time: 0x9999 dispatches unconditionally and its EXTCODESIZE marker
installs at genesis (parentTimestamp==nil); StrictPQ pinned active-from-genesis for
the Lux plugin (whole ecosystem is PQ-native — removed the per-VM config conditional).
Deleted: DexSettleActivationTime const + pointer/accessor/predicates, the cross-repo
mirror, the drift-guard test, and the 'behaves-as-plain-account-before-activation'
replay-safety branches/prose. One activation path; less code. Genesis determinism via
the single core/genesis.go toBlock() derivation. Feature tests green (0x9999 registered/
dispatched/marked-at-genesis; StrictPQ genesis+nil-inert; plugin PQ).
Proves the unclean-restart fix: a non-nil *runtime.Runtime is readable on the
chain the instant construction returns (so it was present for any in-construction
reprocess), and nil stays nil for chains with no identity-gated precompiles.
The 0x9999 DEX value path is identity-gated: the asset resolver refuses to
admit a market unless the running chain's identity matches. core.NewBlockChain
re-executes accepted blocks during construction (loadLastState recovery /
populateMissingTries backfill), but the identity was only bound onto the chain
AFTER eth.New returned (vm.go SetConsensusContext) — so an unclean restart after
a 0x9999 swap re-executed that block with networkID 0 / C-Chain Empty, reverted
the committed swap, failed ValidateState, and the node could not boot.
Fix is purely timing: hand NewBlockChain the *runtime.Runtime the VM already
holds (eth.Settings.ChainRuntime -> core.NewBlockChain) and bind the consensus
context BEFORE reprocess. No new identity type — networkID/chainID already live
on runtime.Runtime; the context is just the last-mile transport to geth's
ConsensusContext API. stdlib runtime aliased stdruntime in blockchain.go.
The 0x9999 DEX precompile must see the true (networkID, cChainID) and prove
an ERC-20 has live code, on BOTH paths:
- block processing: ApplyTransactionWithResults now threads bc.ConsensusContext()
onto blockContext.ConsensusContext when absent (nil-guarded; the miner's
NewEVMBlockContext already sets it). The stale "StatefulPrecompileHook not
exposed" note is removed — vm.NewEVM preserves ConsensusContext onto evm.Context.
- eth_call/estimateGas: ChainContext.ConsensusContext now returns the backend
blockchain's consensus context (which embeds the chain Runtime via
runtime.WithContext), not the RPC request context — the latter made every
identity-reading precompile see networkID 0 / C-Chain Empty (fail-closed) on
eth_call. EthAPIBackend.ConsensusContext exposes it.
- stateDBBridge.GetCodeSize forwards EXTCODESIZE to the internal StateDB so the
OnChainAssetVerifier's C1 live-code proof works; returns -1 (not 0) when
unavailable so the value path fails CLOSED rather than admit a fabricated zero.
Adds cmd/dexlive (real end-to-end 0x9999 value-path harness over a running
localnet C-Chain: deploy 2 ERC-20s, open market, seed maker, taker swap, read
DEXFill + balance deltas; synthetic/no-code negative control must revert) and
the consensus-identity unit tests for both paths.
HIGH-3 host side. The always-on 0x9999 settlement precompile resolves its halt +
pot-seeding authority from contract.AtomicState.GovernanceController(); this is the
production wiring that supplies it — a per-network governance CONTRACT, never a
hardcoded mnemonic-derivable EOA.
- dex_value_wiring.go (installDEXValuePath): bind rt.GovernanceController =
dexGovernanceFor(networkID) onto the chain runtime at boot (the ONE production
site), alongside the asset resolver + native D client, and log it. dexGovernanceFor
is the genesis-set per-network governance-contract seam: it returns ids.ShortEmpty
for every network TODAY (no Governor/Timelock is deployed at a known address on any
Lux network yet), so every network runs FAIL-CLOSED (no halt authority — provably no
single key can DoS the DEX) until its real governance CONTRACT address is wired in,
per net. The directive is explicit: each entry MUST be a governance contract, NEVER a
dev-mnemonic EOA, and NEVER the retired 0x9011…94714. The boot log makes an unset
authority loud ("<unset: halt/seed FAIL-CLOSED — deploy + wire a governance contract>").
- core/precompile_overrider.go + precompile/registry/bridge.go: accessibleStateAdapter
and accessibleStateBridge implement GovernanceController() (ids.ShortID→common.Address,
exact 20-byte conversion); the internal twin interface gains the method. Returns the
zero address (fail-closed) when the runtime/atomic capability is absent.
- precompile/contract/atomic.go (evm-internal twin): AtomicState gains GovernanceController().
Builds clean (CGO off) across plugin/evm, core, precompile/registry; the dexsettle_guard
parity test passes. Normal trading is unaffected (halt switches are emergency controls,
off by default); only the ability to halt is gated — which previously only a mnemonic-EOA
holder had (a vuln), so no legitimate capability is lost and a censorship/DoS vector is removed.
This is the SOLE production install of the always-on 0x9999 DEX value-path trust
root — the seam Red flagged as fail-closed-by-absence (InstallAssetResolver /
InstallDChainClient had zero production callers, so every swap reverted
ErrNoAssetResolver). vm.Initialize now constructs and installs, from the node's REAL
chain context (never a constant):
- a registry-backed dexcore.AssetResolver bound to (networkID, cChainID): it derives
the canonical AssetID with the node's own ids and resolves it in a registry built
from the per-network manifest (EmbeddedManifestFor by EVM chainID, or the runtime-
rooted localnet native manifest). The manifest is identity-bound via
NewRuntimeVerifier (wrong-net/wrong-chain refused) and admitted under the locked-down
DefaultDexAssetPolicy synthetic-config deny-scan.
- the local in-process NativeDChainClient for the book/ledger custody seam (NOT a
remote keeper/venue/ZAP engine).
Wired for ALL networks (mainnet 96369 / testnet 96368 / devnet 96370 / localnet 1337)
so the DEX is live out-of-the-box. FAIL-CLOSED, NEVER FATAL: a construction failure is
logged and the resolver is left UNINSTALLED (swaps revert), never opened; the node
still boots for all non-DEX usage. The install binds the SAME (networkID, cChainID)
the swap path's AtomicState cross-checks, so a mismatch is fail-closed at swap time.
- core/precompile_overrider.go: forward GetCodeSize (EXTCODESIZE) on the concrete
stateDBAdapter so the value path's live-code verifier reads authoritative state.
The determinism half of the unified executor: parallel-eligible settlement
reproduces the canonical sequential state root. Reuses the REAL kernels
(detectConflicts via ConflictEdges, SettleBatch, geth IntermediateRoot) — no
second engine, no mock conflict detector.
Test9999_DisjointFillsParallelRootEqualsSequential
(== TestBlockSTM_DifferentMarketsParallel): fills on different markets ->
detectConflicts finds 0 edges -> SettleBatch root == sequential root. In-test
fail-without: collapsing the two onto one market yields an edge (proves the
detector keys on the market, not vacuously empty).
Test9999_ConflictingFillsReExecToSequentialRoot
(== TestHotMarketBatchDeterministicRoot): two fills sharing an account on one
HOT market -> detectConflicts returns edge (0,1) -> SettleBatch serializes in
ascending-INDEX order -> root == C-then-D AND != the swapped D-then-C. Uses an
order-SENSITIVE hot-market slot (keccak(prev||taker)) so the root genuinely
depends on apply order — a balance-only fill is commutative and would make the
determinism claim vacuous; the in-test guard rejects a commutative Apply.
Fail-without PROVEN by temporary revert: reversing SettleBatch's apply order
makes the root diverge to the swapped order and the conflicting-fills test FAILS
(restored after). Owner-named tests satisfied by the same bodies. Full
core/parallel suite green; no existing tests weakened.
GetPrecompileEnv() is on the SHARED external contract.AccessibleState interface,
so the live Call surface (whose sub-call msg.sender is the asserting precompile's
OWN address) is reachable by every external precompile that type-asserts the
optional callableEnv capability. Bound that blast radius with defense-in-depth:
- precompile/registry/bridge.go: callableEnvAllowlist keyed on the geth env's
self-address (env.Addresses().Self), populated from the canonical
dex.LXSettleAddress (0x9999) + dex.DEXPositionManagerAddress (0x9996) constants
(one source of truth). Every other external precompile (PQ/ZK verifiers at
0x0122xx, future ones) gets a nil callable env and falls back to fail-secure
refusal. No live behaviour change today (none assert callableEnv yet) — it
prevents a FUTURE precompile silently acquiring sub-call power. Sits on top of
each settlement Run's CALL-only guard.
- core/precompile_overrider.go: GetPrecompileEnv comment corrected — the accessor
is off the EVM-INTERNAL interface but IS on the shared external one; reach is
bounded by the bridge gate + the settle CALL-only guard.
- precompile/registry/bridge_precompileenv_test.go: recordingEnv carries a self;
TestBridge_GetPrecompileEnv_NonSettlementSelf_Gated (0x012204 verifier -> nil,
no Call) + _PositionManagerSelf_Allowed (0x9996 -> non-nil); WiredForwardsCall
now models the 0x9999 self.
ROOT CAUSE: the external DEX 0x9999 settlement precompile's ERC-20 leg (transferFrom/
transfer/balanceOf on the token contract) needs the EVM Call surface, but the adapter
chain that delivers its AccessibleState narrowed Call away:
geth evmPrecompileEnv{Call}
-> evm/core accessibleStateAdapter{env} (env captured, not exposed)
-> evm/registry accessibleStateBridge (GetPrecompileEnv() => nil)
-> luxfi/precompile contract.PrecompileEnvironment (no Call)
so poolStateAdapter.callable() got nil and every ERC-20 deposit/withdraw refused with
ErrERC20VaultUnavailable (fail-secure — never minted). Native LUX settlement was
unaffected because it uses GetBalance/SubBalance, already plumbed through the bridges.
FIX (minimal, off the shared many-implementer interfaces):
- evm/core accessibleStateAdapter.GetPrecompileEnv() exposes the geth env it already
holds (concrete accessor, NOT a widening of the internal contract.AccessibleState).
- evm/registry accessibleStateBridge.GetPrecompileEnv() type-asserts that accessor and
returns precompileEnvBridge, which adds the callableEnv-shaped Call the precompile
type-asserts; nil (fail-secure) when no geth env is present.
- Call forwards with NO caller proxying: the token's msg.sender is the precompile self
(0x9999), matching the depositor's approve(0x9999) allowance. Proxying would check
the wrong allowance and break the pull.
geth is UNTOUCHED: vm.PrecompileEnvironment already declares Call; the DEX never runs
through geth's own accessibleStateAdapter (that path serves only the PQ precompiles).
Regression guards: core pins *accessibleStateAdapter satisfies the envProvider accessor
and returns its env; registry pins the bridge returns a non-nil callable env that
forwards Call (value passed, ZERO options) when wired, and untyped-nil otherwise
(no-env + typed-nil traps).
Asserts DefaultGPU().Available()==true and GPU-recovered sender == CPU sender,
proving the block-insert ecrecover path dispatches to the real Metal backend
(libluxgpu_backend_metal.dylib) rather than the CPU fallthrough.
The guard TestDexSettleActivationTime_LayerMirrorsMatch references
dex.DexSettleActivationTime, which is defined in luxfi/precompile and post-dates
evm/go.mod's pinned precompile tag (v0.5.54). Under GOWORK=off (CI), evm/core
failed to build (undefined: dex.DexSettleActivationTime); it only compiled via an
untracked local go.work. The guard was therefore both inert in CI and a latent
CI break.
Move the single cross-repo guard into precompile_alwayson_dexsettle_guard_test.go
behind //go:build dexsettle_guard. Default CI excludes it, so evm/core builds and
tests against the pinned precompile tag with no go.work. The release step bumps
evm/go.mod to a precompile tag that exports the symbol and runs
`go test -tags dexsettle_guard ./core/`, enforcing the DRY invariant atomically
with the bump. precompile cannot host the guard (it does not import luxfi/evm; the
graph is strictly evm->precompile, so referencing extras.DexSettleActivationTime
from precompile would create a cycle). All other 0x9999 activation pins in
precompile_alwayson_test.go use only evm-local symbols and run on every CI build.
The 0x9999 activation timestamp lives in TWO layers that cannot share a symbol: the
canonical extras.DexSettleActivationTime (this evm layer, used by the dispatch gate +
marker transition) and a layer-local mirror dex.DexSettleActivationTime (luxfi/
precompile, used to gate the precompile's new consensus-visible logs — precompile is
below evm in the import graph so it cannot import extras). If they drift, a settlement
could dispatch (extras gate) where the precompile withholds its log (stale mirror) ->
settlement-without-log window -> consensus split on replay. This test imports BOTH and
fails CI the instant either constant moves without the other.
NOTE: compiling this test against the new dex export requires evm go.mod to pin a
luxfi/precompile release that exports DexSettleActivationTime (>= v0.5.55, to be cut
from the precompile branch). Verified locally PASS against the local precompile via a
temporary replace (reverted); the go.mod bump is part of the coordinated release.