A node killed while admin_importChain was accepting a round could not start
its C-Chain again: "required historical state unavailable (reexec=2)". The
import wrote the acceptor tip for the whole round first and then committed the
round's tries one block at a time, so a kill in between left a tip on disk
above the accepted pointer, naming blocks whose state was never written.
reprocessState re-executes from the acceptor tip, which it takes to be at or
below the block it recovers, and searches back from there only 2x the commit
interval for a state to start from.
The import now accepts each block the way the acceptor does, through the one
acceptDurably both call: tip, then trie, a block at a time. And reprocessState
moves an acceptor tip that is above the accepted block back to it, which is
what recovers a database an earlier release left that way.
TestImportArchive_KilledAnywhereRestarts kills an import at every write it
makes, reopens from the accepted pointer the VM would read, resumes, and
restarts once more. On the previous code, at commit interval 1, the kill after
write 11 of 48 does not start again.
Refs #152
Co-authored-by: Hanzo Dev <[email protected]>
admin_importChain and --import-chain-data validate blocks with InsertChain
and then move the accepted tip with SetLastAcceptedBlockDirect. Neither step
does what the acceptor does for a block consensus accepts: index its
transactions and hand its trie to the state manager. An archive node therefore
committed only the roots the import force-committed at round ends and indexed
only each round's last block. After a restart every other height's state was a
missing trie node, eth_getTransactionByHash found one transaction per round,
and with commit-interval 1 the startup head repair could not re-execute the tip
from the height before it, so the C-Chain did not start.
AcceptImported does the acceptor's durable work for each inserted block, in
order, through acceptState, which the acceptor now calls too.
TestImportArchive_HistorySurvivesRestart imports in rounds shorter than the
chain, reopens it on the same database, and checks every height's state and
every transaction's lookup. On the previous code it fails at height 1, and with
commit-interval 1 the chain does not reopen.
A database an earlier release imported keeps the gaps; only a fresh import
under this one fills them.
Fixes#152
Co-authored-by: Hanzo Dev <[email protected]>
Build and vet are clean on the go1.27.1 toolchain. Five packages carry
pre-existing test failures -- core, eth/gasprice, internal/ethapi and the
two params/extras suites -- and they fail identically with this change
reverted, including the same invalid chainID 0 panic.
Co-authored-by: Hanzo Dev <[email protected]>
typescript 7.0.2 is the native compiler and exports no JavaScript API, so hardhat,
ts-node and next cannot read it. Both projects take it aliased as tsgo for the
typechecking and keep typescript on the 6 line for the tools; next 16.0.1 in
particular does not recognize a bare 7 pin and runs an unsolicited
`pnpm add -D typescript` mid-build.
contracts/ reports 0 diagnostics under both compilers with strict spelled out —
TS7 turns it on by default and this config never opted in. docs/ reports the same 5
under both, once the side-effect CSS import gets the ambient declaration TS7 now
demands. Neither project had a typecheck script.
Co-authored-by: Hanzo Dev <[email protected]>
One label per runner, spelled <os>-<arch>. The aliases these jobs named
were removed, and a job naming a label no runner carries waits instead of
failing.
Co-authored-by: Hanzo Dev <[email protected]>
One label per runner, spelled <os>-<arch>. The aliases these jobs named
were removed, and a job naming a label no runner carries waits instead of
failing.
Co-authored-by: Hanzo Dev <[email protected]>
precompile v0.19.22 renames the eleven modules that keyed off Verify, Threshold or
Encapsulate, so the fifty-six this registry links now answer to one convention.
Co-authored-by: Hanzo Dev <[email protected]>
This file named 41 suite packages one by one so their init() would run, because
luxd rejects an upgrade.json activation for a configKey it has never seen with
"unknown precompile config". That made it a second record of what the suite
contains, and it had drifted: aivmbridge, swap and v3 registered in the suite and
were absent here, so no Lux chain could activate them however its genesis was
written.
Importing the suite's registry instead cannot drift — that package derives its set
from the packages that actually register rather than from a list. One record, kept
in the repository that owns the thing recorded.
The list was also pinned to suite v0.19.8 while the suite had moved to v0.19.21,
so even the import alone left 14 of 56 modules linked. With the bump the surface
a luxd built from this module dispatches is 56, which is exactly what the suite
holds — measurable rather than asserted: cmd/surface prints it, and it is the
list an upgrade.json is allowed to name.
Co-authored-by: Hanzo Dev <[email protected]>
luxfi/genesis 336c946 removed warpConfig from the mainnet upgrade schedule
and moved requirePrimaryNetworkSigners into the block-zero declaration,
because the EVM refuses to initialise when a live precompile is re-declared
with a different rule — a second declaration is admitted only to disable one.
These tests still asserted the arrangement that commit abandoned, so both
failed against a correct repository: one looked for a warpConfig entry that
is deliberately gone, the other compared a fixture still carrying it.
Red-review finding #8 is unchanged and now checked at its source: warp is
read from configs/mainnet/cchain.json and must require primary-network
signers, and the upgrade schedule must re-declare it nowhere. The mainnet
and testnet fixtures are re-vendored (48 entries each) and the hardcoded
canonical count follows.
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]>
Three things, all in the import path.
The tree did not build. plugin/evm/vm_database.go names AncientShared and
FreezeThreshold on rawdb.OpenOptions; luxfi/geth v1.20.2 has neither and v1.20.3
has both, so a clean checkout failed to compile before any change. Pinned to
v1.20.3.
The blocks-per-round was the literal 2500 inside importBlocksFromFile, so the
only way to change it was to edit and rebuild. It is a parameter now:
admin_importChain takes an optional count, --import-chain-batch sets it for the
startup path, and defaultImportBatch stands in when neither says. The right
number is a property of the machine -- a host with memory to spare amortises one
InsertChain call over far more blocks -- so it does not belong in the source.
Both importers wrote their own copy of the same closure to advance the accepted
marker and commit. There is one persistAcceptedBlock now and both hand it to the
importer. What genuinely differs between them -- the startup path reconciles the
export frontier and syncs consensus state afterwards, because it runs before the
engine is live -- stays where it belongs.
Co-authored-by: Hanzo Dev <[email protected]>
github.com/luxfi/utils is now github.com/luxfi/util. Import paths and the
module requirement follow the rename, and the luxUtils alias becomes luxUtil.
The old module stays as an indirect requirement that published dependencies
still pull in.
Co-authored-by: Hanzo Dev <[email protected]>
constants.BlackholeAddr is a plain [20]byte, so constants no longer imports
geth. The fourteen sites here all want a geth common.Address, and each file
already imports geth/common, so each takes the free conversion itself.
Only one site was a compile error: the .Bytes() method call in
rewardmanager.DisableFeeRewards. Everywhere else [20]byte is assignable to
common.Address, so the change was invisible to the compiler, and the two
require.Equal assertions in rewardmanager would have compiled and then failed
at run time, because reflect.DeepEqual compares types before bytes. Those two
are wrapped for that reason, not for style.
The two failures in core, TestHeadPin_Across4096Boundary_NoLeak and the
invalid-chainID-0 panic in TestCreateThenDeletePostByzantium, reproduce
identically with these lines reverted and are unrelated.
Co-authored-by: Hanzo Dev <[email protected]>
One family. Font stacks name Zen and fall back to the native faces, tokens
name the role rather than a face, and the packages that used to supply a
face now point at @hanzo/font.
Self-hosted @font-face blocks are removed rather than renamed: rewriting the
family in one would leave a src pointing at a file this repo no longer ships.
@hanzo/font carries the binaries and declares the faces, so the local copies
go with the declaration that used them.
Comments naming a face we no longer use are deleted, not reworded.
luxd serves chain routes under /v1/chain, and the node, cli and sdk releases
already ship it. The deployed gateways still answer the old prefix, so remote
reads break until they are redeployed; the estate moves together rather than
sitting half converted.
Lines that record an observation - a probe, a status code, a past release -
keep the prefix they were written against, as do URLs on hosts that are not
ours. Avalanche endpoints move to /ext/bc, which is the path that host serves.
Co-authored-by: Hanzo Dev <[email protected]>
The rename to /v1/chain is real in luxd source, but the deployed fleet has not
taken it yet: probed just now, api.lux.network, api.lux-test.network,
api.zoo.network and api.hanzo.network each answer 200 on /v1/bc/C/rpc and 404 on
/v1/chain/C/rpc. Composing /v1/chain against those hosts reaches nothing, which
is the failure the rename set out to prevent.
The move belongs with the node redeploy, not ahead of it.
Co-authored-by: Hanzo Dev <[email protected]>
luxd serves chain routes under /v1/chain; the /v1/bc prefix it replaced is
gone, so anything composing it reaches nothing. Lines that record an
observation - a probe, a status code, a past release - keep the prefix they
were written against, as do URLs on hosts that are not ours.
Co-authored-by: Hanzo Dev <[email protected]>
luxd serves chain APIs under one segment from constants.ChainAliasPrefix
(constants v1.6.4, node v1.36.181):
/v1/bc/<alias>/... -> /v1/chain/<alias>/...
/ext/bc/<alias>/... -> /v1/chain/<alias>/...
Observations keep their original addresses: a dated note or a recorded status
code is a fact about when it was written, not an instruction to follow.
A NOTICE was added where the repository forks or republishes upstream work
and carried no repository-level credit for it. MIT requires the copyright
notice to travel with every copy; GPL-3.0 §5(a) requires a modified work to
say that it is modified.
Where LICENSE changed: the FSF's copyright line on the GNU licence text had
been replaced with ours, immediately above the sentence "Everyone is
permitted to copy and distribute verbatim copies of this license document,
but changing it is not allowed." The sibling COPYING.LESSER in the same
repository still carried the correct line, which is how it was found.
This fleet does not publish to GitHub Pages, and the plan does not offer it
for a private repository — creating a Pages site answers 422 — so the job
could not have succeeded on any push it ran for.
Every job in this workflow was the Pages deploy, so the file goes with it.
Sites publish from their hanzo.yml declaration onto the static plane.
This is a tracked file in a public repository. It carried a deployment analysis
naming a cluster, namespace and statefulset, a recovery narrative, transaction
hashes and balances from a running network, an address described as one the
author controls, and a run of dated Problem/Status-FIXED postmortems. That is
operational record, and it belongs to the private one.
The RewardManager section keeps the mechanism, the config that activates it, the
in-process proof, and the per-chain DAO addresses a reader needs to configure it;
what it exercised on a running network is now stated as what was verified rather
than as where and on which pods. The durable facts inside the postmortems are
kept as properties: no beacon chain, so the Cancun blob fields are not part of a
Lux EVM block; a genesis file has to reproduce the chain's block 0; importing
history moves the database under anything reading it.
Also drops "subnet" from two lines describing our own chains.
Co-authored-by: Hanzo Dev <[email protected]>
A receipt root that disagrees stops the node where it stands — that is the
property the placement of this rule exists to prevent, and it holds whether or
not it has ever been reached.
Comment only.
Co-authored-by: Hanzo Dev <[email protected]>
The comment explained the placement by recounting what a node hit when the rule
was enforced during execution. The reason it belongs here does not depend on
that having happened: execution has to be a function of the block alone, and
reading an object out of shared memory while the matching Remove lands at accept
makes it a function of when the block is executed instead.
Behaviour unchanged; comment only.
Co-authored-by: Hanzo Dev <[email protected]>
The ancient store existed in geth and its flags existed on the node, and nothing
joined them: the VM always opened its chain database straight over the key-value
store, so --cchain-ancient reached the C-Chain config and stopped there.
openChainDatabase is that join. Without an ancient directory the key-value store
is the database, as before, and every block the node ever accepted stays in it.
With one, blocks past the freeze threshold live in an append-only store on that
path: the node that owns the directory writes them there and drops its own
copies, and nodes that share the directory read history from it instead of each
keeping a copy. On a machine running many nodes that is the difference between
one copy of the chain and one per node.
The config test was written against an earlier shape, where ancient-read-only
and ancient-shared each required the other — a pair that named one arrangement
twice and was removed. It now pins what is actually enforced: sharing needs a
directory to share, and a store needs a hot window for the block being built to
find its parent. A comment about two writers interleaving one file had been left
sitting above the threshold check, which is not what it describes.
Co-authored-by: Hanzo Dev <[email protected]>
The two settings each required the other, so neither could be given alone and
the pair named one arrangement twice. Reading a store another node writes
already means not writing to it. Nothing branched on ancient-read-only.
Co-authored-by: Hanzo Dev <[email protected]>
parseGenesis names each genesis config key it copies into the extras
config. feeSplitTimestamp was not on that list, so a configured split was
silently dropped and every fee kept burning whole. Name it beside the
other fork timestamps. verifyFeeSplit already refuses a split with no
governed destination, so a misconfigured node fails at parse rather than
at a validator — TestParseGenesis{ReadsFeeSplitTimestamp,RefusesSplit
WithoutRewardManager} pin both halves.
Point the fee-governance test's DAO address at the deployed CREATE2 DAO
Safe 0xF66B025b so the reward destination has one source of truth.
Co-authored-by: Hanzo Dev <[email protected]>
admin_importChain rewrites the chain from a file on disk. It defaulted to
enabled, with a comment saying it was on so that importChain would work, so
every node carried the ability to have its history replaced by anything that
could reach its RPC port -- to spare an operator one line of config during a
restore that happens rarely and is supervised when it does.
A node that is restoring sets admin-api-enabled in its chain config for as long
as the restore takes. Nothing else needs it, and nothing else should offer it.
The tests set the field explicitly and are unaffected.
Co-authored-by: Hanzo Dev <[email protected]>
State sync is skipped when the chain is not far enough ahead of local state,
because bootstrapping would be faster. That trade only exists while
bootstrapping can run at all. It replays blocks against the state beneath them,
so a node missing the state at its own head cannot replay a single block — it
fails to materialize the next one and stops there, whatever the gap is.
On such a node the threshold declines the one mechanism that could repair it,
and small-gap corruption becomes permanent: it cannot execute forward, and it is
not far enough behind to be allowed to sync. The threshold now applies only when
the node holds the state its head is built on. A chain that cannot be read
answers no — the cost of being wrong that way is a sync that was not strictly
needed, against a node that could otherwise never come back.
Co-authored-by: Hanzo Dev <[email protected]>
It was the zero value, so a chain got it only by carrying a per-chain config
file that said so — and most deployments have no chain-config path at all, which
made the one mechanism for rejoining after a deep gap unreachable on whole
fleets. There is no posture where refusing it and re-executing instead is the
right answer, so it is not a decision to hand an operator. The bound is
StateSyncMinBlocks, which lives inside the mechanism: a smaller gap replays and
never reaches this path.
Co-authored-by: Hanzo Dev <[email protected]>
The C-Chain runs as a plugin, so the ZAP server that answers the node lives in
this binary, not in luxd. A node bump alone leaves the old server in place.
v1.3.8 refuses a VM that returns neither a block nor a reason instead of
dereferencing it, and stops reporting an unnamed failure with the code that
means success.
Co-authored-by: Hanzo Dev <[email protected]>
A node was discarding 948 builds every ten minutes on a HEALTHY chain, and had
spent 41 hours doing it on a halted one. The cause was a barrier this fork
dropped.
Upstream holds the builder until the transaction pool has finished resetting onto
the accepted head: `for !needToBuild() || pendingPoolUpdate()`, where
pendingPoolUpdate is chainHeadHash != mempoolHeadHash. We kept the loop and
deleted the second half, along with both hashes and the two setters that maintain
them.
Without it the builder wakes in the window between a block being accepted and the
pool reorganising onto it. The pool still lists what that block just mined, so
needToBuild says there is work; the miner assembles against the new head, finds
nothing left, and returns an empty block; the anti-empty policy discards it.
Nothing changed, so the builder wakes again — immediately, and forever, until the
pool happens to reset.
Restored rather than reinvented: the same two hashes, the same comparison, the
same loop condition, fed by the pool's own reorg event on one side and Accept on
the other. Both are initialised to the current head so a fresh or restarting node
starts converged instead of gated shut.
This also removes proposeEmptyBuild, added earlier today. It built whenever the
pool was non-empty, which is exactly the spin state — it would have turned 948
discarded builds into 948 empty blocks on chain. The barrier removes the
condition it was guarding against, so the guard goes.
Co-authored-by: Hanzo Dev <[email protected]>
An assembled block with no transactions is two conditions wearing one shape. No
demand is the demand-driven property working — discard it, or an idle chain grows
forever. Demand the assembler failed to collect is a bug, and discarding THAT
makes it permanent: no block means no nonce advances, so the pending transaction
is never mined, so the pool never drains, so the condition under which a block
would be built can never return.
The gate read the assembled block, which cannot tell the two apart, so it took
the second for the first. Measured on lux-devnet: the chain stopped at 20:38 UTC
on 2026-08-11 holding ONE executable transaction, sat 41 hours, and behind it the
traffic generator logged 7,100 consecutive failures while luxd-0 and luxd-3
logged errEmptyBlock ~2000 times per six hours. Every node was healthy. Nothing
was wrong with consensus — CERTs gossiped the whole time.
It reads the pool now. Nothing pending is idleness and the block is discarded;
anything pending builds, whatever assembly managed to collect, and logs the
mismatch where somebody can see it. Liveness stops depending on the assembly path
being correct.
The rule moves into proposeEmptyBuild so it can be tested without standing up a
VM, which is why it had no test before.
This does not fix the proximate cause, and is not meant to: the transaction was
in exactly one node's pool (luxd-2) and never reached either proposer, so EVM
transaction gossip is the next thing to look at. What changes here is that such a
failure degrades throughput instead of halting the chain.
Co-authored-by: Hanzo Dev <[email protected]>
The comment claimed Block.Accept passes a batch to sm.Apply(reqs, batch) so the
marker and the shared-memory mutation land in one atomic write. It does not, and
deliberately: a batch cannot cross the plugin's process boundary, atomiczap
refuses one, and passing it made every settling block a fatal Accept.
Accept commits the state first and applies second, which orders a crash toward a
skipped op rather than a replayed one. A comment describing the opposite reads
as a bug on the money path and invites reordering the writes back.
Co-authored-by: Hanzo Dev <[email protected]>
The 0x9999 swap intent gains the operation segment (market, side, limit, size)
appended to the unchanged 69-byte value head. Without it the D-Chain could
consume an intent and credit the taker, but had nothing to place an order with,
so no swap ever crossed and no settlement object was ever produced for
ImportSettlement to consume.
No change to this repo's own code: the D->C settlement object it authenticates
at Block.Verify is still the canonical 69 bytes. Only the C->D leg widened, and
only the C->D leg instructs.
Co-authored-by: Hanzo Dev <[email protected]>
Picks up the settle-path change that makes a settled swap replayable: both
C-side consume paths bind to the object bytes the transaction carries and
declare the consumption, instead of reading shared memory mid-execution.
The block-level authentication of that declaration is in this repo
(plugin/evm/dex_atomic_verify.go).
Co-authored-by: Hanzo Dev <[email protected]>
Two defects, both fatal, both on the C<->D atomic settlement path.
1. Accept handed the versiondb batch to sm.Apply unconditionally. The
C-Chain EVM is an out-of-process plugin, so its SharedMemory is the ZAP
client, and that client refuses any batch — a database.Batch cannot
cross a process boundary. Every block staging an atomic op would have
failed Accept, which is fatal. Commit the versiondb first, then Apply
with no batch. Without a shared batch exactly-once is unreachable; the
choice is at-most-once or at-least-once, and replay is unsound here (a
duplicate put is fatal, and after the peer consumes the object it
re-creates it). A skipped op is survivable: C's settlement replay guard
is EVM state, not the Remove, and a skipped intent Put is refunded by
reclaimIntent. The residual single-node cost is stated in the code.
2. The settle path read the object from shared memory DURING EVM
execution while the consuming Remove landed at Accept. A node
re-executing that block — bootstrapping, state-syncing, re-tracing —
found the object gone, computed a reverted receipt where the network
computed a successful one, and died on the receipt root. The seam
settled live and wedged every node that tried to sync past it.
Execution now binds to the object bytes the transaction carries and
declares the consumption in its logs; this adds the block-level rule
that proves the declaration against shared memory at Verify, on both
the consensus path and the producer's own pre-proposal verify, so a
forged object costs a block rather than forking a receipt root. The
check is gated on the bootstrap frontier, which the shared-memory layer
requires of any chain consuming cross-chain state.
Co-authored-by: Hanzo Dev <[email protected]>
Picks up the cross-chain atomic capability across the ZAP plugin boundary.
The EVM runs out of process, so its rebuilt Runtime had SharedMemory nil and
BCLookup absent; every DEX 0x9999 settlement reverted "requires the
cross-chain atomic capability" and the Accept-time flush of staged C->D
exports silently no-opped. vm v1.3.6 dials the node's per-chain atomic
server at Initialize and installs both.
precompile v0.19.5 additionally tags C->D swap intents with the discovery
trait the D-Chain enumerates by, without which a real intent is invisible to
D's import drive even once the transport works.
accel v1.2.4 -> v1.3.1 is forced by MVS through precompile, not chosen:
pinning it back drags precompile to v0.19.3.
Co-authored-by: Hanzo Dev <[email protected]>
Currency bump: golang builder images -> 1.26.5 (latest stable).
Adds ENV GOTOOLCHAIN=auto to each Go builder stage. The official golang
images set GOTOOLCHAIN=local, so a builder image older than the module's
go directive is a hard mid-build failure. The pin stays the hermetic fast
path; auto is the safety net for the next go.mod bump.
No go.mod go directive was changed. Builder images move; directives stay.
Co-authored-by: Hanzo Dev <[email protected]>
GOPRIVATE switches off the module proxy AND checksum verification together.
Covering github.com/luxfi/* meant every luxfi module was fetched straight from
git and trusted without verification — which is how moved tags got captured into
module caches and served from them indefinitely, making the same commit build on
one machine and fail on another.
Nearly all of luxfi is public and on proxy.golang.org, so it belongs under
normal verification. The private org is lux-private. Twelve luxfi repos are
still private AND still 404 on the proxy; where a repo depends on one it is
listed by name rather than by wildcard, so the exemption matches what actually
needs a direct fetch.
Co-authored-by: Hanzo Dev <[email protected]>
GOPRIVATE switches off the module proxy AND checksum verification together.
Covering github.com/luxfi/* meant every luxfi module was fetched straight from
git and trusted without verification — which is how moved tags got captured into
module caches and served from them indefinitely, making the same commit build on
one machine and fail on another.
Nearly all of luxfi is public and on proxy.golang.org, so it belongs under
normal verification. The private org is lux-private.
Twelve luxfi repos are still private AND still 404 on the proxy, so they are
listed individually rather than by wildcard — coverage now matches what actually
needs a direct fetch, and every other luxfi module is verified against
sum.golang.org again:
bridge broker compliance erc20-go fasthttp graph hsm indexer
mpc ordering password pool
The right end state is those repos moving to lux-private; until then the
explicit list keeps the exemption honest instead of blanket.
Co-authored-by: Hanzo Dev <[email protected]>
node v1.36.35 bumped luxfi/vm to v1.3.3 for a chain.State map race. The C-Chain
does not run in the luxd binary — it is a plugin built from THIS module, and
this module still pinned v1.3.1. So the fix shipped in the process that does not
verify blocks and was missing from the one that does. Read straight off the two
binaries in a v1.36.35 pod:
/luxd/build/luxd github.com/luxfi/[email protected]
/luxd/build/plugins/mgj786NP7… github.com/luxfi/[email protected]
It fired in production on lux-testnet luxd-0 within 5 minutes of the roll:
fatal error: concurrent map writes
github.com/luxfi/vm/components/chain.(*BlockWrapper).Verify
luxfi/[email protected]/components/chain/block.go:44
github.com/luxfi/vm/rpc.(*zapVMServer).handleBlockVerify
luxfi/[email protected]/rpc/vm_server_zap.go:580
v1.3.3 is exactly the fix for that line — it takes state.blocksLock around
verifiedBlocks[blkID] = bw, which v1.3.1 wrote unlocked from every concurrent
ZAP RPC handler.
Worth flagging separately: the plugin dies while luxd survives, so the pod stays
Ready with restarts=0 and info.isBootstrapped(C) keeps answering true while
eth_blockNumber on /v1/bc/C/rpc times out. The readiness probe cannot see this
failure class; only a per-node tip probe can. Recovery is a pod delete.
go build ./plugin/... ./core/... clean, go vet clean, plugin/evm tests pass.
Co-authored-by: Hanzo Dev <[email protected]>
The --import-chain-data startup import is re-entered on every boot, because
production pod specs keep the flag set. Once the chain is already at or above
the file's tip, importBlocksFromFile imports 0 blocks and returns
"no blocks imported (parsed=0)"; Initialize treated that as fatal, so the
C-Chain failed to initialise on every restart of an already-imported node —
/v1/bc/C/rpc 404, node contributes no consensus vote.
This was fixed once. Commit 15e75f43 ("startup RLP-import consensus-wedge +
idempotency") landed BOTH halves on hotfix/v1.104.9. Only the consensus-refresh
half was forward-ported to main (it is here as "FIX (v1.104.11)"); the
idempotency half was not, and isNothingToImportError does not exist on main.
Verified: git merge-base --is-ancestor 15e75f43 main => false; the commit is
reachable only from hotfix/v1.104.9 and release/v1.104.9-hotfix.4.
Observed live on lux-testnet 96368 today rolling node v1.36.35, which is built
from evm main: two nodes came back with a dead C-Chain on exactly this error.
The v1.36.24 plugin binary in the same cluster still contains the string
"Startup import: chain already at/above import tip; nothing to import"; the
v1.36.35 binary does not.
Restores isNothingToImportError and the guard, unchanged in meaning from
15e75f43: idempotent ONLY when the chain already holds accepted content
(curHead > 0). On a fresh chain the same messages still mean a genuinely
empty or corrupt file and stay fatal.
startup_import_idempotency_test.go locks the contract, both RLP shapes plus the
fresh-chain safety case. Red without the fix (undefined: isNothingToImportError,
build failed); green with it:
--- PASS: TestStartupImportIdempotency_AlreadyAtTip (0.02s)
ok github.com/luxfi/evm/plugin/evm 1.260s
Co-authored-by: Hanzo Dev <[email protected]>
gpu v1.0.1/v1.1.0 declare `#cgo pkg-config: lux-gpu` while including
<lux/gpu.h>. Those are two different libraries with confusingly similar names:
luxgpu.pc unified GPU acceleration, -lluxgpu, Cflags -I${includedir}
lux-gpu.pc the DEX order matcher, -llux_gpu, Cflags -I${includedir}/lux
Under lux-gpu the include resolves to .../include/lux/lux/gpu.h, so every cgo
build failed with "fatal error: lux/gpu.h file not found" — the arm64 stall
signature documented in CLAUDE.md. libluxgpu.dylib is also what actually
exports _lux_gpu_ecrecover_batch.
gpu v1.1.2 already declares luxgpu; only the pin was stale. Builds clean with
CGO_ENABLED=1.
Co-authored-by: Hanzo Dev <[email protected]>
These hashes were recorded from direct VCS fetches made while GOPRIVATE covered
github.com/luxfi/*, which switches off the module proxy and checksum
verification together. Tags moved afterwards, so what was written down no longer
matches what sum.golang.org holds, and the build refuses to verify.
proxy.golang.org still serves the originally-published bytes and those verify
against the log, so dropping only the disagreeing lines and re-tidying restores
the build with no version change. Lines that agree were left alone — this is not
a regenerated go.sum.
The mismatch followed the machine, not the repository: the same commit built
wherever the cache was clean. Any machine or CI runner that fetched luxfi
modules under the old GOPRIVATE holds the same poisoned entries.
Co-authored-by: Hanzo Dev <[email protected]>
Six curl examples still told a reader to POST to localhost:9650/ext/...
Same defect as the code fixed in 98af1ed7: luxd pins baseURL="/v1" and has
never served /ext, so every one of these returns 404 page not found when
pasted into a shell.
Co-authored-by: Hanzo Dev <[email protected]>
luxd pins baseURL="/v1" (node/server/http/server.go:33) and has never
served /ext, so both exported constructors — plugin/evm/client.NewClient
and warp.NewClient — built a path no node answers. They are the seam
every out-of-tree caller goes through: cli/pkg/utils/staking.go,
sdk/validatormanager/uptime.go and netrunner/api all hand them a bare
node URI, and staking.go's own SplitRPCURI already parsed /v1/bc/<id>/rpc.
Those callers were already on /v1; this package was the lagging outlier,
so fixing it here fixes all of them without touching one.
Also corrects the simulator's default websocket endpoint, the hardhat
default, and the docs — including api.testnet.lux.network, which does not
serve RPC at all. The testnet host is api.lux-test.network (chainId
0x17870). Prefix-only sweeps miss that class of error.
Tests pin the constructed path against an httptest server, with both an
alias and a blockchain ID, so a future prefix change fails loudly instead
of silently 404ing in production.
Co-authored-by: Hanzo Dev <[email protected]>
TypeScript 7 is the native Go compiler and removes `baseUrl` and
`moduleResolution: node|node10`. Both appear here, so `tsc` from TS7
refuses the config outright (TS5102 / TS5108) and cannot typecheck.
`paths` targets resolve relative to `baseUrl` when it is set and relative
to the tsconfig file otherwise. Every `baseUrl` folded here already
pointed at the config's own directory, so dropping it moves nothing and
the targets are left byte-identical. Where a baseUrl pointed elsewhere,
each affected target was rewritten as join(baseUrl, target).
`moduleResolution` was chosen from the declared `module`: commonjs ->
node16, esnext/preserve -> bundler. Configs whose `module` is unset or
exotic were left alone rather than guessed at.
The result is accepted by BOTH toolchains, so nothing has to upgrade
TypeScript in lockstep. Verified on hanzo/chat packages/api: tsc 5.9
779 -> 778 errors (no regression), and tsc 7.0.2 now runs the project
in 2s where it previously refused the config.
Co-authored-by: Hanzo Dev <[email protected]>
A ballot is replayable by construction — the same bytes verify every time —
so hasVoted[proposalId][nodeID] is the only thing between a validator and
counting its weight twice. Keyed by nodeID, never by address.
Co-authored-by: Hanzo Dev <[email protected]>
P-chain stake is not natively readable from the C-chain EVM, and the obvious
fix — a precompile that reads the validator set in Run() — is consensus-fatal.
The BlockContext handed to Run() (precompile/contract/interfaces.go:63) carries
Number, Timestamp and GetPredicateResults, and nothing else; a validator-set
read from there would have to use the node's own current P-chain height
(node vms/platformvm/validators/manager.go:214 getCurrentHeight ->
state.GetLastAccepted), so two honest nodes executing the same block a second
apart would compute different weights and fork the chain.
The one place a P-chain height IS agreed is predicate verification:
Block.VerifyWithContext passes ProposerVMBlockCtx.PChainHeight to
VerifyPredicate (plugin/evm/block.go:283), the pass/fail bitset is written into
the header (miner/worker.go:514) and re-checked on replay
(plugin/evm/block.go:366). A ballot verified there is a consensus-committed
fact, and historical sets stay reconstructible from P-chain weight diffs.
So the ballot CLAIMS a weight and the predicate refuses any overstatement:
weight <= weightAt(ballot.pChainHeight) - no flash-stake
weight <= weightAt(block.pChainHeight) - no exited or shrunken validator
totalWeight == totalAt(snapshot) - the denominator is exact
Under-claiming is always allowed, so a voter never has to predict which block
its transaction lands in.
The unit of weight is the validator, and it already includes delegations: the
P-chain keeps one weight per nodeID and folds each delegator into it
(node vms/platformvm/state/state_validators.go:570,582 — AddStaker(own stake)
then AddWeight(each delegator) under the same key). One map entry per nodeID is
what makes double-counting a delegation impossible. Delegators do not get a
separate ballot; their override is exit, which needs no code.
Authorisation reuses the BLS key the P-chain already binds to the nodeID, so
there is no registry to capture, no admin to hold it and no key ceremony. The
signature covers (domain, chainID, nodeID, voter, authEpoch) only — a standing
authorisation, reusable for every later proposal, rotated by signing a strictly
greater epoch.
Uptime is deliberately not a gate: it is absent from GetValidatorOutput and is
a per-node subjective measurement (all five mainnet validators report
uptime 0.0000 through the public API), so no deterministic rule can read it.
Proven against a read-only fork of mainnet 96369 — the real five-validator set,
real registered BLS keys, real 5e17/2.5e18 weights — including that no ballot
is accepted for any of them without the operator's key.
Address 0x0200...0006 is the next free slot after warp; eth_getCode on 96369
returns 0x for it and 0x01 for warp, so predicates are already live there.
Co-authored-by: Hanzo Dev <[email protected]>
The fee split braided two independent concerns into one line: how much of a
fee is kept (a protocol constant, floor(fee/2)) and where the kept part goes
(compiled-in extras.FeeRewardVault = 0x0100..02). The second half made the
destination ungovernable and silently bypassed the RewardManager precompile
the moment FeeSplit activated, so turning the burn on would have DISABLED the
one on-chain knob the chain already has.
Split the concerns:
ratio -> stays a right shift. Fixed, auditable, not settable by any
transaction. Changing it remains a release plus a coordinated
fork, deliberately: monetary policy should not move at the
speed of a multisig.
destination -> the configured coinbase, i.e. BlockChain.GetCoinbaseAt, i.e.
the RewardManager precompile's stored address. Already part of
header validity (dummy.verifyCoinbase), so it is verifiable
from the header and movable by one admin transaction.
creditTxFee collapses to one credit site; extras.FeeRewardVault is deleted.
Also adds the ordering gate this created a need for: ChainConfig.Verify now
refuses a config that schedules feeSplitTimestamp while rewardManagerConfig is
not enabled at that time (and allowFeeRecipients is off). Without RewardManager
GetCoinbaseAt falls back to the keyless blackhole, so such a config would burn
half of every fee and strand the other half forever -- the C-Chain already has
~3867 LUX stuck at that address. mainnet/cchain.json currently ships exactly
that shape (feeSplitTimestamp 1785715200, no rewardManagerConfig in any of the
49 precompileUpgrades); the node will now refuse to start on it instead.
This is a no-op on every live chain: mainnet 96369's running config has no
feeSplitTimestamp at all (eth_getChainConfig), the vault balance is 0, and the
branch has never executed. The correction is free today and costs a fork later.
Tests:
core: destination follows the configured coinbase; a redirect mid-chain moves
the whole reward stream while the burn is unchanged; supply conservation
on executed blocks; genesis refuses the ungoverned shape.
dummy: a proposer naming any other coinbase is rejected with ErrInvalidCoinbase,
so crediting the header coinbase is not a validator-capture hole.
extras: the verbatim upgrade.json fragment activating rewardManagerConfig and
feeManagerConfig with the DAO Safe as adminAddresses parses through the
registry, and feeManager's governable surface contains no ratio.
Co-authored-by: Hanzo Dev <[email protected]>
eventstream -> v1.7.8, service/s3 -> v1.97.3 (minimum patched versions), plus
the luxfi escape versions that keep this module off the deleted geth v1.16.x
tags (crypto v1.20.2, constants v1.6.2, warp v1.24.1, ids v1.3.2, p2p v1.22.1).
Verified against a measured baseline: go test ./... is 58 ok / 7 fail BOTH with
and without this change — identical, so the failures are pre-existing.
Chasing these one at a time was whack-a-mole — every repair surfaced the next
moved tag (age, pq, threshold, zap, consensus, constants, sdk, keys…). This is
the systematic pass instead: every distinct luxfi/hanzoai module@version in the
workspace (692 of them) was resolved against proxy.golang.org + a direct fetch,
and any go.sum line disagreeing with what is actually served was corrected.
Not an attack, and checked rather than assumed: for each case the proxy and a
direct fetch AGREE with each other and differ from the recorded hash. Two
independent transports agreeing means nothing is rewriting bytes in flight —
the tags moved at source. sum.golang.org still holds the original, but
GOPRIVATE covers github.com/luxfi/* with GOSUMDB=off, so our own modules never
consult the checksum DB and a moved tag splits consumers silently instead of
failing at publish time.
Only the recorded hash changes. No selected version moves, and `go mod tidy`
was deliberately NOT run, so nothing is upgraded as a side effect.
Verified: `go list -m all` resolves with no checksum error.
The durable fix is upstream: treat a published version as immutable. Cut
x.y.z+1 instead of moving a tag — with GOSUMDB off, no consumer can detect it.
The cevmgpu overlay linked build-phase5b (Apr/May, cevm project 0.2.0) GPU
archives against Jul build-mpt (0.5.2) state libs — nm-clean but era-skewed
ABI layout. Repoint the four GPU .a from build-phase5b to build-mpt-gpu: a
Metal-ON build (LUX_CEVM_ENABLE_METAL=ON) co-built from the SAME current cevm
source tree AND the SAME conan deps as build-mpt (it references build-mpt's
own generators/conan_toolchain.cmake; the GPU backend luxgpu_core_static is
compiled in-tree at project version 0.5.2 — the version build-mpt reports).
GPU host archives and state libs are now the same era by construction, closing
the cross-build ABI-layout risk. Verified on darwin/arm64: `go build -tags
"cevm cevmgpu"` links all four archives from build-mpt-gpu (confirmed via the
full linker command line + a move-aside falsification that fails citing the
build-mpt-gpu paths, then rebuilds green deterministically); the default
`-tags cevm` build still links the build-mpt state libs unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
FRONT 1 of the parallel GPU effort. New file bridge_cevm_gpu.go
(//go:build cgo && cevm && cevmgpu) is a pure LDFLAGS overlay, additive to
bridge_cevm.go: under -tags "cevm cevmgpu" both compile and cgo concatenates
their #cgo LDFLAGS, so the CPU state libs (build-mpt) link alongside the GPU
host archives (build-phase5b Metal-ON build of the same cevm source tree):
libevm-gpu.a + libevm-metal-hosts.a + libevm-kernel-metal.a + libevm-gpu-state.a,
plus -framework Metal -framework Foundation.
Symbol audit (nm): every evm:: core/state/precompile symbol the GPU archives
reference is satisfied by build-mpt's state libs; evm::gpu::* symbols resolve
among the four archives; only Metal/Foundation framework symbols come from
outside (no MetalPerformanceShaders, no luxgpu referenced). Device-free anchors
GPUABIVersion/GPUBackendName pull the GPU dispatch objects into the link.
Verified (darwin/arm64):
go build -tags "cevm cevmgpu" ./plugin/ -> links, 69,125,858 bytes,
Metal+Foundation+CoreFoundation linked, 88 evm::gpu symbols in binary
(EvmKernelHost::create, BlockStmGpuMetal::execute_block, execute_block).
go build -tags cevm ./plugin/ -> links, 68,978,754 bytes (CPU-only,
EvmKernelHost::create count 0 — default cevm stays CPU-only, unchanged).
go build ./plugin/ -> links, 68,701,170 bytes (stub path).
SCOPE: BUILD + LINK only. Metal shaders compile at runtime and
MTLCreateSystemDefaultDevice() returns nil headless, so GPU DISPATCH is
CI-gated on a Metal-capable interactive runner — not claimed here.
Co-authored-by: Hanzo Dev <[email protected]>
Picks up dex 34c2ad2 / v1.14.2, which gates the secp256k1 Metal anchor behind
dexgpu so a default darwin build links: previously the anchor referenced
secp256k1_ecrecover_address_batch_metal while the lib that defines it was only
linked under -tags dexgpu, breaking the link of the C-Chain plugin binary that
pulls dex/pkg/dex. `go build ./plugin/` now links the 68 MB plugin clean in the
default build.
Co-authored-by: Hanzo Dev <[email protected]>
The first cut registered the cevm checkpoint store over init.DB before
initializeDBs ran. That is wrong in standalone-database mode: initializeDBs points
the chain state at a separate standalone DB (vm.db), so checkpoints would land in
the base avaDB, not alongside the chain state. It also predated vm.db being set.
Fix: register AFTER initializeDBs, over vm.db — the RAW durable base DB
(NOT the versiondb), standalone-DB-aware (vm.db == the standalone chain-state DB
when UseStandaloneDatabase is set). This is exactly the pattern warpDB / validatorsDB
use (prefixdb.New(prefix, db) over the raw base) for state that need not commit
atomically with the last accepted block: cevm checkpoints now persist on WRITE and
survive a restart, with no versiondb pending-commit dependency — the durability gap
noted when the feature landed.
Still opt-in (cevm-resident-store, default false), inert without -tags cevm, and
safe by construction (a missing/stale checkpoint falls back to the Go-dump seed;
every root is verified). Default plugin build green; resident lazy-reload test green.
Co-authored-by: Hanzo Dev <[email protected]>
Wires StateLoadFromLazy into the resident applier so the C-Chain, on restart,
resumes cevm from a persisted checkpoint (bounded-RAM lazy load) instead of dumping
the full Go state into cevm — the "own disk-backed state" evolution the seed-cap
comment names. Removes the post-restart declined-too-large seed dump.
- core/parallel/cevm_resident_store.go: SetCevmResidentStore(db) (the VM registers a
durable KV; nil = off, unchanged behavior). tryLazyLoadCheckpoint pages the account
trie from disk via cevmbridge.StateLoadFromLazy and sets the resident to the
checkpoint height; persistCheckpoint writes StatePersist records + the height at a
bounded cadence (default 4096 blocks).
- cevm_shadow.go: getResident tries the checkpoint before a fresh handle;
persistCheckpoint runs after a block's cevm root is VERIFIED against the header.
- plugin/evm: CevmResidentStore config flag (default false) → registers a "cevmstate"-
prefixed sub-DB before chain init.
- cevmbridge/bridge_stub.go: StateLoadFromLazy no-op for the non-cevm build.
Safe by construction + triple-gated: nothing runs unless built -tags cevm, the VM
registered a store, AND a checkpoint exists; and the applier still verifies EVERY
cevm root against the header, so a stale/wrong checkpoint only forces a resync (a
Go-dump seed next block), never a wrong commit. nil store (the default) is the prior
in-memory, seed-from-Go behavior byte-for-byte.
Proof (cevm_resident_store_cevm_test.go, //go:build cevm): seed + apply a block,
persistCheckpoint, simulate restart (free handles, retain store), getResident
LAZY-LOADS the checkpoint (height + root recovered, no Go dump), then the next block
applies to a byte-identical FULL root vs a full-reseed reference. Full parallel cevm
suite green; default (non-cevm) plugin build unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
Wires cevm's C-ABI lazy load into the Go bridge so a node applies blocks over
persisted state without materializing all of it. StateLoadFromLazy builds a
store-backed cevm StateDB (via C.cevm_state_load_lazy) whose account trie is a
from_store_root ref at the persisted root; reads and writes fault account leaves
and storage sub-tries in from zapdb on demand, so RAM is bounded to the block's
touched working set. StateApplyBlock on the handle produces the FULL state root
paged.
Lifetime: unlike a full reseed (which touches the store only during the load),
a lazy handle faults through the store on every later apply, so its cgo.Handle
AND its cevm_kv_store (C-heap allocated) are retained in lazyResources and freed
by StateFree after cevm_state_free has stopped the C++ side faulting through them.
Proof (store_lazy_cevm_test.go, //go:build cevm): seed + apply 2 blocks, persist
to a REAL on-disk zapdb, restart (free handle + close db), reopen, then load the
SAME state two ways — StateLoadFrom (full reseed) and StateLoadFromLazy — and apply
an identical next block to both. Both recover the committed root; the paged apply
root is byte-identical to the full-reseed apply. The full cevmbridge cevm suite
(incl. the existing restart-survival test) stays green.
Co-authored-by: Hanzo Dev <[email protected]>
Wire cevm's new persist/load C-ABI to a geth-style database.Database (in
production luxd's own zapdb), and prove restart-survival.
- store_cevm.go: StatePersist(handle, db) / StateLoadFrom(db) -> handle.
A cevm_kv_store whose get/put point at Go trampolines is built in C
(cevmbridge_make_store), so cevm (C++/cgo) calls db.Get/Put back through
THIS Go runtime — no second Go runtime is embedded (the two-runtime
constraint). The ctx is an opaque cgo.Handle token cast uintptr->void* in
C (no uintptr<->unsafe.Pointer in Go).
- store_callbacks_cevm.go: the //export get/put trampolines (kept separate —
//export forbids C definitions in its file's preamble) resolving the ctx
back to a *kvStore over a Get/Put backend.
- bridge_stub.go: !cevm no-op StatePersist/StateLoadFrom keep `go build ./...`
green without the tag.
- store_cevm_test.go (//go:build cevm): RESTART-SURVIVAL PROOF. Seed + apply
2 blocks against a resident cevm StateDB, StatePersist to a REAL zapdb
(zapdb.New on t.TempDir()), record the root; then StateFree the handle AND
Close the zapdb (full restart), reopen the zapdb from disk, StateLoadFrom
into a NEW handle, and assert StateRoot(new) == pre-restart root
byte-identical. Then apply the NEXT block on the reloaded handle and assert
it matches the stateless (geth-golden) reference.
Result: "persisted 300 accounts to zapdb, reloaded, root
0x5e9db5d6...794d74f == pre-restart root"; next block after reload matches.
All existing gates stay green: cevmbridge golden + resident-across-blocks,
core/parallel conformance 9/9, resident 256-block, shadow 256-block e2e.
MVP scope (honest): persists account state; the resident MPT is rebuilt on
load (root-identical), NOT yet bounded-RAM load-on-demand.
Co-authored-by: Hanzo Dev <[email protected]>
The cevm gated applier (cevm_shadow.go) dumped the WHOLE Go pre-state
every block and rebuilt a fresh cevm StateDB for each — capped at
maxShadowAccounts, which WAS the declined-too-large decline at scale.
Wire the persistent cevm StateDB behind a per-chain handle instead:
- bridge_cevm.go: StateCreate/StateSeed/StateApplyBlock/StateRoot/StateFree
wrappers over the new cevm_state_* C-ABI; marshalling factored into
shared marshal*/unmarshal* helpers (DRY with ProcessBlock). Stubs in
bridge_stub.go keep the default (non-cevm) build a no-op.
- cevm_shadow.go: a residentState per chain (keyed by chain ID). The FIRST
block seeds cevm once from the Go pre-state dump; SUBSEQUENT in-sync
blocks call StateApplyBlock with NO dump and compare cevm's resident root
to header.Root. On AGREE the four-way apply gate writes cevm's post-state
delta into the Go statedb (unchanged) and the resident advances to height
N. On DISAGREE/DECLINE/ERROR the resident is flagged out-of-sync and
re-seeded from the (correct) Go state on the next block. New counters
ResidentApplied / ResidentSeeds / ResidentReseeds. A wrong resident root
can never commit — the root check + four-way gate decline it.
Proof (CGO_ENABLED=1 -tags cevm):
- TestCevmResidentVerifiesRealBlocks: over the same 256 real InsertChain
blocks, cevm's RESIDENT root == Go header.Root at ALL 256 heights,
applied via the resident handle, with ResidentSeeds == 1 (not 256) and
0 resyncs — the declined-too-large killer: 1 full-state dump vs 256 blocks.
- TestStateApplyBlockGoldenRoots + TestStateResidentAcrossBlocksMatchesStateless
(cevmbridge): the resident C-ABI is byte-identical to the geth golden
roots and to a stateless reseed across blocks.
- Existing TestProcessBlockGoldenRoots (stateless), the 256-block
TestCevmShadowVerifiesRealBlocks, and the 9/9 applier conformance all
stay green.
Caveat: this makes the SHADOW/applier resident (no per-block dump). cevm as
the SOLE mainnet engine additionally needs zero declines + disk-backed state
+ node ownership — not claimed here.
Co-authored-by: Hanzo Dev <[email protected]>
The cevm gated applier declined EIP-2930 access-list txs as DISAGREE: the
access list never crossed the bridge, so cevm charged only the 21000 base
intrinsic where the Go EVM charges 21000 + 2400/addr + 1900/key. On a
fee-bearing block that 4300-gas shortfall under-credits the coinbase and
diverges cevm's keccak-MPT root from header.Root, so the four-way gate
(correctly) routed the block to the Go EVM.
Carry the access list end-to-end:
- cevmbridge.Tx gains AccessList []AccessTuple (nil for legacy txs); new
AccessTuple{Address, StorageKeys}.
- bridge_cevm.go marshals it across cgo into the appended cevm_pb_tx.access_list
(tuple array + flattened key buffers allocated in C memory — cTxs is Go
memory passed to C and cgo forbids nested Go pointers — freed on return).
- cevm_shadow.go populates it from tx.AccessList().
Pairs with luxcpp/cevm's process_block surcharge + EIP-2929 warm-set. The
09-access-list-tx conformance scenario flips DISAGREE -> APPLIED
(Agree:1 Applied:1), lifting the applier to 9/9 diverse tx-types. No
regression: the ABIVersion==2 guard, TestProcessBlockGoldenRoots, and the
256-block TestCevmShadowVerifiesRealBlocks (256/256 applied) stay green, and
the default (non-cevm) build is unchanged (additive shared type + stub path).
Co-authored-by: Hanzo Dev <[email protected]>
Adds core/parallel/cevm_applier_conformance_test.go (//go:build cevm) — a
permanent conformance gate that measures cevm's REAL per-tx-type apply/decline
coverage as the gated APPLIER (cevm_shadow.go). For each of 9 diverse scenarios
it builds a fresh preimages-ON chain, generates ONE block exercising exactly one
tx-type (CALL-target contracts pre-deployed in genesis so the measured block is
a single call), resets the shadow counters, runs chain.InsertChain (the applier
path), and records the ShadowStats delta.
Measured today (CGO_ENABLED=1 go test -tags cevm ./core/parallel -run Conformance):
APPLIED (5): value-transfer, call-sstore, call-log1, call-oog, multi(transfer+call)
DECLINED (4): contract-create, call-revert, precompile-identity, access-list-tx
every decline is DISAGREE (cevm's independent keccak-MPT root != Go header.Root);
none hit reverted-mismatch / errored / declined-*. InsertChain OK on all 9
(the Go EVM always applies regardless of cevm's verdict).
Headline: cevm applier covers 5/9 diverse tx-types. New findings vs the doc's
"safe subset" claim: LOG1 and out-of-gas calls DO apply; contract CREATE and
top-level REVERT do not. This table is the punch-list for widening cevm parity.
Measurement only — does NOT modify cevm_shadow.go or the bridge. Tag-gated, so
the default no-tags build is unchanged (build+vet green; -run Conformance finds
no test without -tags cevm).
Co-authored-by: Hanzo Dev <[email protected]>
Apples-to-apples benchmark of the cevm (C++ EVM) cgo ProcessBlock path vs
the Go EVM (luxfi/geth core/state) raw state transition, on the golden block
of N value transfers that both engines prove to the same keccak-MPT root
0x57b7250733ad7f7a...020099.
- TestBenchRootParity: validity gate. At N=1000 cevm==go==geth golden; at
N=10000 cevm==go mutual. If roots diverge the comparison is invalid.
- BenchmarkCevmProcessBlock: whole cgo ProcessBlock (seed C++ state + full
validated EVM block + real keccak-MPT commit + result marshalling).
- BenchmarkGoStateApply: Go state transition only (Sub/Add/SetNonce +
IntermediateRoot(true)); seeding excluded. Narrowest Go baseline.
- BenchmarkGoStateSeedApply: Go seed+apply+root, the pure-function analogue
of ProcessBlock.
Measured (Apple M1 Max, best-of-5, -tags cevm): at N=10k cevm ProcessBlock
~24.5ms / ~8585 Mgas/s vs Go-apply ~71.2ms and Go-seed+apply ~87.6ms — cevm
~2.9x / ~3.6x faster while doing strictly more work (full EVM + validation +
gas + cgo round-trip vs state-mutation-only).
Co-authored-by: Hanzo Dev <[email protected]>
The cevm C-ABI is now v2 (post-state delta export); the golden-roots test
guard still expected v1 and Fatalf'd before the byte-exact root check. Roots
unchanged across v1->v2. TestProcessBlockGoldenRoots now green (N=1/100/1000
byte-match geth).
Co-authored-by: Hanzo Dev <[email protected]>
cevm now ACTUALLY EXECUTES blocks (applies post-state) in luxfi/evm's block
executor, not just shadow-verifies the root — safely gated so it can never
diverge consensus.
Bridge (cevmbridge): ProcessBlock now surfaces the ABI-v2 post-state DELTA and
logs the C-ABI already exports — PostAccounts (address, absolute nonce/balance/
code, Deleted, CodeChanged), PostStorage (address, key, value), and Logs
(address, topics, data, tx_index) — alongside the root + per-tx results.
Executor (cevm_shadow.go): on the AGREE path (cevm root == header.Root) the
executor PROMOTES to applier under an APPLY-THEN-VERIFY-ELSE-DECLINE gate that
is consensus-safe BY CONSTRUCTION:
- reconstruct receipts from cevm's per-tx {status, cumulative gas, gasUsed,
logs} + txs (Type, TxHash, ContractAddress via crypto.CreateAddress for
CREATE, per-receipt Bloom) mirroring core/state_processor.go applyTransaction;
- replay cevm's post-state delta onto an ISOLATED statedb.Copy() and compute
its IntermediateRoot(IsEIP158);
- keep the writeback ONLY IF all FOUR quantities block_validator.ValidateState
checks byte-match the header: state root, aggregate bloom, receipt-trie hash
(DeriveSha), and cumulative gas. Then replay the identical delta onto the
LIVE statedb and return the receipts — the Go EVM does NOT re-run the block;
- on ANY mismatch, or an unrepresentable block (blob tx, cevm-rejected tx,
tx-result count skew, missing preimage, too-large, disagree, finalize-gap),
return (nil, nil) with the LIVE statedb UNTOUCHED so the Go EVM applies.
The trial runs on a deep Copy() rather than Snapshot()/RevertToSnapshot()
because IntermediateRoot -> Finalise -> clearJournalAndRefund invalidates the
journal, making a post-IntermediateRoot revert unreliable; verifying on a copy
leaves the live statedb pristine on every decline (nothing to revert). A wrong
or incomplete delta can therefore never reach the live state.
New honest counters: Applied (four-way gate passed, cevm EXECUTED the block) and
RevertedMismatch (agreed on root but the gated writeback/gate failed → declined
to Go). Invariant: Applied + RevertedMismatch == Agree.
Proven (-tags cevm, build-mpt CPU): the e2e test drives 256 real InsertChain
blocks (768 transfers) through Process -> ExecuteBlock and reports
Applied:256 RevertedMismatch:0 Disagree:0 — cevm executed every block in the
proven safe subset and InsertChain succeeded (ValidateState's four-way check
passed on the written-back state). Whole ./core suite stays GREEN: 9794 blocks
observed, all safely declining (preimages off => cannot seed cevm) and falling
through to Go, no regression. ./core/parallel suite green.
Co-authored-by: Hanzo Dev <[email protected]>
Wires the existing cevm (C++ EVM) byte-exact shadow verifier so it observes
EVERY block of the real ./core suite, and reports the true tally — without
weakening the gate or ever letting cevm apply state.
- core/main_cevm_test.go (//go:build cevm): a cevm-only TestMain that runs the
whole ./core suite then prints the aggregate parallel.CevmShadowStats(). Because
state_processor.go:124 routes every block through
parallel.DefaultExecutor().ExecuteBlock — which under -tags cevm is the
cevmShadowExecutor — this is the honest whole-suite count. main_test.go's goleak
TestMain is gated //go:build !cevm so exactly one TestMain exists per build.
- core/parallel/cevm_shadow_registration_test.go (//go:build cevm): asserts
DefaultExecutor() is the cevmShadowExecutor (not the no-op fallback) — the
precondition for the whole claim.
- core/parallel/cevm_shadow_e2e_test.go: scaled the real-StateProcessor agreement
proof from 3 to 256 genuine InsertChain blocks (768 transfers). cevm's
independent real-MPT process_block root byte-matched the Go EVM header.Root on
ALL 256/256 blocks (0 disagree, 0 finalize-gap).
- Four precompile test files (genesis_dexsettle_reproduce, precompile_alwayson,
precompile_statedb_subbalance, precompile_env_call) gated //go:build !cevm: they
alone pull precompile/registry -> luxfi/precompile/dex -> luxfi/dex/pkg/dex whose
cgo file #includes the absent dex_gpu.h, breaking the CGO_ENABLED=1 build the
bridge needs. They are precompile tests, not block-execution tests, and still run
in every non-cevm build — an orthogonal blocker isolated, not a cevm regression.
HONEST RESULTS (CGO_ENABLED=1 -tags cevm):
- Whole ./core suite: 78 PASS / 0 FAIL / 32 SKIP. cevm observed all 9794 blocks;
it declined every one as no-preimage (the production default runs preimages OFF,
and the shadow needs trie-key preimages to recover the 20-byte addresses that
seed cevm) — 0 disagree, 0 errored, 0 false roots. The Go EVM applied every
block; cevm never touched state.
- Real-block agreement (preimages ON, the config the shadow requires), driven
through core.BlockChain.InsertChain -> StateProcessor.Process -> the shadow:
256/256 blocks byte-exact agreement (768 transfers).
Default (!cevm) build unchanged: ./core and ./core/parallel green in both CGO
modes; the 10 gated precompile tests present in the default build.
Co-authored-by: Hanzo Dev <[email protected]>
Route-gated activation of the Lux C++ EVM (cevm) through the BlockExecutor
seam (SEAM 1): parallel.RegisterExecutor makes core/state_processor.go run
ExecuteBlock on every block. Because the CONSENSUS root is taken from the Go
statedb.IntermediateRoot() (block_validator.go), cevm-as-applier would need
host-callback state write-back (deferred); so this lands cevm as a pure
SHADOW VERIFIER — it recomputes the block's keccak-MPT root independently and
confirms it matches the Go EVM's header.Root, then ALWAYS returns (nil,nil)
so the Go path applies the block. Root-only checking sidesteps both known
cevm gaps (event logs, state write-back).
core/parallel/cevmbridge: cgo seam to cevm's extern "C" cevm_process_block
(luxcpp/cevm build-mpt CPU libs, real MPT commit — NOT the placeholder-root
gpu path). Two variants share one API: bridge_cevm.go (//go:build cevm) links
the verified build-mpt + Conan link line and calls the real process_block;
bridge_stub.go (//go:build !cevm) is pure Go and declines, so `go build ./...`
stays green on any machine and the C++ path is opt-in via -tags cevm (the
existing backend_cevm.go convention). Balance is LE limbs; value/gasPrice/ctx
256-bit fields are BE — matching go_process_block.h exactly.
core/parallel/cevm_shadow.go: the BlockExecutor. Snapshots the pre-block state
read-only via DumpToCollector (address<-trie-key preimage, nonce, balance,
code, storage), builds the cevm tx array (sender via block signer, effective
gas price matching evm/core/state_transition.go's full-fee-to-coinbase model),
runs cevmbridge.ProcessBlock, and cross-checks the root against header.Root.
Atomic counters tally agree / disagree / finalize-gap (withdrawals move
header.Root; cevm runs no engine.Finalize — a state no-op on Lux otherwise) /
declined-too-large / declined-no-preimage / declined-tx / errored, read via
CevmShadowStats. Panics in the snapshot/bridge are swallowed and counted: the
verifier can never destabilise consensus. Registration is gated on
cevmbridge.Enabled so default and -tags parallel builds are byte-unchanged.
parallel.go: initialize txExecutors at declaration instead of in init().
Backend files register from their own init() (backend_cevm.go under -tags
cevm), and Go orders init() funcs alphabetically by filename — backend_cevm.go
precedes parallel.go, so the old init()-time map creation raced too late and
panicked on a nil map under -tags cevm. Var initializers run before all
init() funcs, fixing the order deterministically.
Proven:
- cevmbridge golden roots through the Go marshalling byte-match luxfi/geth:
N=1 0xb9d849f9…805e9, N=100 0x2028713f…7cf0f, N=1000 0x57b72507…20099.
- e2e (TestCevmShadowVerifiesRealBlocks, -tags cevm): 3 real blocks through
core.BlockChain.InsertChain -> StateProcessor.Process -> shadow ExecuteBlock;
cevm's real-MPT root byte-matched the Go EVM header.Root on all 3
(Processed:3 Agree:3 Disagree:0, zero declines/errors).
- default (stub) core/parallel suite unchanged and green.
Co-authored-by: Hanzo Dev <[email protected]>
Range by index + take the slice element address instead of copying types.Block
(which carries an atomic.Pointer cache). go vet ./core/ is now clean.
Co-authored-by: Hanzo Dev <[email protected]>
creditTxFee runs once per tx. Bench (darwin/arm64, -benchtime=2s):
RawBaseline (pre-split, bare AddBalance) : 168.6 ns/op 4 allocs
Legacy (shipped dormant path) : 162.9 ns/op 4 allocs <- within noise
Split (active vault credit + burn) : 175.5 ns/op 5 allocs
The GetExtra+IsFeeSplit branch adds no measurable overhead when dormant; the
split path costs ~7% + 1 alloc only when the DAO activates it.
Co-authored-by: Hanzo Dev <[email protected]>
Completes the WIP: all 6 openChain call sites now pass snapshotVerify
(verifyAt honors --verify-snapshot; inspect/plan/resolve/apply use apply-mode
snapshots-off), and the missing --vote-guard-file flag is declared+bound. The
tool is the offline operator front-end for core.FinalizeRewind — the recovery
for incident 1085xxx (inner EVM ahead of proposervm finality after RLP
import->produce). Fail-closed: exclusive DB lock (luxd stopped), --yes gate,
max-loss fat-finger guard, identity-bound re-execution, dry-run inspect/plan,
post-rewind re-open verify. Compiles + vets clean.
Co-authored-by: Hanzo Dev <[email protected]>
Documents the owner decision: 100% of C-Chain tx fees route to the DAO Gov
Safe via the RewardManager precompile (config-only, proven on devnet), with
the 50/50 creditTxFee/FeeSplitTimestamp burn+stake split committed but DORMANT
(gated off) as a ready in-protocol option.
Co-authored-by: Hanzo Dev <[email protected]>
FinalizeRewind (core/rewind.go) rolls the EVM accepted tip DOWN to a
consensus-finalized height — offline recovery for the proposervm/EVM height
inconsistency (mainnet incident 1085xxx: inner EVM ran ahead of finality after
RLP import->produce). Fail-closed: asserts head==lastAccepted==target with
state present and canonical-above removed; INVALIDATES (never repoints) the
flat snapshot so it rebuilds from targets trie (repointing would serve the
discarded tips balances fleet-wide). Zero-re-exec target via
HighestCommittedStateAtOrBelow.
rlp_seam_red_test.go: RED proof of the import->produce seam (the exact
regenesis flow) — produced blocks chain contiguously onto the imported RLP tip
with no gap/divergence. Passes on real data (lux-mainnet 1083347..1083873
contiguous; zoo 12301 blocks). rewind_test reproduces the recovery at unit
scale. All pass CGO off.
(cmd/repair-cchain, the offline CLI wrapper, is unfinished WIP and NOT included
— its capability lives here in FinalizeRewind.)
Co-authored-by: Hanzo Dev <[email protected]>
Single fee-disbursement seam in state_transition.go delegates to creditTxFee
(core/fee_split.go): full-fee-to-coinbase legacy path by default, or a
deterministic 50%-true-burn + 50%-to-FeeRewardVault (0x0100..0002) split once
the standalone FeeSplitTimestamp fork is active. Gated OFF everywhere
(FeeSplitTimestamp nil) so the LIVE path stays RewardManager -> DAO Gov Safe;
this is the ready in-protocol-split option the DAO can enable later.
Proven (CGO off): fee_split_test (split math, conservation, odd-wei, determinism,
legacy), fee_split_block_test (end-to-end burn on accepted blocks),
reward_manager_routing_test (100% -> DAO Safe + live on-chain setRewardAddress
redirect). params/extras suite green incl. the 3 mainnet boot-compat guards.
Co-authored-by: Hanzo Dev <[email protected]>
luxMainnetLiveUpgradeJSON claimed the mainnet precompiles activate at
blockTimestamp 0. The ACTUAL deployed manifest (universe luxd-startup.yaml)
and canonical genesis upgrade.json both schedule all 49 at 1766708400 (the
strict-PQ fork, past the RLP tip) precisely so genesis stays 2-alloc and
block-0 hashes to 0x3f4fa2a0 for clean RLP import. The 0-timestamp fixture
was pre-forward-dating (2026-06-27) and made the boot-compat guard cry wolf.
Verified all 16 keys exist in canonical @1766708400. Guard function intact
(comment now forbids ever moving an activation back to 0).
Co-authored-by: Hanzo Dev <[email protected]>
Computes core.Genesis.ToBlock().Hash() on the maintained EVM path, replacing
the legacy coreth-based state/cmd verifiers (which no longer build against the
current stack). Used to close the mainnet regenesis launch gate: canonical
genesis/configs/mainnet/cchain.json and the deployed k8s cChainGenesis both
compute 0x3f4fa2a0...ec61e (== RLP block-0), state root 0x2d1cedac.
Co-authored-by: Hanzo Dev <[email protected]>
The startup --import-chain-data path (vm.go, after importBlocksFromFile) advanced
core.BlockChain + acceptedBlockDB to the imported tip but never refreshed the
in-memory consensus wrapper vm.State (components/chain), which initChainState had
built at the pre-import genesis tip. On the FIRST import boot the consensus engine
then reads genesis while the EVM head is the imported tip and the proposervm wedges
("BuildBlock: preferred block not fetchable", preferred=empty id) until a restart
re-runs initChainState off acceptedBlockDB.
admin_importChain runs on a LIVE node whose vm.State is already current, so only the
startup path needs this. Now syncs vm.State via the existing, boot-safe
chain.State.SetLastAcceptedBlock, fail-closed (a boot that cannot establish the
imported tip stops loudly rather than serving consensus at a stale genesis).
Empirically proven on lux-testnet 96368 / node v1.36.17 (luxfi/evm v1.104.9): first
import boot wedged at tip 251, healed only on restart; with this the first boot
finalizes past the import tip with no restart. Imported-state execution / stateRoot
unchanged.
Co-authored-by: Hanzo Dev <[email protected]>
The message/validators/warp marshalers are hand-rolled per-type packers
(marshalValue type switch) that ignore struct tags — the linearcodec that read
them is gone. Pure dead metadata. Golden round-trip tests green (byte-identical).
Co-authored-by: Hanzo Dev <[email protected]>
api v1.0.16 added Capabilities uint64 to zap InitializeResponse (Quasar-export
capability bit). The plugin built at v1.0.15 answered Initialize 8 bytes short,
so node-side Decode died with unexpected EOF — the cross-process ZAP e2e
(node vms/rpcchainvm/zap TestCEvm_DialsZAPdbServer) caught it. Additive field;
plugin leaves the bit clear until the EVM wires CapQuasarExport.
Co-authored-by: Hanzo Dev <[email protected]>