Runs one ethlambda consensus node paired with one ethrex execution node
over the Engine API. Every slot, ethlambda builds a block, asks ethrex to
produce the execution payload (engine_forkchoiceUpdatedV3 + getPayloadV4),
embeds it in the Lean block, and ethrex imports it (newPayloadV4). The chain
advances and finalizes on both layers.
Single validator → finalizes solo. Good for a quick local demo.
- ethrex on
PATH(v15+), orETHREX=/path/to/ethrex. - A dual-key (devnet5+) lean genesis bundle. By default the script looks in
lean-quickstart/local-devnet/genesis. Generate one with:(needs the lean-quickstartcd lean-quickstart && ./generate-genesis.sh local-devnet/genesis
mainbranch and Docker). To pay block rewards to a real address, add tovalidator-config.yaml'sconfigblock:suggested_fee_recipient: "0x00000000000000000000000000000000deadbeef"
cargo(the script buildsethlambdain release) unlessSKIP_BUILD=1.jq(optional, for the demo commands below).
scripts/engine-api-demo/run.sh # build + start ethrex and ethlambda
scripts/engine-api-demo/run.sh stop # stop bothConfigurable via env vars (see the header of run.sh): ETHREX,
LEAN_GENESIS_DIR, DATA_DIR, GENESIS_OFFSET, the four ports, SKIP_BUILD.
- EL importing CL-built payloads — chain climbing,
miner= the configuredsuggested_fee_recipient:curl -s -X POST http://127.0.0.1:8545 -H 'content-type: application/json' \ --data '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["latest",false]}' \ | jq '.result | {number, hash, miner}'
- ethlambda fork-choice tree (browser): http://127.0.0.1:5052/lean/v0/fork_choice/ui
- Both layers in lockstep:
tail -f "$DATA_DIR/ethlambda.log" | grep -E 'proposer|finalized|head updated' tail -f "$DATA_DIR/ethrex.log" | grep -E 'BLOCK|executed|Fork choice'
The round-trip invariant: ethrex's FCU head equals ethlambda's
block.body.execution_payload.block_hash.
ethrex builds payloads from its mempool, so anything submitted to its HTTP-RPC
lands in the next slot's payload — and therefore inside the Lean block. The
genesis prefunds the well-known hardhat/anvil dev account #0
(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, 10k ETH), and send-txs.sh
signs EIP-1559 self-transfers from it (via uv run --with eth-account; no
permanent install):
scripts/engine-api-demo/send-txs.sh 5 # sign + submit 5 transfersOne slot later (~4s), show the full tx → mempool → payload → Lean block → execution round-trip:
# The receipt: executed on the EL (status 0x1, note the block number N)
curl -s -X POST http://127.0.0.1:8545 -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<tx hash>"]}' \
| jq '.result | {status, blockNumber}'
# The same transactions, raw, inside the Lean block's execution payload at slot N
curl -s http://127.0.0.1:5052/lean/v0/blocks/<N> \
| jq '.body.execution_payload | {blockNumber, gasUsed, transactions}'Override RPC_URL / KEY via env to use a different endpoint or sender.
| File | Purpose |
|---|---|
run.sh |
Orchestrator (run / stop); reads the EL genesis hash from ethrex's log, so nothing is hardcoded. |
send-txs.sh |
Signs and submits demo transactions from the prefunded dev account (requires uv). |
genesis-el.json |
Execution-layer genesis: chainId 9, Shanghai/Cancun/Prague @0 (pre-Amsterdam → no EIP-7928 block-access-list), Prague system contracts + one prefunded dev account. |
- Fork level. ethlambda pins the V4 (Prague) Engine methods — the
pre-Amsterdam, no-BAL path. Current ethrex's
newPayloadV5requires the EIP-7928 block-access-list (Amsterdam, off by default), so the EL genesis here stops at Prague. Fork-aware version selection (and V5/BAL support) is a future refinement. run.shre-stampsGENESIS_TIMEin the leanconfig.yamlon each run.- Logs and data live under
DATA_DIR(default$TMPDIR/ethlambda-el-demo).