Skip to content

msf-1: mainnet shadowfork from the jochemnet benchmark snapshots - #92

Merged
qu0b merged 13 commits into
masterfrom
qu0b/msf-1
Oct 7, 2026
Merged

qu0b merged 13 commits into
masterfrom
qu0b/msf-1

Conversation

@qu0b

@qu0b qu0b commented Oct 7, 2026

Copy link
Copy Markdown
Member

Changes over the sepsf-2 copy (e14d8ea). Every EL restores benchmarkoor's
jochemnet/24402727 datadir instead of a canonical snapshot. jochemnet started
from the mainnet 24350000 snapshot and ran 1 s blocks of bloat state up to
24402727 (head ts 2026-01-31). Its head is not on mainnet.

Snapshots (snapshots.ethpandaops.io/jochemnet//24402727/), layouts
checked from the first 64 MB of each tarball:

  • geth/besu/nethermind snapshot.tar.zst, reth snapshot-v2.tar.zst (v2
    storage), erigon snapshot-pruned.tar.zst. Sizes 0.7-1.26 TB compressed, so
    the 1.8 TB droplets (DO's largest local disk) stay.
  • ethrex snapshot.tar.zst (0.3 TB) is new compared to sepsf. It extracts into
    /data/ethrex/chain-1, which is where ethrex 29 keeps a chain-id-1
    custom-genesis db. It runs --syncmode=full; the May db is migrated on start.
  • nimbus-el still has no snapshot and snap-syncs from our peers.
  • shadowfork.yaml: snapshot_fetcher_filename moves from play vars (which
    beat group_vars) to a template default, so it can be set per EL.

Genesis (egg 6.2.0, CHAIN_ID=1, mainnet deposit contract):

  • jochemnet publishes no _snapshot_eth_getBlockByNumber.json. The one inside
    the erigon tarball is the stale mainnet 24350000 block. SHADOW_FORK_FILE is
    therefore files/shadowfork_block.json, the full-tx eth_getBlockByNumber of
    a restored EL; TODO before playbook.yaml. Tested with a stand-in head: the
    real genesis role keeps osaka/bpo1/bpo2 at mainnet times and puts
    amsterdamTime at genesis + 40 epochs. genesis.ssz builds; a hash-only block
    makes eth2-testnet-genesis fail.
  • Mainnet has no EIP-8282 builder contracts, so the devnet-7/8
    deploy-eip8282-contracts assertoor startup test is back. It sends the same
    0x4e59 factory calldata as the sepolia deployment txs; checked with cast
    create2.

Clients:

  • nethermind: --config=mainnet, nethermind_db/mainnet, FlatDb (the
    tarball's mainnet/flat layout).
  • erigon: keeps --externalcl and --keep.stored.chain.config; db.size.limit
    is 2TB. The sepsf-2 --snap.stop host_vars are dropped: segments are frozen
    up to 24401000, so ~1.7k blocks are left to retire (sepsf-2 had 220k).
  • Static EL enodes reset (they named sepsf-2's hosts), plus a note to
    firewall EL p2p fleet-wide: mainnet peers have none of our blocks.

Other:

  • Images: nimbus v26.10.0, prysm v7.2.1, ethrex 29.0.1 (released).
  • terraform: VPC 10.32.0.0/16.
  • Secrets: the metrics/logs usernames and the xatu ingest address are now
    glamsterdam-msf-1.

Launch TODOs (marked TODO(msf-1)): genesis time, gloas_fork_epoch,
files/shadowfork_block.json, shadowfork_el_static_enodes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo

qu0b and others added 2 commits October 7, 2026 08:43
Changes over the sepsf-2 copy (e14d8ea). Every EL restores benchmarkoor's
jochemnet/24402727 datadir instead of a canonical snapshot. jochemnet started
from the mainnet 24350000 snapshot and ran 1 s blocks of bloat state up to
24402727 (head ts 2026-01-31). Its head is not on mainnet.

Snapshots (snapshots.ethpandaops.io/jochemnet/<client>/24402727/), layouts
checked from the first 64 MB of each tarball:
- geth/besu/nethermind snapshot.tar.zst, reth snapshot-v2.tar.zst (v2
  storage), erigon snapshot-pruned.tar.zst. Sizes 0.7-1.26 TB compressed, so
  the 1.8 TB droplets (DO's largest local disk) stay.
- ethrex snapshot.tar.zst (0.3 TB) is new compared to sepsf. It extracts into
  /data/ethrex/chain-1, which is where ethrex 29 keeps a chain-id-1
  custom-genesis db. It runs --syncmode=full; the May db is migrated on start.
- nimbus-el still has no snapshot and snap-syncs from our peers.
- shadowfork.yaml: snapshot_fetcher_filename moves from play vars (which
  beat group_vars) to a template default, so it can be set per EL.

Genesis (egg 6.2.0, CHAIN_ID=1, mainnet deposit contract):
- jochemnet publishes no _snapshot_eth_getBlockByNumber.json. The one inside
  the erigon tarball is the stale mainnet 24350000 block. SHADOW_FORK_FILE is
  therefore files/shadowfork_block.json, the full-tx eth_getBlockByNumber of
  a restored EL; TODO before playbook.yaml. Tested with a stand-in head: the
  real genesis role keeps osaka/bpo1/bpo2 at mainnet times and puts
  amsterdamTime at genesis + 40 epochs. genesis.ssz builds; a hash-only block
  makes eth2-testnet-genesis fail.
- Mainnet has no EIP-8282 builder contracts, so the devnet-7/8
  deploy-eip8282-contracts assertoor startup test is back. It sends the same
  0x4e59 factory calldata as the sepolia deployment txs; checked with cast
  create2.

Clients:
- nethermind: --config=mainnet, nethermind_db/mainnet, FlatDb (the
  tarball's mainnet/flat layout).
- erigon: keeps --externalcl and --keep.stored.chain.config; db.size.limit
  is 2TB. The sepsf-2 --snap.stop host_vars are dropped: segments are frozen
  up to 24401000, so ~1.7k blocks are left to retire (sepsf-2 had 220k).
- Static EL enodes reset (they named sepsf-2's hosts), plus a note to
  firewall EL p2p fleet-wide: mainnet peers have none of our blocks.

Other:
- Images: nimbus v26.10.0, prysm v7.2.1, ethrex 29.0.1 (released).
- terraform: VPC 10.32.0.0/16.
- Secrets: the metrics/logs usernames and the xatu ingest address are now
  glamsterdam-msf-1.

Launch TODOs (marked TODO(msf-1)): genesis time, gloas_fork_epoch,
files/shadowfork_block.json, shadowfork_el_static_enodes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
@redpandabot

This comment has been minimized.

qu0b and others added 3 commits October 7, 2026 09:51
…ates)

From a five-way review against the client release tags, plus local runs of
every EL image (2026-10-07).

Genesis:
- egg's mainnet genesis.json does not load as is. nethermind 2.1.0 fails on
  alloc keys without 0x, and besu 26.9.0 needs the EIP-7002/7251 request
  contract addresses. scripts/normalize-shadowfork-genesis.py fixes both; a
  play right after the genesis role runs it, for chain id 1 only. All seven
  ELs load the result with genesis hash 0xd4e567..cb8fa3 and our
  amsterdamTime.

Keep real mainnet out (chain id 1 has mainnet's fork id until amsterdamTime):
- erigon: --nodiscover (the only way to stop its mainnet DNS tree) and
  --no-downloader (jochemnet segments carry mainnet names). Tested on v3.7.1.
- reth: --disable-dns-discovery.
- nethermind: --Network.EnableEnrDiscovery=false and
  --Discovery.UseDefaultDiscv5Bootnodes=false.
- bootnode geth: explicit --bootnodes, so it no longer falls back to
  mainnet's.
- The restore deletes each tarball's p2p identity and saved peers (nodekey,
  nodes/, discovery-secret, known-peers.json, key, node.key,
  node_config.json, <net>/peers, <net>/discoveryNodes), so hosts restored
  from the same tarball don't share a node id.
- The DO firewall admits 30303 only from our droplets
  (el_p2p_extra_source_addresses for a local join host).
- The iptables script also matches --sport 30303 and reports failed or
  unreachable hosts. It runs right after init-server.

Wallets / tooling:
- fund-shadowfork.sh skips every EIP-7702-delegated wallet. mainnet's
  mnemonic-4 (faucet_private_key) is delegated to a sweeper.
- k8s: faucet disabled (that key) and homepage disabled (its MetaMask button
  offers chain id 1); faucet-agents stays.
- nimbus-el: --debug-snap-sync-resume=true; start only after Gloas
  finalizes, because v0.4.2 needs a post-Amsterdam pivot.

Gate:
- scripts/shadowfork-pregenesis-check.sh. genesis.json checks: 0x keys,
  request addresses, fork and blob keys, CL head hash. Per EL host: head
  and hash, eth_config next activation, unique node id, fleet-only peers,
  disk, erigon preverified.toml, ethrex metadata.json. Tested against a mock
  RPC with injected failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
76 droplets in total. NUMBER_OF_VALIDATORS stays 36000: terraform splits
each entry's 1000-key range over its 2 nodes (checked with terraform console:
contiguous 0..36000, all supernodes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
Replaces the 2-per-pair matrix (76 droplets). 3000 keys per node, 36000
total; every CL and EL client holds 1/6 of stake and no CL/EL pair repeats.
Names keep the hosts group_vars reference as EL bootnodes. Plus bootnode, 2
buildoor and 1 nimbus-el. Checked with terraform console.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo

@redpandabot redpandabot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

msf-1 shadows mainnet from the jochemnet benchmark snapshots: per-EL snapshot filenames, chain-id-1 shadowfork genesis with a committed block file, mainnet-specific EL peer isolation, a pre-genesis gate, and a regenerated 41-node inventory. The last commit aligns inventory.ini with the restored 36-validator nodes.tf layout and I verified all node names, validator ranges and terraform-computed regions match. The one open item is the still-absent shadowfork_block.json the genesis role requires.

Issues

  • 🟡 ansible/inventories/msf-1/group_vars/all/all.yaml:107 — shadowfork_block.json lookup targets a file not in the repo — see the thread on that line

Reviewed @ 6cbfefd5
"Weeks of coding can save you hours of planning."

export DEPOSIT_CONTRACT_ADDRESS=0x00000000219ab540356cBB839Cbe05303d7705Fa
export SHADOW_FORK_FILE=/config/shadowfork_block.json
# TODO(msf-1): commit files/shadowfork_block.json.
shadowfork_block.json: "{{ lookup('ansible.builtin.file', (ansible_inventory_sources[0] | dirname) ~ '/files/shadowfork_block.json') }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 shadowfork_block.json lookup targets a file not in the repo

This resolves to ansible/inventories/msf-1/files/shadowfork_block.json, but no files/ directory is tracked. The ethereum_genesis role consumes the dict via ethereum_genesis_generator_config_files | dict2items (roles/ethereum_genesis/tasks/generate_genesis.yaml), so the missing file lookup raises and aborts the genesis play before any host runs. It is marked TODO, but until the file is committed playbook.yaml cannot get past genesis.

Comment thread scripts/shadowfork-pregenesis-check.sh Outdated
fmt="${RPC_URL_FMT:-}"
[ -n "$fmt" ] || fmt="https://$user:$pass@rpc-{host}.srv.glamsterdam-$net.ethpandaops.io"
fleet="$(grep -oE 'ansible_host=[0-9.]+' "$inv" | cut -d= -f2 | sort -u)"
hosts="$(cd "$root/ansible" && ansible -i "$inv" 'ethereum_node:bootnode' --list-hosts 2>/dev/null | tail -n +2 | tr -d ' ')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 pre-genesis gate always fails on the nimbusel node

The gate queries ethereum_node:bootnode, which includes lighthouse-nimbusel-1. nimbusel has no snapshot, gets no static peers (shadowfork_el_static_enodes is empty) and its snap-sync needs a post-Amsterdam pivot (nimbusel.yaml), so it cannot be at shadowfork_height before genesis; the loop reports "no RPC answer" (line 59) or a head mismatch and exits 1. Exclude nimbusel from the check (or special-case it) or the gate can never pass as documented.

qu0b and others added 8 commits October 7, 2026 10:47
It has no snapshot and only starts after Gloas, so it cannot be at the
shadowfork head before genesis (redpandabot review). The disk check still
covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
41 droplets: full 6x6 CL/EL matrix (36 x 1000 validators), bootnode, 2
buildoor and 2 nimbus-el. Applied 2026-10-07 10:46/10:57 UTC (49+52
resources; the 12 earlier droplets got tag updates only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
bootnodoor rejects a genesis timestamp of 0 (mainnet's): the bootnode play
writes it a genesis.json copy with timestamp 1, which gives the same fork ids
(every mainnet time fork is later). Its EL genesis hash is now mainnet's
0xd4e5...8fa3; deposit_contract_block_hash.txt is the shadowfork head, so the
fork ids were wrong (prague 0xc70e321f instead of 0xc376cf8b).

Static enodes: the 27 geth/besu/reth/nethermind ELs from the first run's facts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
Genesis 2026-10-07 17:30:00 UTC (1791394200) on jochemnet block 24402727
(0x488010e5...5ba8); Gloas at epoch 40 = 21:46:00 UTC (amsterdamTime 1791409560).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
…e; genesis 19:00 UTC

benchmarkoor's jochemnet prestate (gas-benchmarks amsterdam-repricings-v5.2.0) is bal-devnet-7
Amsterdam payloads without EIP-8282: release geth v1.17.7 rejects block 24402728 with "empty
system contract: no code at 0x0000bFF4...8282". Rebuilt what it does to state, pre-Amsterdam,
on a restored geth via the engine API and imported it into all 39 ELs (all at 0x61c24011):

  24402728 (0xe87f3382): funding.txt's 15001 withdrawals (2^64-1 gwei) + 1 ETH deployer
  24402729 (0x61c24011): EIP-8282 deposit + exit contracts via the 0x4e59 factory

The 5000 empty gas-bump blocks are left out (gas limit already 1T, no state effect besides the
2935/4788 buffers). New SHADOW_FORK_FILE = 24402729; shadowfork_head_height for the gate and the
fund script (shadowfork_height stays the snapshot height). The 17:30 genesis output is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
The 30303 port match missed our ELs' outbound TCP dials (ephemeral source port) to real
mainnet nodes listening on other ports: msf-1 nethermind/geth peered with mainnet nodes on
30304/30404 once bootnodoor's fork ids were right. Jump every packet from the EL container's
IP to SF-P2P too (private/VPC destinations pass). Re-run after an EL container is recreated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
Genesis 2026-10-07 19:00:00 UTC (1791399600) on block 24402729 (0x61c24011...6d3d, jochemnet
24402727 + benchmark funding + EIP-8282 contracts); Gloas at epoch 40 = 23:16:00 UTC
(amsterdamTime 1791414960). Pre-genesis gate PASS on all 39 ELs at 17:54 UTC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnamtoTL7VJzozbP63tWdo
@qu0b
qu0b merged commit 61aefb8 into master Oct 7, 2026
2 checks passed
@redpandabot

redpandabot Bot commented Oct 7, 2026

Copy link
Copy Markdown

This re-review covers the msf-1 shadowfork setup: a mainnet/jochemnet snapshot restore, a two-block IP-imported prestate (funding withdrawals plus EIP-8282 deploys), mainnet-chain-id isolation (firewall/discovery/static peers), and bootnodoor/mainnet-genesis handling. Both previously open findings are fixed at HEAD: files/shadowfork_block.json is now committed and resolves on the lookup path, and the pre-genesis gate excludes nimbusel from its RPC checks (ethereum_node:bootnode:!nimbusel). I traced the prestate scripts, genesis normalizer, snapshot downloader identity stripping, and terraform firewall and found no new concrete defects.

Partial review: 136 of 139 changed files included. Skipped 3 file(s) whose individual diff exceeds 500000 bytes:

  • "ansible/inventories/msf-1/files/prestate/block1.json"
  • "network-configs/msf-1/metadata/genesis.json"
  • "network-configs/msf-1/parsed/parsedConsensusGenesis.json"

No findings in the reviewed scope; no whole-PR approval issued.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant