Commit Graph
452 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 2f9a315893 One EVM per process; other EVMs are plugins the host loads
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]>
2026-09-09 12:53:54 -07:00
zeekayandHanzo Dev b50fd4ba8f Take the geth address conversion locally now that BlackholeAddr is an array
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]>
2026-09-06 00:28:23 -07:00
zeekayandHanzo Dev 510f63046b core: recover already-rewound boundary state
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 16:45:31 -07:00
zeekayandHanzo Dev ec335e108b core: rebuild partial accepted state after head repair
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 16:37:18 -07:00
zeekayandHanzo Dev 7178099092 core: repair stale processing head on restart
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 15:25:04 -07:00
zeekayandHanzo Dev b9fd0a5082 core: reschedule tx unindexer for newest accepted head
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 13:04:27 -07:00
zeekayandHanzo Dev 6337b219aa style: gofmt the tracked EVM tree
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 11:51:32 -07:00
zeekayandHanzo Dev da70f3d430 evm: the 0x9999 seam halted at Accept and could never be synced past
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]>
2026-08-07 06:37:17 -07:00
zeekayandHanzo Dev a10a2f1e0e fee: decomplect the split RATIO from the fee DESTINATION
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]>
2026-07-27 12:34:18 -07:00
zeekayandHanzo Dev c4979a244c cevmbridge: co-build cevm GPU archives same-tree as build-mpt (close ABI-layout risk)
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]>
2026-07-22 09:15:44 -07:00
zeekayandHanzo Dev 00c521b7e8 cevmbridge: add cevmgpu build-tag overlay linking cevm GPU host archives
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]>
2026-07-22 01:18:55 -07:00
zeekayandHanzo Dev 4693f7467b cevm: disk-backed resident state — lazy-load a checkpoint at startup, no Go dump
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]>
2026-07-21 20:28:02 -07:00
zeekayandHanzo Dev 1bb675f5b1 cevmbridge: StateLoadFromLazy — bounded-RAM block apply from persisted state
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]>
2026-07-21 16:39:52 -07:00
zeekayandHanzo Dev 449a5f1f79 core/parallel/cevmbridge: durable resident state — StatePersist/StateLoadFrom over zapdb
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]>
2026-07-21 10:39:23 -07:00
zeekayandHanzo Dev 9faa90f10d core/parallel: cevm applier holds RESIDENT state across blocks (no per-block dump)
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]>
2026-07-21 10:15:47 -07:00
zeekayandHanzo Dev 2d93929048 core/parallel: carry EIP-2930 access list into cevm (access-list fee/root parity)
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]>
2026-07-20 14:11:52 -07:00
zeekayandHanzo Dev 9641ce4a6a core/parallel: add cevm diverse applier-conformance harness (measures 5/9 tx-types)
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]>
2026-07-20 13:21:48 -07:00
zeekayandHanzo Dev ba49281f80 core/parallel/cevmbridge: add cevm-vs-Go block/state-layer benchmark
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]>
2026-07-19 21:55:35 -07:00
zeekayandHanzo Dev 7eb5c60167 core/parallel/cevmbridge: fix stale ABI-version guard (1 -> 2)
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]>
2026-07-19 21:43:18 -07:00
zeekayandHanzo Dev 3c323394d3 core/parallel: promote cevm from shadow-verifier to GATED APPLIER
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]>
2026-07-19 21:05:12 -07:00
zeekayandHanzo Dev c435ce5aea core: run the ./core block suite through the cevm shadow verifier and report the honest agreement rate
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]>
2026-07-19 18:14:45 -07:00
zeekayandHanzo Dev f70050eed6 core/parallel: wire cevm (C++ EVM) in as a byte-exact block shadow-verifier
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]>
2026-07-19 18:04:03 -07:00
zeekayandHanzo Dev 2141ab22c4 core: fix range-copies-lock vet warning in rlp_import_test
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]>
2026-07-17 21:01:11 -07:00
zeekayandHanzo Dev 358ed11885 core: fee-path hot-path benchmark — proves dormant split is zero-cost
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]>
2026-07-17 21:00:27 -07:00
zeekayandHanzo Dev 12f259f0c4 core: C-Chain accepted-tip rewind recovery + RLP-seam adversarial test
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]>
2026-07-17 17:21:34 -07:00
zeekayandHanzo Dev 1abb712fe8 core: C-Chain tx-fee disbursement seam — creditTxFee (50/50 split dormant)
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]>
2026-07-17 17:12:55 -07:00
zeekayandHanzo Dev a0cecf5c7c core: head-pin failures degrade non-fatally (match acceptor backend tolerance)
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]>
2026-07-07 20:31:28 -07:00
zeekayandHanzo Dev 0f90bd00f6 core: pin accepted head state root so idle GC can't evict it (durable freeze fix)
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]>
2026-07-07 17:47:03 -07:00
zeekayandHanzo Dev 6f80479bf5 core: pin head state root in triedb dirty cache (Bug B — EVM head-state Reference fix)
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]>
2026-07-07 10:59:15 -07:00
zeekayandHanzo Dev ed0715a31d test: fix stale golden vectors + txpool test baseline (all green)
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]>
2026-07-02 22:17:00 -07:00
zeekayandHanzo Dev d260656efc evm: gate DEX 0x9999 genesis injection on protocol ActivationTime (preserve pre-2025 genesis)
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]>
2026-07-02 16:50:11 -07:00
zeekayandHanzo Dev 919f3559c0 core+miner: rebuild pruned parent state on demand at BuildBlock (post-accept state-loss recovery)
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]>
2026-07-01 10:25:32 -07:00
zeekayandHanzo Dev f510f35b41 core: materialize accepted block state on the accept path (equivocation-commit-gap backstop)
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]>
2026-07-01 06:23:49 -07:00
zeekay 5460f8370e feat(evm): enable standard precompiles — remove vestigial strict-PQ refusal layer
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.
2026-06-27 21:14:05 -07:00
zeekay 5fa8db6037 test(evm): fix snapshot-recovery prepare for non-empty (genesis-active) genesis
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.
2026-06-26 20:03:13 -07:00
zeekay 1c691fb210 test(evm): regen genesis hashes for genesis-active 0x9999 + fix MaxCodeSize drift
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.
2026-06-26 18:34:33 -07:00
zeekay cdbc464bfc feat(evm): genesis-active gateless 0x9999 + StrictPQ — first-run, one way, no dated fork
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).
2026-06-26 14:40:00 -07:00
zeekay 12a9fbcc9a test(evm): regression guard — chain identity bound before NewBlockChain reprocess
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.
2026-06-23 08:54:50 -07:00
zeekay 7341b4a885 fix(evm): bind chain identity before NewBlockChain reprocess (no new type)
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.
2026-06-23 08:53:26 -07:00
zeekay 8272580165 evm: thread real consensus identity + GetCodeSize into 0x9999 value path
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.
2026-06-23 01:06:53 -07:00
zeekay b80a7d0ffd evm: wire the per-network DEX governance controller into the 0x9999 value path
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.
2026-06-22 22:50:19 -07:00
zeekay b0f0787bef feat(evm): wire the 0x9999 value-path asset resolver + native D-client at boot
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.
2026-06-22 20:56:52 -07:00
zeekay 8b3f3cf9bc test(parallel): Block-STM determinism slice on SettleBatch — anti-fork (Phase 1)
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.
2026-06-22 19:39:07 -07:00
zeekay 06c4d8916c precompile env: gate the Call surface to the DEX settlement family (0x9999/0x9996)
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.
2026-06-22 14:37:01 -07:00
zeekay 1e41171291 precompile env: wire 0x9999 ERC-20 Call surface to the DEX precompile
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).
2026-06-22 14:37:01 -07:00
zooqueen 46a7de4200 test(parallel): live GPU ecrecover dispatch proof (Metal vtable, not CPU fallthrough)
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.
2026-06-22 02:36:21 -07:00
zeekay 25daffe90e evm/core: release-gate the cross-repo DexSettleActivationTime DRY guard behind a build tag
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.
2026-06-21 13:51:10 -07:00
zeekay a309d4900a evm/core: guard dex.DexSettleActivationTime == extras.DexSettleActivationTime
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.
2026-06-21 13:16:17 -07:00
zeekay 8e04c4e23f evm: fix 0x9999 dispatch consensus-divergence on replay path + harden value/genesis edges (v1.99.34)
Red review of the 0x9999 dated-fork activation (v1.99.33) found ONE HIGH that
blocked the production deploy, plus a money-path footgun and two guards. The
money path (V4 swap ABI, EXTCODESIZE marker, two-phase atomic settle) is
byte-for-byte unchanged — only the dispatch-path timestamp source, the SubBalance
fail-mode, the genesis-builder guard, and stale comments change. Bumps
precompile v0.5.53 -> v0.5.54 (Red L1 comment scrub).

