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]>
39 KiB
Lux EVM Module
Project Overview
Lux EVM (formerly EVM) is the Ethereum Virtual Machine implementation for Lux L2 chains. This module provides EVM compatibility for the Lux network.
CRITICAL VERSION REQUIREMENTS
ALWAYS use these Lux-specific versions:
github.com/luxfi/node v1.22.64- Latest Lux node versiongithub.com/luxfi/geth v1.16.64- Our fork of go-ethereum (with PQ crypto precompiles)github.com/luxfi/crypto v1.17.27- Cryptographic primitivesgithub.com/luxfi/precompiles v0.1.2- Standalone precompile contractsgithub.com/luxfi/p2p- P2P networking packagegithub.com/luxfi/warp- Warp messaging packagegithub.com/luxfi/consensus- Consensus package
IMPORTANT: Package Usage
- Use
lp118package for p2p handlers - Import:
github.com/luxfi/p2p/lp118 - All handler IDs:
lp118.HandlerID - All functions:
lp118.NewCachedHandler,lp118.NewSignatureAggregator - NEVER import from ava-labs packages
- NEVER use go-ethereum directly, always use luxfi/geth
p2p.Handler Interface
The p2p.Handler interface uses these methods:
Gossip(ctx, nodeID, gossipBytes)- NOT AppGossipRequest(ctx, nodeID, deadline, requestBytes) ([]byte, *p2p.Error)- NOT AppRequest
p2p.Sender Interface
The p2p.Sender interface requires:
SendRequest(ctx, nodeIDs, requestID, request) errorSendResponse(ctx, nodeID, requestID, response) errorSendError(ctx, nodeID, requestID, errorCode, errorMessage) errorSendGossip(ctx, config p2p.SendConfig, msg) error
p2p.Network Methods
Request(ctx, nodeID, requestID, deadline, request)- NOT AppRequestResponse(ctx, nodeID, requestID, response)- NOT AppResponseGossip(ctx, nodeID, gossipBytes)- NOT AppGossip
warp Package Types
NewUnsignedMessage(networkID uint32, sourceChainID ids.ID, payload []byte)- takes ids.ID directlyValidator.NodeIDisids.NodeID- NOT []byte
Module Structure
/home/z/work/lux/evm/
├── plugin/evm/ # Main VM implementation
├── core/ # Core blockchain logic
├── consensus/ # Consensus engine (dummy for Lux)
├── eth/ # Ethereum protocol implementation
├── miner/ # Block building and mining
├── precompile/ # Precompiled contracts and warp
├── network/ # P2P networking
├── params/ # Chain configuration
├── scripts/ # Build and test scripts
└── warp/ # Cross-chain messaging
Current Build Status
✅ ALL TESTS PASSING - v0.8.18
All 62 test packages pass. Key fixes:
- PQ Crypto Precompiles - ML-DSA, SLH-DSA, ML-KEM integrated via geth v1.16.64
- Import Cycle Fixed - precompiles v0.1.2 has no geth/core/vm dependency
- Bind Tests Fixed - All ABI binding tests now pass
Package Versions (2025-12-25)
| Package | Version | Status |
|---|---|---|
| evm | v0.8.18 | ✅ All tests pass |
| geth | v1.16.64 | ✅ With PQ precompiles |
| precompiles | v0.1.2 | ✅ Standalone |
| crypto | v1.17.27 | ✅ All tests pass |
| node | v1.22.64 | ✅ Builds clean |
Post-Quantum Crypto Precompiles
LP-aligned addresses (P=2 for PQ/Identity family):
- ML-DSA (FIPS 204) - Lattice-based signatures -
0x12202 - SLH-DSA (FIPS 205) - Hash-based signatures -
0x12203 - ML-KEM (FIPS 203) - Key encapsulation -
0x12201
EVM Upgrade Defaults (2026-03-29)
All EVM network upgrades are enabled at genesis (timestamp 0) by default:
- EVMTimestamp = 0 (always)
- DurangoTimestamp = 0 (Shanghai: PUSH0, warm coinbase)
- EtnaTimestamp = 0 (Cancun: MCOPY, TSTORE, TLOAD, BLOBHASH, BLOBBASEFEE)
- FortunaTimestamp = 0
- GraniteTimestamp = 0
EVMDefaultChainConfig also sets ShanghaiTime and CancunTime at 0 with
BlobScheduleConfig so geth's jump table selects the full Cancun instruction set.
The mapping: Durango -> Shanghai, Etna -> Cancun. These are set in SetEthUpgrades().
Stateful precompiles (nativeminter, feemanager, etc.) are NOT enabled in the
default config because they require explicit permission configuration (admin
addresses). Call SetAllGenesisPrecompiles() on the extras config to enable them.
Genesis precompile activation: ApplyPrecompileActivations now runs at genesis
(parentTimestamp=nil). Previously it returned early, preventing genesis precompile
state from being written. The fix ensures deterministic genesis because precompile
configs are part of the chain config.
C-Chain Tx-Fee Routing — RewardManager → DAO Gov Safe (LIVE PATH)
Owner decision (supersedes the 50/50 below): 100% of every C-Chain tx fee accrues to the chain's DAO Gov Safe, DAO-governed and redirectable on-chain. This decouples collection from policy — the DAO later decides burn / validators / treasury via governance, on-chain, without another node upgrade. Real money on a 2T supply — staged devnet→testnet→mainnet, never touch mainnet fee config without staged proof + owner go.
Mechanism — existing RewardManager precompile, NO new fee-path code
The routing is stock C-Chain machinery, activated by config:
rewardmanagerprecompile at 0x10205 (precompile/contracts/rewardmanager/).- Activated via precompileUpgrades (
upgrade.json) at a dated timestamp (coordinated fork, NOT a genesis edit), gated so mainnet is inert until set. initialRewardConfig.rewardAddress = <DAO Gov Safe>→StoreRewardAddress.adminAddresses = [<DAO Gov Safe>]→ the DAO can callsetRewardAddresson-chain anytime (the "continually governed / redirectable" requirement; role-gated atcontract.go:195— non-admin callers revert).- Routing path (all existing production code):
GetCoinbaseAt(core/blockchain_reader.go:411) readsGetStoredRewardAddress→ setsheader.Coinbase = reward address→core/state_transition.go:546creditTxFeecredits the full fee to the coinbase. So 100% of the fee lands at the DAO Safe. - The 50/50
creditTxFee/FeeSplitTimestampcode (below) stays committed but DORMANT (FeeSplitTimestamp nil ⇒ legacy full-fee-to-coinbase branch), so it is transparent to RewardManager and available if the DAO ever wants an in-protocol split.
Proven (in-process, production code paths)
core/reward_manager_routing_test.go — TestRewardManagerRoutesFeesToDAOSafe: with
RewardManager active + reward = Lux DAO Gov Safe 0x8E29b816…, the production
GetCoinbaseAt returns the DAO Safe (NOT blackhole); a real setRewardAddress tx
from the admin redirects routing to a new address live (observed via GetCoinbaseAt).
Combined with TestCreditTxFeeLegacy (full fee → coinbase, FeeSplit off) this proves
100% → reward address + on-chain DAO redirect. RewardManager's own suite
(precompile/contracts/rewardmanager/) covers role-gating + storage.
Verified end to end on a multi-node network
Activation, routing and redirect were exercised on a running 5-node network with an unmodified node and evm build — RewardManager is already present, so this is a config change and not a rebuild.
- Activation: append
rewardManagerConfigtoprecompileUpgradeswithinitialRewardConfig.rewardAddressset to the chain's DAO Safe andadminAddressesto the address that will govern it, then restart the fleet to load it. - 100% routing: every tx fee accrues to the reward address; the blackhole address stays flat.
- On-chain redirect:
setRewardAddressfrom an admin moves accrual to the new address from the next block, and back again — the DAO keeps control without a node upgrade. - Determinism: the reward balance and the state root agree across every node at the same height, which is what makes this a consensus change rather than a local accounting one.
Per-chain DAO reward addresses (read canonical from ~/work/lux/standard/deployments/org-safes/)
- Lux C-Chain 96369 → DAO Gov Safe
0x8E29b816c6C35b13cE1ff68D33E245C2bda8ac3D - Zoo 200200 → DAO Safe
0x229599f227231d8C90fcF1a78589F5DC4b7A6962 - Hanzo / Pars → DAO Safes not yet in org-safes/ — STAGE before activation.
Deployable
upgrade.jsontemplates:~/work/lux/standard/deployments/rewardmanager/(lux-96369.json,zoo-200200.json;blockTimestampsentinel 9999999999 = inert until the operator sets the coordinated fork time).
How the upgrade schedule reaches the node
The C-Chain upgrade schedule is a file at <chain-config-dir>/C/upgrade.json. An
operator delivering it by ConfigMap copies the key into that path at boot; the
node reads it on start, so a schedule change is a restart and not a rebuild.
Tx-Fee 50/50 Split — burn + staking-reward fold (DORMANT option, NOT the live path)
Superseded as the live behavior by the RewardManager DAO routing above. This code is committed but gated OFF (
FeeSplitTimestampnil everywhere) and kept as a ready option if the DAO ever chooses an in-protocol burn+stake split. The P-Chain fold-in was NOT built (design only). Original design follows.
Owner decision (superseded): every C-Chain transaction fee is divided 50% true burn (real supply reduction) + 50% folded into the P-Chain staking reward (validators' returns grow from fee flow, unified into the native reward payout — NOT a separate C-Chain coinbase stream).
Current reality this replaces
The C-Chain credits the entire fee (base fee + tip, EVM-mode accounting) to
block.Coinbase, which defaults to the blackhole 0x0100…0000
(core/state_transition.go fee-credit site; coinbase forced to blackhole by
GetCoinbaseAt). Nothing is burned; the fee just piles up in an uncontrolled EOA
(~3,778 LUX stuck there). The intended split was never built. Note: geth's own
core/state_transition.go:569 is a different path used only by geth-direct
chains — the C-Chain runs evm's vendored core/state_transition.go, so the
split lives there.
Mechanism (the ONE seam, deterministic, config-gated)
Single fee-disbursement site: evm/core/state_transition.go execute(). The
per-tx credit is delegated to creditTxFee (evm/core/fee_split.go):
- Gate —
extras.ChainConfig.FeeSplitTimestamp(a standalone fee-routing fork, sibling toAllowFeeRecipients; intentionally NOT in theNetworkUpgradesopcode-fork chain — it is orthogonal to EVM/Durango/Quasar/… and has no ordering relationship to them).IsFeeSplit(time)gates activation. A compatibility check (checkConfigCompatible) freezes the activation time once effective, so a node cannot silently reschedule it and fork. - Split —
fee=gasUsed × effectiveGasPrice(a consensus value):rewardShare = fee >> 1(floor(fee/2)) → credited toextras.FeeRewardVault(0x0100…0002), a protocol-owned account that holds real, tracked balance.burnShare = fee − rewardShare(ceil(fee/2)) → credited to no account. Never re-crediting it removes it from the account trie: the sum of all balances (the supply) drops by exactlyburnShare. This is a true burn, the same mechanism as the EIP-1559 base-fee burn — NOT a transfer to a dead EOA (which leaves supply unchanged). The odd wei on an odd fee goes to the burn.
- Legacy path unchanged — when
FeeSplitis inactive, the full fee goes to the coinbase exactly as before.
Determinism: fee is a consensus value; the split is pure uint256 integer math
(one right shift) to a fixed address — every validator computes an identical
post-state, so the handler cannot fork the chain. A running multi-node net that
keeps finalizing is the determinism proof.
Conservation invariant
Per tx (fee F): Δ(Σ balances) = −burnShare, Δ(vault) = +rewardShare,
rewardShare + burnShare = F. Whole-system, after the vault is atomically exported
to P-Chain and paid to validators (a move, not a mint):
total_supply_after = total_supply_before − Σ burnShare. The reward half is
supply-neutral (redistributed); the curve-based staking mint is orthogonal and
unchanged. No mint for the fee half ⇒ structurally impossible to double-mint.
Implemented + PROVEN on real block execution (C-Chain half)
Files: evm/core/fee_split.go (helper), evm/core/state_transition.go (seam),
evm/params/extras/config.go (FeeRewardVault, FeeSplitTimestamp, IsFeeSplit,
compat, description). Tests (all pass, CGO_ENABLED=0 go test ./core/ ./params/extras/):
core/fee_split_test.go— split math, conservation, odd-wei rule, determinism (identical post-state root), legacy-path preserved.params/extras/fee_split_test.go— activation-gate boundary + compat freeze.core/fee_split_block_test.go— end-to-end on produced+accepted blocks: 10 LUX genesis, 5 transfers @ 21000 gas × 225 gwei ⇒- blackhole
0x0100…0000balance = 0 (stuck behavior replaced) - vault
0x0100…0002= 11,812,500,000,000,000 wei (exactly 50%) - BURNED = 11,812,500,000,000,000 wei — supply 10e18 → 9.9881875e18 (real decrease)
- burn == vault (exact 50/50 on even fees); sender debit == value + fee. Conservation exact.
- blackhole
P-Chain fold-in (designed; scoped — the larger, consensus-critical lift)
The reward half must reach P-Chain validators as staking reward. Chosen model R1 (move, not mint) — simplest correct, conservation by construction:
- Periodic atomic export of the
FeeRewardVaultbalance C→P (Avalanche/Lux shared-memory export/import — the native conservation-exact value-move; today exports are user-initiated, so a system-triggered epoch export is new machinery: a hook in the C-ChainFinalizeAndAssemble/atomic path emitting an export UTXO to a canonical P-Chain fee-reward-pool address, draining the vault). - P-Chain fee-reward pool — new persisted state (
vms/platformvm/state:Get/SetFeeRewardPool) funded by the import. (New persisted field ⇒ state serialization/migration ⇒ consensus-critical; must be gated + migration-tested.) - Distribution — at
RewardValidatorTxcommit (vms/platformvm/txs/executor/proposal_tx_executor.gorewardValidatorTx, ~line 285 wherereward := validator.PotentialReward), add the validator's pro-rata share of the pool (deterministic: by stake-weight over the accrual epoch) to the payout UTXO — unifying it into the native staking-reward path with no second mint. - Supply sync — decrement P-Chain
currentSupplyby the epoch's burnBso the reward curve (vms/platformvm/reward/calculator.go, driven byremainingSupply = SupplyCap − currentSupply) sees true post-burn supply.
Rejected alternative R2 (burn-on-C + re-mint-on-P): same net supply, but needs a trustless cross-chain counter read and an exact burn==mint match (a double supply-touch = double-mint/loss risk). Violates "no double-mint, simplest correct".
Interpretation flag for owner: "unifies into the staking-reward mint" is realized as unifying into the staking-reward payout, funded by moved fees, not a second mint — the conservation-safe reading. Confirm before P-Chain build.
Rollout (gated — do NOT skip a stage)
- Devnet 96367 — build the C-Chain plugin via ARC CI (never local docker),
deploy to the 5-validator devnet, set
feeSplitTimestampin the C-Chain upgrade config to a near-future time. Send txs; verify via RPC on all 5 nodes:eth_getBalance(0x0100…0002)grows by Σfee/2;eth_getBalance(0x0100…0000)stays flat; per-txgasUsed+effective price ⇒ expected burn; identical state root / block hash at each height across all 5 (determinism); capture before/after + tx hashes. (Unit + block-level tests above already prove the invariants deterministically.) - Testnet — same config, longer soak; confirm no fork, supply-decrease trend.
- Mainnet 96369 — ONLY after owner go: add
feeSplitTimestamp(future) to the C-Chain upgrade config via a dated fork tx (not a genesis edit). Separate owner decision: sweep vs. burn the ~3,778 LUX already stuck at0x0100…0000.
Guardrails honored: luxfi packages only; patch-semver; ARC CI builds (local builds here were unit-tests only); deterministic; supply conserved exactly.
Key Implementation Details
Context Management
- VM uses
context.Contextinstead of consensus.Context struct - Access consensus data via helper functions:
consensus.GetChainID(ctx)consensus.GetNetworkID(ctx)consensus.GetNodeID(ctx)consensus.GetLogger(ctx)consensus.GetWarpSigner(ctx)
Interface Compatibility
- Block implements both
chain.Blockandconsensuschain.Block - BuildBlock returns
consensuschain.Blockfor compatibility - AppSender uses
set.Set[ids.NodeID]for node sets - Version uses
consensus/version.Applicationnot node's version
Package Dependencies
NEVER use these packages:
- ❌
github.com/ethereum/go-ethereum- Usegithub.com/luxfi/geth - ❌
github.com/ava-labs/*- Usegithub.com/luxfi/* - ❌
github.com/luxfi/node v1.16.x- Use v1.13.4 for compatibility
Always use:
- ✅
github.com/luxfi/consensus- Local consensus package - ✅
github.com/luxfi/crypto- Local crypto package - ✅
github.com/luxfi/warp- Local warp package - ✅
github.com/luxfi/geth- Our Ethereum fork
Build Commands
cd /home/z/work/lux/evm
go build ./... # Currently fails due to interface issues
go test ./... # Will work after build issues are resolved
Common Issues and Fixes
-
Version Requirements
- Always use node v1.16.15 or later Lux versions
- Never use ava-labs packages
- Check go.mod replace directives
-
Interface Compatibility
- Block needs SetStatus method (even if no-op)
- BuildBlock must return consensuschain.Block
- Context is context.Context, not a struct
-
Missing Metrics
- Metrics registration is currently disabled (TODO)
- Will be re-enabled when consensus context supports it
-
ID Type Conversions
// Convert between node's IDs and consensus IDs func nodeIDToConsensus(id nodeids.NodeID) ids.NodeID { var consensusID ids.NodeID copy(consensusID[:], id[:]) return consensusID }
Testing
- Run with
-shortflag for quick tests - 28 packages with tests, 14 without (expected)
- Tests will pass after build issues are resolved
Important Notes
- This module is actively being migrated from evm
- Maintains backwards compatibility with existing Lux L2 chains
- Uses single validator POA for development (k=1 consensus)
- Major refactoring needed to reconcile ID type differences between packages
Documentation Status (2025-11-12)
✅ Documentation Enhanced
Successfully created comprehensive documentation for the Lux EVM implementation.
Documentation Created
- Enhanced index.mdx (
/Users/z/work/lux/evm/docs/content/docs/index.mdx)- Complete EVM overview and architecture
- Key differences from standard EVM
- Smart contract deployment guide
- Gas optimization strategies
- Comprehensive API reference (eth, web3, net, admin, debug, validators, warp)
- Integration with Lux blockchain
- Performance tuning configuration
- Security best practices
- Troubleshooting guide
- Migration guides from Ethereum and C-Chain
Documentation Features Added
- Architecture Section: VM, Core, Precompiles detailed
- API Reference: 40+ JSON-RPC endpoints documented
- Code Examples: JavaScript, Solidity, configuration files
- Performance Guide: State management, transaction pool, benchmarking
- Security Guide: Access control, gas limits, cross-chain security
- Troubleshooting: Common issues and debug commands
- Migration Guides: From Ethereum and C-Chain
Build Status
- ✅ Documentation site builds successfully
- ✅ Next.js 16.0.1 with Turbopack
- ✅ Static site generation working
- ✅ All pages render correctly
Completeness Score: 95/100
What's Complete
- ✅ Overview and introduction (100%)
- ✅ Architecture documentation (100%)
- ✅ API reference (100%)
- ✅ Smart contract deployment (100%)
- ✅ Gas optimization (100%)
- ✅ Integration guide (100%)
- ✅ Performance tuning (100%)
- ✅ Security considerations (100%)
- ✅ Troubleshooting (100%)
- ✅ Migration guides (100%)
What Could Be Added (5%)
- Additional code examples for each precompile
- Detailed tutorials for specific use cases
- Video documentation links
- Interactive API explorer
- Benchmark results and graphs
Precompiled Contracts Available
- DeployerAllowList - Contract deployment permissions
- FeeManager - Dynamic fee configuration
- NativeMinter - Native token minting
- RewardManager - Validator rewards
- TxAllowList - Transaction permissions
- Warp - Cross-chain messaging
- PQCrypto - Post-quantum cryptography
- Quasar - Advanced consensus features
Readonly Database Support (2025-11-22)
Status: ✅ VERIFIED WORKING
Successfully implemented and verified readonly database access for legacy PebbleDB databases.
Key Changes
-
Database Factory Fix (
~/work/lux/database/factory/pebbledb.go)- Added
readOnly boolparameter tonewPebbleDB()function - Passes readonly flag to
pebbledb.New()instead of hardcodedfalse - Committed to database repo (commit aaee95a)
- Added
-
EVM Integration (
~/work/lux/evm/go.mod)- Added replace directive:
replace github.com/luxfi/database => ../database - Enables EVM to use local database with readonly fix
- Added replace directive:
-
Test Verification (
test-readonly-db.go)- Successfully opens 7.1GB legacy PebbleDB in readonly mode
- Can read all keys without write access
- No corruption or modification risk
Legacy Database Details
- Location:
/Users/z/work/lux/state/chaindata/lux-mainnet-96369/db/pebbledb - Size: 7.1GB (751 files)
- Blockchain ID:
dnmzhuf6poM6PUNQCe7MWWfBdTJEnddhHRNXz2x7H6qSmyBEJ - Chain ID: 96369
- Purpose: Legacy evm data for regenesis export
Correct Migration Approach
IMPORTANT: Do NOT manually migrate database files between formats.
The proper workflow using lux-cli and VM interfaces:
- Deploy L2: Use
lux l2 createandlux l2 deployto create a Net - Export Data: Use VM's exporter interface via lux-cli export commands
- Import to C-Chain: Use VM's importer interface via
lux migrate import
VM Importer/Exporter Interface
Each VM must implement:
- Exporter Interface: Serialize blockchain state to portable format
- Importer Interface: Deserialize and load blockchain state
- Format: VM-agnostic, standardized data structure
Current lux-cli Status
The lux migrate command exists but is incomplete:
lux migrate prepare- Placeholder, needs migration-tools implementationlux migrate import- Not yet implementedlux migrate bootstrap- Partial implementationlux migrate validate- Not implemented
Implementation Needed
To complete the migration workflow:
-
VM Exporter (
plugin/evm/export.go):func (vm *VM) Export(ctx context.Context) ([]byte, error) { // Export blockchain state to standardized format // Include: genesis, blocks, state trie, metadata } -
VM Importer (
plugin/evm/import.go):func (vm *VM) Import(ctx context.Context, data []byte) error { // Import blockchain state from standardized format // Validate and load into C-Chain database } -
lux-cli Migration Tools:
- Implement
migration-tools/migrate.gothat calls VM exporter/importer - Complete
lux migrate prepareto use VM interfaces - Implement
lux migrate importfor C-Chain import
- Implement
Architecture Principles
- ✅ Use VM's native export/import interfaces
- ✅ Let each VM handle its own data format
- ✅ Generic migration via standardized interfaces
- ❌ NO manual database file copying
- ❌ NO format-specific conversion scripts
- ❌ NO direct database manipulation outside VM
Current Implementation Status (2025-11-23)
✅ Fixed:
- Import paths updated to use
luxfi/gethinstead ofgo-ethereum - Import paths updated to use
luxfi/idsinstead ofluxfi/node/ids - Chainmigrate interfaces.go fixed with correct imports
- Duplicate ChainMigrator definition resolved (renamed struct to Migrator)
- Broken implementation files disabled (.go.broken extension)
- Package consistency verified - all luxfi packages used correctly
📦 Required Package Imports:
- Ethereum types:
github.com/luxfi/geth(NOT go-ethereum) - IDs:
github.com/luxfi/ids(NOT luxfi/node/ids) - Logging:
github.com/luxfi/log(ALWAYS use luxfi/log for consistency) - Chainmigrate:
github.com/luxfi/node/chainmigrate
🔄 In Progress:
- Fixing exporter.go to match actual VM structure
- Need to access NetworkID from chainCtx, not config
- Need to find correct method for GetTd (total difficulty)
- Need to create proper error types (ErrMissing, ErrNotImplemented)
- Need to convert uint256.Int to *big.Int for balance
⏳ Next Steps:
- Complete exporter.go fixes to compile successfully
- Create importer.go implementation
- Test export functionality with readonly database
- Create migration-tools in lux-cli that use these interfaces
- Complete
lux migratecommand implementation - Test full export → import workflow
- Verify C-Chain can serve exported data via RPC
lux-cli Integration (2025-11-23)
✅ COMPLETE: ChainExporter integrated with lux-cli
Integration Architecture:
lux-cli migrate
↓
migration-tools/migrate (symlink)
↓
node/cmd/chainmigrate/chainmigrate (binary)
↓
node/chainmigrate/interfaces.go (ChainExporter interface)
↓
evm/plugin/evm/exporter.go (implementation)
CLI Tool Location:
- Binary:
/Users/z/work/lux/node/cmd/chainmigrate/chainmigrate - Symlink:
/Users/z/work/lux/cli/migration-tools/migrate
Usage via lux-cli:
lux migrate prepare \
--source-db ~/.node/chaindata/subnet-96369/db/pebbledb \
--output ./mainnet-migration \
--network-id 96369 \
--validators 5
Direct Binary Usage:
node/cmd/chainmigrate/chainmigrate \
--src-pebble /path/to/source/db \
--dst-leveldb /path/to/dest/db \
--chain-id 96369 \
--start-block 0 \
--end-block 1000 \
--batch-size 100
Features:
- ✅ Uses luxfi/log for logging
- ✅ Uses luxfi/geth for Ethereum types
- ✅ Uses ChainExporter interface
- ✅ Configurable batch sizes
- ✅ Block range selection
- ✅ Export-only and import-only modes
Integration Tests: All passing ✅
Next Steps:
- Complete full EVM integration (initialize VM with readonly DB)
- Implement importer.go for destination chain
- Test end-to-end export → import workflow
Integration Approach - RPC-BASED (2025-11-23)
✅ RPC Control: lux-cli uses netrunner + RPC only
Previous Approaches (WRONG): ❌
- Created ad-hoc cmd/chainmigrate binary in node repo
- Used symlinks to bridge binaries
- Used ChainExporter interface as Go import in lux-cli
- Direct Go package dependencies
Current Approach (CORRECT): ✅
- lux-cli: RPC client for fleet control
- netrunner: Deploys and manages node fleet
- EVM MigrateAPI: RPC endpoints for export/import
- NO Go package imports between cli and evm
- Pure RPC communication only
Architecture:
lux-cli (RPC client)
↓ HTTP JSON-RPC calls
netrunner (fleet manager)
↓ deploys nodes
EVM node (with MigrateAPI)
↓ migrate_getBlocks
↓ migrate_importBlocks
Database (PebbleDB/LevelDB)
Implementation:
// lux-cli/cmd/migratecmd/utils.go
func runMigration(sourceRPC, destRPC string, chainID int64) error {
// Get current block via RPC
blockNum, err := getCurrentBlock(ctx, sourceRPC)
// Call migrate_getBlocks via RPC (no Go imports!)
req := &RPCRequest{
Method: "migrate_getBlocks",
Params: []interface{}{0, blockNum, 100},
}
callRPC(sourceRPC, req, &blocks)
// Import via RPC to destination
req = &RPCRequest{
Method: "migrate_importBlocks",
Params: []interface{}{blocks},
}
callRPC(destRPC, req, &result)
}
EVM RPC Endpoints:
// plugin/evm/api_migrate.go
type MigrateAPI struct {
vm *VM
}
// RPC: migrate_getChainInfo
func (api *MigrateAPI) GetChainInfo() (*ChainInfo, error)
// RPC: migrate_getBlocks (batch, max 100 blocks)
func (api *MigrateAPI) GetBlocks(start, end, limit uint64) ([]*BlockData, error)
// RPC: migrate_streamBlocks (streaming via channels)
func (api *MigrateAPI) StreamBlocks(start, end uint64) (chan *BlockData, chan error)
// RPC: migrate_importBlocks
func (api *MigrateAPI) ImportBlocks(blocks []*BlockData) (int, error)
Benefits:
- True fleet control via RPC (lux-cli controls remote nodes)
- No Go package coupling between repos
- Can control nodes anywhere (local, remote, cloud)
- Netrunner handles deployment, lux-cli handles orchestration
- Works with any number of nodes
- Clean separation: deploy vs control vs execution
Workflow:
-
Deploy source EVM with netrunner (readonly DB):
netrunner engine start evm-source --data-dir=/readonly/db -
Deploy destination C-Chain with netrunner:
netrunner engine start c-chain -
Run migration via lux-cli (auto-discovers RPC endpoints):
lux migrate prepare # RPC endpoints discovered from netrunner at runtime # Source: ext/bc/<blockchain-id>/rpc (old 96369 net) # Dest: ext/bc/C/rpc (C-Chain) # Internal RPC uses port 9630 (not 9650) # Hosts/ports known at runtime, not hardcoded
RPC Path Format:
- C-Chain:
ext/bc/C/rpc(uses C alias) - Old 96369 Net:
ext/bc/dnmzhuf6poM6PUNQCe7MWWfBdTJEnddhHRNXz2x7H6qSmyBEJ/rpc(uses blockchain ID)
MigrateAPI Registration Status (2025-11-23)
✅ COMPLETE: MigrateAPI Registered with EVM Node
The MigrateAPI has been successfully registered with the EVM node and is now available via RPC.
Changes Made:
- Added
MigrateAPIEnabledconfig flag toplugin/evm/config/config.go - Set default to
trueinplugin/evm/config/default_config.go - Registered MigrateAPI in
plugin/evm/vm.go(similar to WarpAPI) - Fixed type errors in
plugin/evm/api_migrate.go:- Changed
Transactionsfield from[]types.Transactionto[]*types.Transaction - Fixed
WithBodycall to usetypes.Bodydirectly
- Changed
Available RPC Methods:
migrate_getChainInfo- Returns blockchain metadata (chain ID, network ID, current height, etc.)migrate_getBlocks- Exports blocks in batches (max 100 blocks per call)migrate_streamBlocks- Streams blocks via channels (not yet exposed via JSON-RPC)migrate_importBlocks- Imports blocks to the blockchain
Configuration:
{
"migrate-api-enabled": true // Default: true
}
Testing Status:
- ✅ EVM plugin builds successfully
- ✅ MigrateAPI properly registered in RPC handler
- ✅ CLI commands (export-data, import-data) implemented
- ⏳ End-to-end RPC testing pending
Next Steps:
- Deploy EVM node with readonly database
- Test
migrate_getChainInfoRPC call - Test
migrate_getBlockswith various block ranges - Test full export → import workflow via lux-cli
- Verify imported data on destination chain
Properties this EVM holds
No beacon chain. Lux networks run their own consensus, so the Cancun-era EIP-4844 beacon fields are not part of a Lux EVM block and the blob-fee path is not reachable on a fork that does not enable it.
A genesis file has to reproduce the chain's block 0. For a chain being brought up against existing history, the genesis hash computed from genesis.json must equal the original block 0 hash exactly; import checks it rather than assuming it, because a genesis that differs describes a different chain.
Importing history is not a read-only operation. admin_importChain moves the
chain database underneath everything reading it, so it runs against a node that
is not concurrently serving those reads.
Post-Quantum Cryptography Precompiles (2025-12-24)
Status: ✅ COMPLETE - All PQ Crypto Precompiles Implemented and Tested
Lux EVM includes native precompiled contracts for NIST FIPS 203-205 post-quantum cryptography algorithms.
Precompile Addresses (LP-aligned)
| Precompile | Address | Description |
|---|---|---|
| PQCrypto Unified | 0x0000000000000000000000000000000000012201 |
All PQ crypto operations (P=2 PQ/Identity) |
| ML-DSA Verify | 0x0000000000000000000000000000000000012202 |
Dedicated ML-DSA verification |
| SLH-DSA Verify | 0x0000000000000000000000000000000000012203 |
Dedicated SLH-DSA verification |
Gas Costs (Per Mode)
ML-DSA Signature Verification (FIPS 204)
| Mode | Security | Mode Byte | Gas Cost |
|---|---|---|---|
| ML-DSA-44 | Level 2 | 0x44 |
75,000 |
| ML-DSA-65 | Level 3 | 0x65 |
100,000 |
| ML-DSA-87 | Level 5 | 0x87 |
150,000 |
ML-KEM Key Encapsulation (FIPS 203)
| Mode | Security | Mode Byte | Encap Gas | Decap Gas |
|---|---|---|---|---|
| ML-KEM-512 | Level 1 | 0x00 |
6,000 | 6,000 |
| ML-KEM-768 | Level 3 | 0x01 |
8,000 | 8,000 |
| ML-KEM-1024 | Level 5 | 0x02 |
10,000 | 10,000 |
SLH-DSA Signature Verification (FIPS 205)
| Mode | Hash | Security | Mode Byte | Gas Cost |
|---|---|---|---|---|
| 128s | SHA-256 | Level 1 | 0x00 |
50,000 |
| 128f | SHA-256 | Level 1 | 0x01 |
75,000 |
| 192s | SHA-256 | Level 3 | 0x02 |
100,000 |
| 192f | SHA-256 | Level 3 | 0x03 |
150,000 |
| 256s | SHA-256 | Level 5 | 0x04 |
175,000 |
| 256f | SHA-256 | Level 5 | 0x05 |
250,000 |
| 128s | SHAKE | Level 1 | 0x10 |
50,000 |
| 128f | SHAKE | Level 1 | 0x11 |
75,000 |
| 192s | SHAKE | Level 3 | 0x12 |
100,000 |
| 192f | SHAKE | Level 3 | 0x13 |
150,000 |
| 256s | SHAKE | Level 5 | 0x14 |
175,000 |
| 256f | SHAKE | Level 5 | 0x15 |
250,000 |
Implementation Files
precompile/contracts/
├── mldsa/
│ ├── contract.go # ML-DSA precompile (182 lines)
│ ├── contract_test.go # 334 lines, 10 test cases
│ └── module.go # Registration
└── pqcrypto/
├── contract.go # Unified PQ precompile (382 lines)
├── contract_test.go # 234 lines, 20 test cases
├── module.go # Registration
└── config.go # Configuration
Mode Byte Encoding
Critical: Precompile mode bytes differ from library internal values:
// Precompile mode bytes (used in input)
ModeMLDSA44 uint8 = 0x44 // Library: mldsa.MLDSA44 = 0
ModeMLDSA65 uint8 = 0x65 // Library: mldsa.MLDSA65 = 1
ModeMLDSA87 uint8 = 0x87 // Library: mldsa.MLDSA87 = 2
The precompile implementation converts between these formats in the Run() method.
Function Selectors (PQCrypto Unified)
| Selector | Bytes | Operation |
|---|---|---|
"mlds" |
0x6d6c6473 |
ML-DSA Verify |
"encp" |
0x656e6370 |
ML-KEM Encapsulate |
"decp" |
0x64656370 |
ML-KEM Decapsulate |
"slhs" |
0x736c6873 |
SLH-DSA Verify |
Test Status
=== ML-DSA Tests ===
TestMLDSAVerify_ValidSignature PASS
TestMLDSAVerify_InvalidSignature PASS
TestMLDSAVerify_WrongMessage PASS
TestMLDSAVerify_InputTooShort PASS
TestMLDSAVerify_EmptyMessage PASS
TestMLDSAVerify_LargeMessage PASS
TestMLDSAVerify_GasCost PASS
TestMLDSAPrecompile_Address PASS
=== PQCrypto Tests ===
TestPQCryptoPrecompile PASS
TestMLDSAVerify PASS
TestMLKEMEncapsulateDecapsulate PASS
TestSLHDSAVerify PASS
TestGasCalculation (15 subtests) PASS
Total: 20 tests, 0 failures
Documentation
Full specification documented in:
- LP-3520: Post-Quantum Cryptography Precompile Implementation Guide
- LP-4200: Post-Quantum Cryptography Suite for Lux Network
- LP-3502: ML-DSA Post-Quantum Signature Precompile
Dependencies
github.com/luxfi/crypto/mldsa- ML-DSA implementation (FIPS 204)github.com/luxfi/crypto/mlkem- ML-KEM implementation (FIPS 203)github.com/luxfi/crypto/slhdsa- SLH-DSA implementation (FIPS 205)- Backend: Cloudflare CIRCL (audited, FIPS compliant)
Pluggable Backends + Auto-Import
Backend selection (build tags)
| Build | Backend | Notes |
|---|---|---|
go build ./plugin |
Go EVM (Block-STM) | Default. Pure Go. |
-tags gpu |
Go EVM + Metal GPU bridge | core/parallel/gpu_bridge.go. Requires working luxfi/gpu module. Currently broken upstream — MLX bindings (mlx_zeros, mlx_from_slice_int64, mlx_floor, mlx_full, etc.) are stubs in ~/work/luxcpp/mlx-c-api/mlx_c_api.c returning NULL with mismatched signatures. Don't use until upstream lands real MLX impls. |
-tags cevm |
C++ EVM via cgo | core/parallel/backend_cevm.go. Real GPU path — links ~/work/luxcpp/cevm/build-phase5b/lib/{libevm,libevm-gpu-state,libevm-metal-hosts,libevm-kernel-metal}. Requires bash chains/evm/cevm/fetch-luxcpp.sh to populate libs. |
-tags revm |
Rust EVM via FFI | core/parallel/backend_revm.go. Requires librustc_revm.a from luxfi/revm. |
gpu_bridge.go:4 build tag is cgo && darwin && gpu (opt-in).
--import-chain-data plumbing
luxd's --import-chain-data=<path.rlp> flag now flows through to the EVM plugin:
node/config/config.go:2012-2028config bridge injects the path intonodeConfig.ChainConfigs["C"].ConfigJSON map.- EVM plugin reads
vm.config.ImportChainData(declared inplugin/evm/config/config.go, JSON keyimport-chain-data). - After
initializeChain()inplugin/evm/vm.go::Initialize(), ifImportChainData != "", callsimportBlocksFromFile(chain, path, persistAccepted)fromplugin/evm/admin_api.go:220. Same code path asadmin_importChainRPC. - Batches of 2,500 blocks. State trie committed every
CommitInterval(default 4,096) blocks;acceptedBlockDBupdated atomically with each commit.
Canonical mainnet hashes/state root and required precompiles live in
~/work/lux/genesis/LLM.md "Canonical Requirements" — single source of truth.
Build commands
cd ~/work/lux/evm
# Default: builds and installs the plugin at the canonical path
# (~/.lux/plugins/<EVMID> where EVMID = CB58(ids.ID{'e','v','m'}) per luxfi/constants.EVMID).
# Handles codesign on darwin in one shot. No hardcoded plugin paths, no aliases.
./scripts/build.sh
# Variants (build, then have luxd point its --plugin-dir at the chosen build):
./scripts/build.sh /tmp/evm-default # plain Go EVM (default)
GOFLAGS='-tags cevm' ./scripts/build.sh /tmp/evm-cevm # C++ backend (real GPU)
GOFLAGS='-tags revm' ./scripts/build.sh /tmp/evm-revm # Rust REVM backend
The plugin filename is the binary's location: luxd's chain manager looks up
<plugin-dir>/<VMID> and constants.EVMID = ids.ID{'e','v','m'} (CB58 =
mgj786NP…) is the canonical primary-network C-Chain ID. Same ID is used
for every EVM-based L2 chain in Lux. No second filename, no aliases.
Known upstream gaps
luxfi/[email protected]MLX C bindings unimplemented — stubs in~/work/luxcpp/mlx-c-api/mlx_c_api.c. Don't use-tags gpuuntil landed.luxfi/[email protected]API drift —accel.VMSession,NewVMSession,WithPriority,PriorityHighundefined. Blockschains/keyvmand likely other chains/*vm. Independent of EVM stack.luxfi/corethis DEPRECATED percoreth/DEPRECATED.md— use this repo (luxfi/evm) for all new C-Chain and L1 EVM work.