rpcChainVM protocol unchanged (42) — node compatibility preserved. Pairs with
the genesis-injection fix (0bd9927a2) for evm module tag v1.104.1.
Co-authored-by: Hanzo Dev <[email protected]>
Forward-integration onto main (v1.103.0 line) of the accept-empty recovery fix
released as v1.101.4. errEmptyBlock moves from SyntacticVerify (parse/verify/
accept) to the build path only, so a consensus-finalized empty block Accepts
while a demand-driven proposer still never produces one.
Co-authored-by: Hanzo Dev <[email protected]>
ROOT CAUSE of the C-Chain bootstrap ancestry-descent stall: ParseBlock was STATEFUL.
BlockGasCost is NOT in the RLP block — it is DERIVED from the parent header. UnmarshalBlock
computed it via vm.blockChain.GetHeaderByHash(parent), and SyntacticVerify REQUIRED it non-nil
(errNilBlockGasCostEVM). For a bootstrap descent parsing ancestry blocks AHEAD of the accepted
height the parent is not present, so every such block failed to parse — masked by the ZAP
errorToZAP mapping as the misleading "malformed block id (does not connect to accepted chain)".
The Lux bootstrapper indexes the whole fetched batch by (id,height,parent) up front, so it could
not index the ancestry, never found the frontier tip in the batch, and re-requested forever ->
ErrStalled. Confirmed on devnet: "syntactic block verification failed: nil blockGasCost is
invalid after EVM activation".
FIX — decomplect parse from verify (matches upstream avalanchego coreth: parse decodes, Verify
enforces connectivity):
- block.go: new ensureBlockGasCost() fills BlockGasCost from the parent header WHEN PRESENT
(SetHeaderExtra is hash-keyed, so it persists for a block first parsed while its parent was
absent). Best-effort at parse; authoritative at Verify (AcceptBootstrapBlock calls blk.Verify
only after the contiguity gate height==finalized+1, so the parent is ACCEPTED/present).
- block_verification.go: SyntacticVerify no longer requires BlockGasCost (stateless,
block-intrinsic checks only); keeps the uint64 sanity check when it IS set.
- vm.go UnmarshalBlock: replace the inline parent-lookup with block.ensureBlockGasCost().
SAFETY (parse != accept): parse only decodes + stateless syntactic checks; no non-connecting or
gas-cost-invalid block can be ACCEPTED just because it now parses. Verify/Accept
(InsertBlockManual -> consensus/dummy header verification) recomputes the expected BlockGasCost
from the (present) parent and rejects a mismatch; the bootstrap contiguity gate accepts blocks
oldest-first (parent finalized). A wrong-hash block is rejected off-path by the content-addressed
descent. BlockGasCost is not in the block hash -> no malleability.
evm plugin/evm tests pass.
Co-authored-by: Hanzo Dev <[email protected]>
Pre-existing idle-builder wake race (not caused by QCv2): on a demand-driven
C-Chain with no empty blocks, a tx arriving while no builder is parked in
waitForNeedToBuild broadcasts pendingSignal to no waiter and is lost; the tx is
then only caught on the NEXT needToBuild() poll, which — depending on the
proposervm slot loop and engine re-call cadence — could leave the first tx after
idle pending for a long/indefinite time (the mainnet treasury-sweep-stuck and
v1.32.1 devnet DEXFill-didn't-complete symptoms).
Fix: startPendingTxPoll — a node-lifetime ticker (builderPollInterval = 500ms)
that periodically broadcasts pendingSignal so any parked builder re-evaluates
needToBuild() against the live mempool. It is decomplected from the tx-submit
subscription (awaitSubmittedTxs remains the primary, immediate wake) so the two
wake sources are independent. It NEVER forces an empty block — a wake only
produces a build when needToBuild() is true — so demand-driven chains stay
empty-block-free, and it changes wake TIMING only, never block CONTENTS, so
validators still build byte-identical blocks (deterministic). needToBuild now
routes through an injectable pendingTxs predicate so the wake path is testable
without a live txpool.
Regression: TestBuilderWakesOnPendingTxAfterIdle reproduces the node bridge's
exact waitForEvent call on an idle builder, delivers a pending tx with NO submit
broadcast, and asserts it returns PendingTxs within a bounded deadline. It FAILS
on the pre-fix code (times out — the lost wakeup) and passes after.
TestBuilderPollBackstopStopsOnShutdown asserts the ticker exits cleanly. Builder
tests green under -race.
Co-authored-by: Hanzo Dev <[email protected]>
Decomplect the build-pacing decision out of waitForEvent into a pure
nextBuildTime(earliestBuild, lastBuildTime) function (no clock read), so the
logic the network-halt fix depends on is deterministically testable. The
wall-clock comparison stays in waitForEvent.
TestNextBuildTime covers all 5 cases (first-build paces to target rate; past
earliestBuild builds immediately; target rate dominates retry floor; retry
floor prevents hot-loop; boundary tie-break). TestNextBuildTime_NeverEarlier
ThanTargetRate locks the invariant the fix relies on: the next build is NEVER
scheduled before parent.Time+TargetBlockRate, so a too-early high-fee block can
never re-introduce the small-tip stall.
Second fresh-net block-production bug, exposed once WaitForEvent was unblocked.
The hand-rolled node notification bridge calls WaitForEvent in a tight loop and
builds a block the instant any tx is pending — with no pacing (ava's proposervm,
which Lux does not wrap, normally provides it). Two consequences:
1. Block gas cost: cost = parentCost + step·(TargetBlockRate − elapsed). A block
built immediately after its parent (elapsed≈0) requires step·TargetBlockRate
(e.g. 200000·2 = 400000) of tips to pass verifyBlockFee. A large deploy
covers it; a single small transfer (~100000) cannot → BuildBlock fails →
the tx never mines.
2. Equivocation: with all 5 validators building instantly off slightly different
mempool snapshots, they produce different blocks at the same height; two can
each gather an α-of-K cert → conflicting finality certs → Logger.Crit → the
node exits(1) and crash-loops.
Fix: pace waitForEvent to parent.Time + TargetBlockRate (the point at which the
block gas cost decays back to the parent's level, 0 from genesis), mirroring the
timing ava's coreth builder applies. Small-tip txs now mine, and the ~rate-long
window lets the mempool gossip-converge so validators build the IDENTICAL block
(same ID, one cert) instead of racing to finalize conflicting blocks. Retry
floor 1ms→100ms so a failed BuildBlock cannot hot-loop.
Fresh-start block-production stall root cause. The node-side notification
bridge (node/chains/manager.go) calls VM.WaitForEvent in a loop with a
non-cancellable context.Background(), starting BEFORE SetState(NormalOp)
initializes vm.builder. The first WaitForEvent therefore parks in the
builder==nil select { ctx.Done | stateSyncDone | shutdownChan } and, since the
context never cancels and state-sync is off, blocks FOREVER -- the serial
bridge loop never re-calls WaitForEvent to observe the now-ready builder, so no
PendingTxs ever reaches the engine and no block is ever built.
ava avoids this with common.NotificationForwarder + cn.OnChange=CheckForEvent,
which cancels the outstanding WaitForEvent and re-subscribes on VM state change.
Lux replaced that with the hand-rolled bridge and lost the recovery path.
Fix (self-contained, VM-side): add builderReady chan, closed once by
onNormalOperationsStarted after vm.builder is set, and add a select case so a
WaitForEvent that parked early re-fetches the ready builder and proceeds.
Makes WaitForEvent correct for any caller, cancellable or not.
go.sum: re-record luxfi/age,pq checksums after upstream re-tag (verify ON).
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.
Rename across the evm warp backend and the warp precompile: warp.Core ->
warp.Message, NewCore -> NewMessage, ParseCore -> ParseMessage, the .Core field
-> .Message, and local core/msgCore/testCore vars -> msg/testMsg. deps: warp
v1.21.0 -> v1.22.0, precompile v0.11.0 -> v0.12.0.
geth's core/types and core/vm imports are untouched — the rename is scoped to
the warp message type, never the geth core package. ./warp/... and
./precompile/contracts/warp/... green, including TestWarpWrongNetworkID.
Track the upstream rename across the evm warp backend
(warp/{service,backend,verifier_backend}) and the warp precompile
(precompile/contracts/warp/{config,contract,contract_warp_handler},
precompileconfig): SignedCore->Core, WarpEnvelope->Envelope.
Identifiers only — the digest D, ZAP c14n bytes and BLS Beam / Corona Pulse /
ML-DSA signing domains are unchanged. ./warp/... and
./precompile/contracts/warp/... green, including the 10k-validator predicate
sign->aggregate->verify.
Hard-fork the last evm warp consumers onto the canonical ZAP message format
(one and one way only). warp v1.20.0 deleted UnsignedMessage; the signed
subject is now warp.SignedCore (D = keccak256(coreDST ‖ zap_c14n)) and the
signed wrapper is warp.WarpEnvelope. The BLS Beam signs warp.BeamSigningBytes(D).
deps: warp v1.19.5 -> v1.20.0, precompile v0.5.59 -> v0.10.0.
- warp/{backend,verifier_backend,service}: UnsignedMessage -> SignedCore;
NewMessage -> NewWarpEnvelope; sign/verify over BeamSigningBytes(core.ID()).
- precompile/contracts/warp: ParseMessage -> ParseWarpEnvelope; .UnsignedMessage
-> .Core; .Signature.(*BitSetSignature) -> .Beam (concrete). VerifyPredicate
verifies the Beam over D with exported primitives (Bits + bls +
BeamSigningBytes) because the precompile quorum denominator must include
keyless validators, which warp.VerifyEnvelope/GetCanonicalValidatorSet reject.
- precompileconfig.WarpMessageWriter.AddMessage(*SignedCore) (mirrors precompile v0.10.0).
- plugin/evm/vm.go: warpVerifierAdapter over *SignedCore; collapse the duplicate
external-warp import alias (warptypes -> luxwarp).
- remove redundant warp/{aggregator,bls_adapter,signer_adapter}.go: provably-dead
duplicates of warp.SignatureAggregator / warp.NewSigner (zero production refs).
- tests migrated to ZAP; fixed a pre-existing offchain message-key test bug;
revived the predicate suite (validator state injected via Runtime, the
production path, replacing the decoupled WithValidatorState key).
Verified: go build ./... green; go test ./warp/... ./precompile/contracts/warp/...
green (incl. 10k-validator TestWarpPredicate). grep UnsignedMessage = 0.
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).
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.
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 host side of the 0x9999 native C<->D atomic seam, and the consensus-liveness
fix Red found.
THE BUG: Block.Accept applied the staged atomic ops to shared memory FIRST
(sm.Apply, no batch -> committed immediately) and advanced the node-local seq
marker LATER, in a separate versiondb.Commit(). atomic.SharedMemory.Apply is NOT
idempotent (duplicate Put errors). A crash BETWEEN them left shared memory
mutated but the marker unadvanced; on restart the same parent->current window
re-collected, sm.Apply re-applied the present Put, errored, and Accept returned
fatal -> the chain HALTED.
THE FIX (mirrors chains/dexvm commitAtomic): flushDexAtomic -> stageDexAtomic
stages the advanced dexAtomicSeqKey marker INTO the versiondb in-memory layer and
returns (reqs, sm). Block.Accept snapshots the versiondb (marker + lastAccepted)
via CommitBatch() and passes it to sm.Apply(reqs, batch) — its WriteAll commits
the shared-memory mutation AND the batch in ONE database write. The marker advance
and the SM mutation are now all-or-nothing: a crash applies-and-marks both or
neither, and a re-accept of an applied window sees an advanced marker -> empty
window -> no duplicate Put -> no halt. With no atomic ops the plain
versiondb.Commit() path is unchanged.
Also: core/precompile_overrider.go wires CallIndex() through the EVM accessible-
state adapter; precompile/registry/bridge.go + precompile/contract/atomic.go
forward the optional contract.AtomicState capability to luxfi/precompile.
Regression for the closed halt window lives in precompile
native_haltwindow_test.go (unit). The live-path test (real versiondb + crash
injection) is CI-only (CGO + luxfi/accel LUXCPP). Local symbol-resolution proof:
CGO_ENABLED=0 go build ./plugin/evm/ compiles clean.
The C-Chain DEX settles certified D-Chain fill receipts at the 0x9999 precompile
(LP-9999); it never queries a live matcher. Remove the deprecated
DexZapEndpoint -> SetBackend(NewZAPEngine()) wiring from vm.go and the
DexZapEndpoint config knob: a synchronous in-block ZAP query against the
D-Chain's moving book forks consensus (each validator observes differently-timed
fills -> divergent StateRoot, proven by chains/dexvm
TestRED_PerValidatorRelay_SplitsConsensus). ZAP survives only as the dexvm's own
build-path transport, never as a C-Chain live backend. Receipts ride in V4 swap
hookData and are verified inline (deterministic BLS).
Bump github.com/luxfi/precompile v0.5.47 -> v0.5.49 to embed the complete,
red-swarm-hardened 0x9999 receipt-settlement surface.
Task 2 (Red H2): StrictPQTimestamp was wired from config.PQ (vm.go) but the
gate was unproven inert. The canonical lux-mainnet config already sets
pq:true and upgrade.json sets strictPQTimestamp=Quasar-TS; added behavioral
tests proving NetworkUpgrades.IsStrictPQ activates (genesis when pq:true ->
StrictPQTimestamp=0; at the timestamp on running chains; Override carries it).
GUARDRAIL fix: vm.Initialize installed gethvm.AllForbidden() (=pq.Strict),
which forbids the standard bn256 precompiles 0x06-0x08 — breaking
Ethereum-compat dapps. Now installs the EVM-owned LuxStrictPQ() projection
(pq_profile.go): every classical family forbidden EXCEPT bn256. Lux's
security-critical pairing/DLOG is in CUSTOM precompiles (0x0900, 0x22
Pedersen, zkvm verifiers) + the consensus cert, each gated by the SAME
profile bit (config.PQ also pins StrictPQTimestamp=0 -> RefuseUnderStrictPQ).
Tests (b): IsStrictPQ true on a prod strict chain (genesis + timestamp +
Override paths), false when nil. (f): LuxStrictPQ admits bn256 0x06-0x08 and
still refuses every other family; differs from AllForbidden only by bn256.
Previous ExportChain returned (bool, error) and would re-export from
scratch every call, leaving partial files with no marker on Ctrl-C.
Now returns *ExportChainResult with first/last block hashes and a Status
field ("ok" | "noop" | "interrupted"). Behavior:
* Atomic JSON sentinel at <file>.sentinel (tmp+rename) updated every
1024 blocks and on terminal states.
* Idempotency: if file exists with a "done" sentinel matching
{first, last, firstHash, lastHash}, returns Status=noop without
re-exporting. Safe to call from a cron CronJob.
* Interrupt: ctx.Done() at every block boundary writes a sentinel
with Status=interrupted + HighestExported so the caller can resume.
* Block iterator still streams (no whole-chain buffering); gzip path
unchanged.
Symmetric with admin_importChain which returns *ImportChainResult, and
with the cmd/rlp-export CLI shipping alongside.
Server-side test coverage: writeSentinel atomicity, readSentinel
absent + corrupt fall-through, JSON shape pinning for both the
sentinel and ExportChainResult, exportProgressInterval stability.
Quasar Edition rip: there is one and only one upgrade-key namespace.
The strict decoder forces parse-time failure on any unknown field —
which catches typos and stale aliases (e.g. etnaTimestamp) at config
roll instead of silently muting forks at activation.
Why this matters: encoding/json by default ignores unknown JSON keys
and zero-initializes the corresponding struct fields. After the
EtnaTimestamp → QuasarTimestamp rename, any deployed upgrade.json
still carrying etnaTimestamp would parse cleanly with the new field
left nil, and the fork would never activate. The strict decoder
turns that silent-disable failure mode into a loud boot-time error.
Pairs with: luxfi/genesis kill-etna sweep + node v1.28.22 plugin bump.
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.
luxfi/genesis is now indirect (no direct EVM imports), but tidy pulled in
fresher minimum versions of its transitive deps:
- constants v1.5.2 -> v1.5.7
- crypto v1.19.6 -> v1.19.15
- metric v1.5.1 -> v1.5.5
- precompile v0.5.23 -> v0.5.27
- otel v1.42.0 -> v1.43.0
Plus drops StephenButtolph/canoto, fatih/structtag, grpc-prometheus from
indirect block (no longer transitively required).
No functional changes; this is purely the dependency cleanup that follows
the luxfi/genesis v1.13.8 bump.
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).
The original tests called precompile.Run() directly and expected the
PQ refusal to happen inside the precompile body. That contract no
longer holds: after CR-1, every classical precompile's Run() body is
strict-PQ-agnostic — the gate lives in (*EVM).runPrecompile and reads
the per-chain Profile from ChainConfig.PQ.
This rewrite tests the two contracts the plugin layer is actually
responsible for:
1. AllForbidden() == pq.Strict() (Hash() identity check) — every
classical primitive family flagged.
2. (*Profile).RefuseUnder(op) returns the family-specific sentinel
error for every op a strict-PQ chain must refuse.
3. nil profile (the default for non-PQ chains) admits every op.
Cross-precompile integration coverage (the strict-PQ chain runs an
EVM that actually refuses ecrecover at the boundary) lives in
geth/core/vm/pq_gate_test.go — closer to the dispatch chokepoint and
not duplicated here.
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.
Config field LuxStrictPQ -> PQ (JSON key "pq", was "lux-strict-pq").
VM.Initialize now calls gethvm.SetPQProfile(gethvm.AllForbidden()) when
config.PQ is true, refusing every classical precompile family
(ecrecover, sha256/ripemd/blake2F, alt_bn128, BLS12-381, KZG) at the
EVM gate — not just ecrecover.
Test file renamed lux_strict_pq_test.go -> pq_test.go. Covers the
strict-PQ path, classical-compat path, and a cross-family refusal
matrix asserting AllForbidden() turns on every Forbid* flag.
VM.Initialize installs the chain-wide strict-PQ posture into the
go-ethereum precompile layer when Config.LuxStrictPQ is true. The
ecrecover precompile at 0x01 then returns ErrClassicalAuthForbidden
(gas is still charged per EIP-150) and never produces a recovered
address.
Default (LuxStrictPQ=false) preserves classical-compat semantics
required by legacy chains and the Lux-Permissive profile.
Mirrors the coreth wire-up; the gate itself lives in luxfi/geth
(commit 738ecc775, tag v1.16.88) and is single-implementation. Bumps
luxfi/geth pre-release v1.16.86-0.20260413… → v1.16.88 to pull in
the gate; transitive deps (luxfi/accel v1.0.7→v1.0.8, luxfi/consensus
v1.22.70→v1.22.84, luxfi/precompile v0.5.12→v0.5.14) update as
required by geth v1.16.88's own go.mod.
Tests:
TestEcrecoverStrictPQ_Forbidden — strict-PQ returns the error
TestEcrecoverClassicalCompat_Works — nil profile keeps upstream path
Adds three RPC methods under the "lux" namespace:
- lux_evmBackend: returns current active backend (gevm/revm/cevm)
- lux_evmBackends: returns all available backends + active
- lux_setEvmBackend: switches backend at runtime (admin-only)
- block_builder.go: drain buffered tx events, build blocks until pool empty
- vm.go: force CancunTime=0 if nil during genesis init
- go.mod: replace github.com/luxfi/geth with local patched fork (65k MaxCodeSize)
- sync/client: hardcode 65536 code size limit
Note: Cancun opcodes (MCOPY, TSTORE) still not activating at runtime.
The genesis parsing path may not reach the force-enable code.
Needs investigation of the ZAP plugin's genesis flow.
- 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
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"
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.
The needToBuild() function was using a MinTip filter that rejected
all pending legacy transactions without GasTipCap set, causing block
production to stall at height 0.
Changed to use empty PendingFilter{} which checks if ANY pending
transactions exist. The miner applies proper fee filters when
building blocks anyway.