H1 (HIGH, blocked deploy) — consensus-divergence on the relaunch/replay path:
  core.LuxPrecompileOverrider.PrecompileOverride read the PROCESS-GLOBAL
  params.lastRulesContext via GetRulesExtra(Rules{}). That global is rewritten
  last-writer-wins by SetRulesContext on every Rules()/RulesAt() — including
  concurrent eth_call/estimateGas/miner goroutines. The geth EVM invokes
  PrecompileOverride LAZILY during opcode execution, a different instant than when
  precompileHook set the global. On a live, RPC-serving post-fork node replaying a
  PRE-fork RLP block (admin.importChain), a concurrent post-fork eth_call could
  overwrite the global timestamp between the verify goroutine's NewEVM(pre-fork
  block) and its tx dispatch -> the pre-fork block sees 0x9999 ENABLED and
  dispatches SettleContract.Run during plain-account execution -> STATE DIVERGENCE.
  Fix: the dispatch gate now decides from the overrider's OWN per-EVM fields
  (o.chainConfig + o.timestamp) via the pure params.GetExtrasRules — which performs
  the SAME dated-fork (extras.IsDexSettleActive) computation GetRulesExtra did,
  minus the racy global read. Audited every GetRulesExtra reader: only the overrider
  was on the precompile-dispatch path; no other dispatch/consensus-replay call site
  consults the 0x9999 gate. Every replay of a given block now yields the same
  enabled set on every validator, independent of concurrent activity.

