A genesis header with Cancun active (CancunTime=0) populated the Cancun fields
(ParentBeaconRoot/ExcessBlobGas/BlobGasUsed) but left WithdrawalsHash nil,
because the assignment was guarded by num.Sign()!=0 (skipped at block 0). RLP
encodes post-merge fields positionally, so a nil WithdrawalsHash followed by
present Cancun fields yields a header ReadHeader cannot decode:
rlp: input string too short for common.Hash, decoding into ...WithdrawalsHash
NewHeaderChain then fabricates a fake genesis header, which has the wrong state
root, so block-building on a fresh L1 fails and the chain produces zero blocks.
Cancun implies Shanghai, so a Cancun header MUST carry WithdrawalsHash. Set it
(EmptyWithdrawalsHash) whenever Cancun is active and it's nil. Chains preserving
a pre-withdrawals genesis hash opt out via SkipPostMergeFields (short-circuits
the whole post-merge block), so they're unaffected.
Verified on lqd devnet (chainId 8675312): genesis header round-trips, balance
readable at 0x0, self-transfer mines block 1.
On lux-mainnet 2026-06-02 every newly-created L2 EVM chain (hanzo, zoo,
spc, pars) panicked at chain create with:
panic in eth.New: unable to commit genesis block to statedb:
triedb parent [0x56e81f17171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421] layer missing
Root cause: the operator does not specify state-scheme in chain config, so
vm.ethConfig.StateScheme = "" enters eth.New. backend.go then calls
customrawdb.ParseStateSchemeExt("", chainDb) which delegates to
geth/rawdb.ParseStateScheme — upstream returns PathScheme as the implicit
default for any database with no stored scheme (every fresh L2). That
silently re-enables path mode even though vm.go's preflight at the top
of Initialize hard-refuses it. Genesis commit then runs through pathdb's
Update with parentRoot=EmptyRootHash and dies the moment the layer tree
is consulted.
Fix: extract the scheme normalisation into resolveStateScheme() and lock
the empty-string default to HashScheme here, before backend.go can inherit
geth's path-by-default. Path explicit is still refused as before, so this
is purely an operator-default fix.
Adds:
- plugin/evm/vm_statescheme_test.go: TestResolveStateScheme covering all
four input cases (empty→hash, hash passthrough, path refused, custom
passthrough).
- core/genesis_pathdb_test.go: TestGenesisCommit_PathDB_{FreshDB,
SetupGenesisBlock_FreshDB} document the upstream pathdb genesis-commit
invariant on a fresh in-memory DB so future drift here is caught
immediately.
Also bumps compatibility.json to register v0.18.4-v0.18.18 with the
existing RPC chain VM protocol (42); restores TestCompatibility (which
was failing on main since v0.18.4 because the table was never updated).
Version: v0.18.17 -> v0.18.18.
Bug fix (closure-swarm v1.28.15 boot blocker):
PQ gate at plugin/evm/vm.go was setting vm.chainConfig.PQ BEFORE
vm.chainConfig was assigned from g.Config — chainConfig was nil at
that point, panic on dereference. Moved the gate AFTER assignment.
Repro: fresh-PVC bootstrap with chain-config.json pq:true caused
"runtime error: invalid memory address or nil pointer dereference"
in the EVM ZAP plugin → C-Chain never bootstraps. Discovered during
localnet 1337 bring-up against v1.28.15.
Fix: move the gate to AFTER `vm.chainConfig = g.Config`. PQ profile
is now correctly installed on a non-nil chainConfig.
Added:
- cmd/rlp-verify/main.go — decodes C-Chain RLP export, verifies
genesis hash matches genesis JSON, each block's ParentHash chains
cleanly. What admin.importChain does sans state execution; sufficient
proof that an RLP+genesis pair is wireable to a luxd boot.
Indirect dep bumps:
- github.com/ChainSafe/go-schnorrkel v1.1.0 (sr25519Verify precompile)
- github.com/cosmos/go-bip39 v1.0.0 (bridgeRegistrarConfig precompile)
- github.com/luxfi/ai v0.1.0 (aiMiningConfig precompile)
Build tag: drop `gpu` tag on darwin gpu_bridge.go (parallel/gpu is one
concept; the darwin gpu bridge always compiles when cgo+darwin).
Lux mainnet C-Chain canonical genesis hash is
0x3f4fa2a0b0ce089f52bf0ae9199c75ffdd76ecafc987794050cb0d286f1ec61e,
matching the 1,082,781-block historic RLP export at
lux-mainnet-96369.rlp. The genesis JSON activates Cancun at
genesis time (cancunTime=0) for MCOPY etc., but the genesis BLOCK
itself predates Cancun and must use the 16-field pre-Shanghai
header format to preserve hash continuity with the historic chain.
luxfi/geth's core.Genesis exposes SkipPostMergeFields for exactly
this case. luxfi/evm shipped the JSON unmarshaling but ignored the
flag in toBlock(), so any post-merge field that fork-activates at
genesis time (ParentBeaconRoot, ExcessBlobGas, BlobGasUsed,
WithdrawalsHash) gets baked into the header and shifts the
computed genesis hash, e.g. to 0x1ade42ec…. luxd then panics
during EnsureGenesisState because the resulting state root has no
parent in pathdb's layertree ("triedb parent
[0x56e81f17…] layer missing"), which blocks mainnet C-Chain
bootstrap and admin_importChain.
Mirror the upstream geth pattern: gate the entire Shanghai+
post-merge field assignment block with `if !g.SkipPostMergeFields`,
and round-trip the flag through MarshalJSON/UnmarshalJSON in
gen_genesis.go.
Verification (TestSkipPostMergeFieldsGenesisHash pins both):
- `skipPostMergeFields: true` → 0x3f4fa2a0… (canonical, RLP block 0)
- flag absent / false → different hash (gate is active)
Renames LUX_-prefixed env vars to their canonical, non-noisy forms:
- LUX_RLP_PATH -> RLP_PATH (test override for the RLP fixture path)
- LUX_VERSION -> LUXD_VERSION (the luxd version being downloaded/cloned)
- LUX_PLUGIN_DIR -> PLUGIN_DIR (the lux plugin install dir, ~/.lux/plugins)
LUX_VERSION had two callers: shell scripts treating it as the luxd
version, and install_luxd_release.sh that already aliased
'LUXD_VERSION=${LUXD_VERSION:-${LUX_VERSION#v}}' on read. Collapse this
to a single canonical name (LUXD_VERSION) and a one-liner v-prefix
strip; no more aliasing.
LUX_PLUGIN_DIR points at ~/.lux/plugins on disk — the path is unchanged,
only the env var name. INSTALL=1 ./scripts/build.sh continues to work,
just driven from $PLUGIN_DIR.
Note: the historical README mention of LUX_VM_RUNTIME_ENGINE_ADDR /
LUX_VM_TRANSPORT (legacy luxfi/vm <= v1.1.0 names) is left in place
because it documents past behavior, per task convention for CHANGELOG/
historical docs.
BREAKING: external callers (CI, dev scripts, operators) must update
env var names. Per CLAUDE.md no-backwards-compatibility rule, the old
forms are removed.
CTO swarm audit found the strict-PQ tx-pool gate was dead code:
the previous commit checked vm.ActivePQProfile() (process-global)
but the EVM plugin sets vm.chainConfig.PQ per-chain and never
calls SetPQProfile. So the global stayed nil and the strict-PQ
branch never fired — classical tx types were still admitted on
strict-PQ chains, defeating the gate.
This commit reads pool.chainconfig.PQ directly — same field the
EVM precompile gate reads (params.ChainConfig.PQ via runPrecompile),
so the tx-pool admission and the precompile refusal are now driven
by the same per-chain profile pin. Multi-chain hosts (strict-PQ +
permissive in one process) get the right gate per pool because
the profile lives on each chain's config.
Drops the vm package import (no longer needed).
Closes red-team finding "per-chain profile" from the strict-PQ
adversarial review.
Closes CR-2 from the cross-stack adversarial audit: PQ posture on
multi-chain hosts moves from a process-global to a per-chain
ChainConfig setting. A strict-PQ subnet and a permissive L1 in the
same node process now refuse / admit classical primitives correctly,
independent of which subnet last initialised.
Surface:
- NetworkUpgrades.StrictPQTimestamp *uint64 (json:"strictPQTimestamp")
nil = chain is classical-permissive (default for non-Lux integrators)
0 = strict-PQ from genesis (set by plugin/evm when config.PQ=true)
- NetworkUpgrades.IsStrictPQ(time uint64) bool — fork rule
- chainConfigAdapter.IsStrictPQ — bridges params/extras into the
contract.StrictPQReporter contract that stateful precompiles call.
- extChainConfigBridge.IsStrictPQ — type-asserts via a local
interface so non-Lux chains integrating Lux precompiles remain
classical-permissive (no contract widening forced on them).
- plugin/evm: when config.PQ=true, vm.chainConfig.PQ is installed
(covers 0x01..0x0a standard precompiles via runPrecompile gate) AND
extras.StrictPQTimestamp is set to genesis (covers Lux stateful
precompiles — KZG, Groth16, PLONK, fflonk, Halo2, BN254-Pedersen,
BabyJubJub, Pallas/Vesta — via contract.RefuseUnderStrictPQ).
Module bumps:
- github.com/luxfi/geth v1.16.91
(bundles MLDSATxType (0x42) + per-chain PQ profile gate)
- github.com/luxfi/pq v1.0.1-0.20260512064747-3d4c9414e15e
(RefuseUnder promoted to method on *Profile)
No semantic change for chains that do not opt into PQ:
StrictPQTimestamp is nil, IsStrictPQ returns false, every classical
primitive admits, gas accounting unchanged.
When the EVM has a non-nil ActivePQProfile (set by vm.Initialize
when chain config.PQ==true), the chain is strict-PQ:
- Refuse every classical tx type (Legacy / AccessList /
DynamicFee / Blob / SetCode) at the pool boundary
- Accept only MLDSATxType (0x42)
When ActivePQProfile is nil (permissive chain), accept the
classical types AND MLDSATxType — clients that want to dual-sign
or proactively migrate can submit ML-DSA-signed txs before the
chain flips to strict-PQ.
ValidationOptions grows AcceptMLDSA bool — the legacy uint8
Accept bitmap can't hold a slot for the 0x42 type byte (the
high-byte design was deliberate: scheme tag matches
NodeIDSchemeMLDSA65 in luxfi/ids; the downside is the bitmap
doesn't fit it).
Closes the gap between the existing EVM precompile PQ gate
(refuses ecrecover etc. INSIDE a contract execution) and the
tx-submission boundary (a classical-signed tx never enters the
mempool on a strict-PQ chain — not "enters but reverts on
ecrecover later").
Requires luxfi/geth@9c60b9dfa (MLDSATxType + MLDSASigner).
- cmd/vmid prints constants.EVMID — Go source of truth for the C-Chain VM
ID. Build scripts and Makefile now call this instead of hardcoding the
base58 string. Drops a stale subnet-evm ID (ag3GReYP…) from the Makefile.
- scripts/build.sh: default output is build/evm; INSTALL=1 installs under
the derived VMID at \$LUX_PLUGIN_DIR. Codesigns on darwin in-line so a
fresh binary doesn't get SIGKILLed by gatekeeper post-cp.
- core/parallel/gpu_bridge.go: build tag flipped from \`!nogpu\` (opt-out)
to \`gpu\` (opt-in) so default builds don't drag in luxfi/gpu cgo.
- go.mod: luxfi/gpu v0.30.0 → v1.0.1. v0.30.0 referenced removed MLX
symbols (mlx_from_slice_int64 etc.); v1.0.1 migrated to lux/gpu.h and
builds cleanly with -tags gpu and -tags \"cevm gpu\".
- LLM.md: build commands updated; \"no canonical / no aliases\" reflects
reality (single VMID, one filename, derived from Go).
Bumps luxfi/geth pin to v1.16.90 to pull in the PQ profile gate
(refuse(Op) at every classical precompile) and the restored
p3q + pulsar registrations. Transitively bumps luxfi/precompile to
v0.5.16.
Additional fixes:
- core/genesis.go: skip WithdrawalsHash on genesis block to preserve
the canonical Lux mainnet genesis hash (chain ID 96369) under
Durango fork activation.
- params/config_extra.go: surface SetEthUpgrades errors with
os.Stderr context for boot diagnostics.
- precompile/contracts/warp/module.go + precompile/modules/registerer.go:
housekeeping.
Removed stale .bak files.
backend_auto.go used to auto-select CppEVM when a GPU was detected
("parallel backend: GPU detected, backend=cevm, gpu=Metal"). But
cevmExecutor.ExecuteTransaction returned (nil, nil) — every tx fell
through to Go EVM regardless. The "GPU acceleration" log line was
misleading; the real path was always Go EVM.
Per LP-108 "one and one only": collapsed auto-select to GoEVM. The
GPU presence is still detected and logged honestly:
parallel backend: Go EVM (Block-STM across all cores)
cores=10 gpu_status="Metal (detected; cevm dispatch wiring
pending — runs Go EVM)"
To re-enable CppEVM auto-selection, the per-tx → batched-block
adapter in backend_cevm.go::ExecuteTransaction must be completed
AND a parity test against Go EVM Block-STM must pass.
Documented the gap in backend_cevm.go and backend_revm.go executor
comments. The (nil, nil) return is the documented "this backend
declines; use Go EVM" contract that the parallel framework's
DefaultTransactionExecutor expects (parallel.go:107).
Build verified: CGO_ENABLED=0 go build ./core/parallel/... clean.
Reference cevm bridge that needs wiring:
github.com/luxfi/chains/evm/cevm/cevm_cgo.go::ExecuteBlock
takes []Transaction (block-batch), TransactionExecutor is per-tx;
adapter must accumulate per-tx work and submit at block close.
This is the production-wiring counterpart to LP-108 Phase 4 vapor
deletion. The "GPU EVM is plumbed" claim is now contingent on the
adapter, not on ambient registration.
Add Block-STM algorithm implementation to core/parallel with:
- TxApplyFunc callback to avoid core<->parallel circular imports
- Goroutine pool with configurable worker count (default NumCPU)
- Address-level read/write set tracking per transaction
- Conflict detection: flag tx[i] when it reads a slot tx[j<i] wrote
- Sequential re-execution of conflicting transactions on merged state
- Cumulative gas accounting in commit phase
- Atomic metrics counters for observability
12 new tests covering executor creation, fallthrough behavior,
RW set construction, conflict detection, cumulative gas ordering,
error propagation, and interface compliance.
In the sequential execution fallback, check for a registered
TransactionExecutor (revm/cevm) and dispatch through it before
falling through to the default geth interpreter. Logs active
backend on first block when a non-default backend is present.
Also fix blockstm.go import cycle by injecting TxApplyFunc instead
of referencing core package symbols directly.
All Lux EVM deployments now have Shanghai and Cancun active at
genesis by default:
- ShanghaiTime=0: PUSH0, warm coinbase
- CancunTime=0: MCOPY, TSTORE, TLOAD, BLOBHASH, BLOBBASEFEE
SetDefaults() no longer clobbers explicit timestamp=0 values.
This means genesis configs with evmTimestamp=0 correctly inherit
Cancun activation without needing explicit shanghaiTime/cancunTime.
Also fixed ApplyPrecompileActivations to write precompile state at
genesis block (previously guarded by block > 0 check).
- admin_api.go: add afterCommit callback to importBlocksFromFile so
acceptedBlockDB is updated atomically with each state commit,
eliminating the crash window between state persist and DB update
- admin_api.go: resume import from current head (skip already-imported
blocks) for restart-safety after partial imports
- vm.go: simplify readLastAccepted to return genesis on ErrNotFound
without recovery hacks
- consensus.go: simplify Cancun/beacon validation — Lux networks do
not use Ethereum beacon chain; only reject non-zero BlobGasUsed
- blockchain.go: chunked batch deletes in reorg to avoid BadgerDB
ErrTxnTooBig on large chains
- blockchain.go: tolerate "not supported" from snapshot flatten
(pathdb backend) instead of log.Crit crash
- genesis.go: fix duplicate comment
Pathdb only maintains recent state, so the genesis state root gets pruned
on chains that have progressed past block 0. Previously this triggered a
recommit that would panic because pathdb's layer tree lacks the
EmptyRootHash parent layer. Now we check for block 1 existence to
distinguish fresh chains (recommit needed) from established chains
(pruning expected and safe to skip).
When enable-automining is set in config (dev mode), skip the empty block
validation to allow anvil-like behavior with instant block production
even when there are no pending transactions.
This fixes the infinite loop error when running luxd --dev:
"syntactic block verification failed: empty block"
- Update geth to v1.16.75 (ConsensusContext is now context.Context)
- Update precompile to v0.4.6 (graphvm types defined locally)
- Remove unused runtime imports from core/evm.go and core/precompile_overrider.go
- Simplify GetConsensusContext to directly return context from PrecompileEnvironment
Updates dependencies:
- github.com/luxfi/vm v1.0.22 → v1.0.23 (adds ZAP transport Serve)
- github.com/luxfi/log v1.3.0 → v1.4.1 (fixes module path declaration)
- github.com/luxfi/consensus v1.22.53 → v1.22.56
- github.com/luxfi/crypto v1.17.39 → v1.17.40
- github.com/luxfi/database v1.17.38 → v1.17.39
- github.com/luxfi/geth v1.16.69 → v1.16.73
- github.com/luxfi/p2p v1.18.7 → v1.18.8
- github.com/luxfi/warp v1.18.4 → v1.18.5
The vm package now provides a ZAP-based Serve function by default,
eliminating the need for the grpc build tag for VM plugins.
Also includes various improvements to validators, warp service,
and precompile interfaces.
- Enable FHE precompile from standalone precompiles package
- Rename luxRulesHook to precompileHook for clarity
- DEX precompiles to be imported from standalone precompiles package
- Clean up debug logging from vm.go
The FHE precompile uses pure Go TFHE implementation from
github.com/luxfi/fhe for on-chain homomorphic encryption.
- Set blockContext.ConsensusContext from chain context for warp precompiles
- Remove WARP DEBUG fmt.Println statements from precompile_overrider.go
- Fix GetConsensusContext to use env.ConsensusContext() instead of context.Background()
- Rename plugin/evm/upgrade/subnetevm package to feewindow
- Rename SubnetEVMTimestamp to EVMTimestamp (JSON: evmTimestamp)
- Rename IsSubnetEVM method to IsEVM
- Rename SubnetEVMDefaultChainConfig to EVMDefaultChainConfig
- Rename TestSubnetEVMChainConfig to TestEVMChainConfig
- Rename TestPreSubnetEVMChainConfig to TestPreEVMChainConfig
- Rename ethclient/subnetevmclient to ethclient/evmclient
- Update VM factory ID from "subnetevm" to "evm"
- Remove all backward compatibility aliases
- Update all documentation and comments
Breaking change: This removes all SubnetEVM naming - only "EVM" is supported.
No backward compatibility maintained as per user requirement.
- Update ethapi test fixtures for new block hash format
- Fix TestNotify to include Lux-specific header fields
- Fix sync/handlers tests for proper block decoding
- Add genesis hash and RLP import tests