L2 (money-path footgun) — registry stateDBBridge.SubBalance fail-open silent mint:
  when the subBalancer type-assertion failed it logged + returned a zero 'previous
  balance' WITHOUT subtracting, while the caller treats the debit as successful ->
  native value minted. Now FAILS CLOSED: it panics, which the EVM recovers into a
  reverted call, so the enclosing value move rolls back rather than committing
  half-done. Structurally unreachable in prod (the concrete adapter implements
  SubBalance) but no longer mints if a future StateDB omits it.

INFO1 (latent footgun) — genesis-config builders bypassed the dated fork:
  SetAllGenesisPrecompiles/AllGenesisPrecompiles iterated ALL RegisteredModules()
  incl the AlwaysOn 0x9999 and wrote timestamp-0 genesis configs, which would enable
  0x9999 from block 0 on a fresh chain — bypassing the dated fork and re-introducing
  the genesis-marker mutation. Both builders now skip AlwaysOn modules.

INFO2 (tests):
  - core: -race regression TestDexSettle_PrecompileOverride_NoGlobalRace reproduces
    the exact relaunch race (pre-fork verify vs concurrent post-fork eth_call
    clobbering the global) and asserts the pre-fork side ALWAYS sees 0x9999 ABSENT,
    post-fork ALWAYS PRESENT. PerEVMTimestamp pins the gate reads o.timestamp not the
    global. Replay e2e asserts a pre-fork value transfer to 0x9999 credits a PLAIN
    account WITHOUT invoking the precompile. All proven to FAIL under the old
    global-read code and PASS under the fix; -race clean.
  - extras + core: TestGenesisPrecompileBuilders_SkipAlwaysOn / _Skip9999 pin INFO1.
  - subbalance test comment refreshed to 0x9999 + fail-closed semantics.
2026-06-21 12:42:42 -07:00
zeekay d6a6584ad1 dex(0x9999): timestamp-gated dated-fork activation + marker install (v1.99.33)
Replace the v1.99.32 "always-on, dispatch-only, no marker" activation of the
0x9999 DEX settlement money path with the CTO's exact model: a SINGLE canonical
dated fork that both enables dispatch AND installs the EXTCODESIZE marker —
forward, at the fork boundary, never in historical genesis.

The fork: extras.DexSettleActivationTime = 1766704800 (Dec 25 2025 00:00:00 UTC),
defined ONCE in params/extras/network_upgrades.go alongside the Durango/Quasar/
Fortuna/Granite built-in upgrades. NOT per-net config (no per-net upgrade.json
precompileUpgrades entry, no genesisPrecompiles entry) — one dated fork, identical
on every Lux network, DRY. This is the proper canonical fork mechanism for a
system precompile that takes no per-net parameters (0x9999 resolves protocolFee-
Controller=DefaultDAOTreasury and dChainID at runtime from the consensus context).

Two coupled effects, both gated on that one timestamp:

  1. DISPATCH (params/config_extra.go GetExtrasRules): the AlwaysOnModules() inject
     into the enabled precompile set is now wrapped in extras.IsDexSettleActive(ts)
     — present at/after Dec 25, ABSENT before. This is REQUIRED for canonical
     history: replaying a pre-Dec-25 block (the ~/work/lux/state RLP snapshot, e.g.
     lux-mainnet 96369 = 1,082,780 blocks via admin.importChain) must see 0x9999 as
     a PLAIN ACCOUNT so a historical value transfer to that address behaves as it
     did originally, not as a precompile dispatch. The prior unconditional inject
     would have diverged replay from canonical state.

  2. MARKER (core/state_processor_ext.go ApplyPrecompileActivations): on the block
     transition that CROSSES the fork (params.IsDexSettleForkTransition →
     extras.IsForkTransition), write the STANDARD precompile-activation marker
     (SetNonce=1 + SetCode{0x1}) into 0x9999 — identical to the config-driven path.
     This is the forward state upgrade: EXTCODESIZE(0x9999)>0, eth_getCode!=0x, and
     Solidity's contract-existence guard passes for a typed IPoolManager(0x9999)
     .swap(...). The prior change DROPPED this marker to avoid mutating genesis;
     installing it forward at the fork preserves the genesis hash (pre-activation
     blocks get NO marker) while making typed calls work post-activation. Reverts
     the v1.99.32 "writes NO genesis state for always-on modules" skip.

IsForkTransition fires exactly once (!parentForked && currentForked). For a fresh
net whose genesis timestamp is already >= the fork (parent==nil), it fires at
genesis — so fresh/local/new nets carry the marker from block 0 via the SAME
mechanism, no separate genesisPrecompiles entry (CTO step 4). For an existing net
(genesis ts < fork) it does NOT fire at genesis — genesis hash immutable — and
fires forward at the Dec 25 block.

KEPT byte-for-byte: runtime DChainID via BCLookup.Lookup("D"), DefaultDAOTreasury
protocolFeeController, networkID/cChainID from runtime context, AlwaysOn module
tagging + AlwaysOnModules(), all settlement/money-path logic.

0x9010: with precompile v0.5.53 the 0x9010 module is no longer registered, so the
EVM never dispatches it and eth_getCode(0x9010)=0x. registry comment updated to
LP-9999. No EVM-side 0x9010 dispatch reference existed.

deps: precompile v0.5.52 -> v0.5.53.

Tests (core/precompile_alwayson_test.go, rewritten — DRY, replaces the prior
unconditional/no-marker assertions):
  - TestDexSettle_DispatchGate_TimestampGated: 0x9999 absent pre-fork, present
    at/after (replay safety + post-activation dispatch).
  - TestDexSettle_NoGenesisMarker_ExistingNet: genesis (ts<fork) writes no marker
    (genesis hash immutable).
  - TestDexSettle_MarkerInstalledAtForkTransition: marker (nonce=1 + code, EXTCODESIZE>0)
    installed at the boundary block.
  - TestDexSettle_ForkTransitionFiresOnce: fires once; fresh-net genesis>=fork at
    block 0; existing-net genesis<fork not at genesis.
  - TestAlwaysOn_9999_Registered: 0x9999 bridged AlwaysOn.
  core + params/extras green incl. the real-mainnet genesis-hash guards. (Two
  pre-existing unrelated failures on pristine main: params/config_test.go references
  a removed DurangoTimestamp field; params/extras mainnet upgrade.json ringConfig/
  warpConfig timestamp guards — both fail identically with these changes stashed.)
2026-06-21 12:09:46 -07:00