Skip to content

feat(genesis): emit v2 (257-byte) batch header at bootstrap#970

Closed
curryxbo wants to merge 29 commits into
feat/sequencer-finalfrom
feat/genesis-batch-v2
Closed

feat(genesis): emit v2 (257-byte) batch header at bootstrap#970
curryxbo wants to merge 29 commits into
feat/sequencer-finalfrom
feat/genesis-batch-v2

Conversation

@curryxbo
Copy link
Copy Markdown
Contributor

@curryxbo curryxbo commented Jun 2, 2026

Summary

  • GenesisBatchHeader now emits a v2 (257-byte) header instead of v0 (249-byte).
  • Adds explicit lastBlockNumber field at offset 249 (= 0 for fresh genesis).
  • No Solidity changes: Rollup.importGenesisBatch already accepts v1/v2 layouts via _loadBatchHeader dispatch; v0/v1/v2 share the leading 249-byte layout so the V0-codec field reads in importGenesisBatch work uniformly; the ZERO_VERSIONED_HASH check on blobVersionedHash holds for v2's "no blob attached" sentinel.

Why

Every subsequent batch on a fresh chain uses the 257-byte v1/v2 layout — only batchIndex 0 was a v0 outlier. Tooling, audits, and downstream parsers had to special-case the genesis row without any payoff. Bumping genesis to v2 makes batch storage layout uniform across the chain.

Mainnet immutability

This change only affects genesis emission for fresh chains (devnet bootstrap, future mainnet redeploys). Chains already deployed with a v0 genesis keep their v0 committedBatches[0] permanently — no migration involved.

Test plan

  • go test ./ops/l2-genesis/morph-chain-ops/genesis/... — PASS (existing layer_two_test.go::TestBuildL2DeveloperGenesis exercises GenesisBatchHeader)
  • Sanity check generated bytes: len=257, version=2, blobVersionedHash=0x010657...4014 (ZERO marker), postStateRoot populated, lastBlockNumber=0x0000000000000000
  • Wet devnet smoke: deploy on a fresh devnet, confirm Rollup.importGenesisBatch(headerBytes) succeeds and downstream commitBatch flows continue normally.
  • Confirm no regression in common/batch parsing (uses an independent v0 fixture for cache init test, unaffected).

🤖 Generated with Claude Code

allen.wu and others added 29 commits May 28, 2026 17:04
- L1Sequencer.sol: sequencerHistory[], updateSequencer, getSequencerAt, initializeHistory
- Bindings: updated ABI for new contract interface
- SequencerVerifier: L1 history cache with interval cursor optimization
- Signer: simplified interface (removed IsActiveSequencer)
- 022-SequencerInit.ts: fixed initialize call (1 param instead of 2)
- Docker: added L1_SEQUENCER_CONTRACT env for all nodes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- run-test.sh: added build-malicious and p2p-test commands
- docker-compose.override.yml: malicious-geth-0 and malicious-node-0 services
- Tests: T-01~T-05 (active attacks) + T-06 (BlockSync pollution) + T-07 (resilience)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix grep -c multiline: use || true instead of || echo "0"
- Fix env var loss: malicious override must include full env list
- Swap approach: reuse synced sentry instead of fresh malicious container
- Uncomment CONSENSUS_SWITCH_HEIGHT for V2 mode activation
- Add SEQUENCER_PRIVATE_KEY to node-0 override

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use staking key (0xd998...) as SEQUENCER_PRIVATE_KEY for node-0
- Add initializeHistory() call in setup to register sequencer on L1
- Fixes "no sequencer record" error in V2 mode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- T-06: use blocksync-forge (blocksync/reactor.go) instead of sync-forge
  (broadcast_reactor.go) - targets the actual V1 vulnerability path
- T-06: stop node-3 to create gap, restart to trigger BlockSync
- Phase 0: explicit checks for V2 mode, signer, and switch height
- T-04: use futureHeight (currentHeight+10000) for deterministic unsolicited test
- Separate log files per phase to prevent cross-contamination

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add L1Sequencer.t.sol: 27 Foundry tests covering initialize,
  initializeHistory, updateSequencer, getSequencerAt binary search
  edge cases, and access control
- Regenerate l1sequencer.go with abigen (bytecode now matches current
  contract with sequencerHistory[], binary search, etc.)
- Update verifier.go: L1SequencerHistoryRecord -> L1SequencerSequencerRecord
- Add exponential backoff retry (10s -> 20s -> ... -> 5min) when initial
  history load fails, instead of waiting full 5 minutes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avoids stuttering in abigen output (L1SequencerSequencerRecord ->
L1SequencerHistoryRecord). No ABI/storage layout change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…om L1 contract

Unify the upgrade height source: instead of a CLI flag / env var, the
verifier now sets upgrade.UpgradeBlockHeight from the first history
record fetched from the L1Sequencer contract.

- node/l1sequencer/verifier.go: call SetUpgradeBlockHeight on first
  successful history load (prev==0)
- node/cmd/node/main.go: remove ConsensusSwitchHeight flag read;
  require L1 Sequencer contract address
- node/flags/flags.go: delete ConsensusSwitchHeight flag definition
- docker-compose.override.yml: remove 5× MORPH_NODE_CONSENSUS_SWITCH_HEIGHT
- run-test.sh: remove set_upgrade_height function, add wait_for_l1_finalized
  to ensure L1 contract data is finalized before L2 nodes start

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These env var overrides (DEPOSIT_CONTRACT_ADDRESS, SYNC_START_HEIGHT)
and the malicious_geth_data volume should be managed via overlay/override
files, not by modifying the base compose file directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ncer

Add hakeeper module implementing a 3-node Raft cluster for sequencer HA.
The HA cluster provides automatic leader election, block replication, and
failover without changing the on-chain sequencer identity.

node/hakeeper/:
- HAService: wraps hashicorp/raft, implements SequencerHA interface
- Config: layered loading (defaults -> TOML file -> CLI flags -> resolve -> validate)
  supports auto-detection of server_id (hostname) and advertised_addr (local IP)
- BlockFSM: Raft FSM for block replication; onApplied callback drives geth apply
- leaderMonitor: gates block production behind Barrier to ensure log catch-up
- rpc/: JSON-RPC admin API (ha_leader, ha_clusterMembership, ha_addServerAsVoter,
  ha_removeServer, ha_transferLeader, ha_transferLeaderToServer)
  with HTTP middleware token auth on write operations

node/flags/flags.go:
- New flags: --ha.enabled, --ha.config, --ha.bootstrap, --ha.join,
  --ha.server-id, --ha.advertised-addr, --ha.rpc-token

node/cmd/node/main.go:
- initHAService(): init HA from flags/config when --ha.enabled is set
- Fix typed-nil interface bug: pass untyped nil when HA is disabled

node/sequencer/tm_node.go:
- Pass HA service to tendermint node setup

node/go.mod:
- Add hashicorp/raft v1.7.1, raft-boltdb/v2

ops/docker-sequencer-test/:
- docker-compose.ha-override.yml: 3-node Raft cluster config for devnet
- run-ha-test.sh: 29-case integration test suite (config, cluster, block
  production, failover, admin API, lifecycle)
- run-perf-test.sh: performance test harness

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire up the new engine_newL2BlockV2 API for reorg support:

- Executor.ApplyBlockV2 now returns (applied bool, err error) matching
  the updated L2Node interface; detects idempotent skips and reorgs
  using BlockNumber + BlockByNumber checks before calling NewL2BlockV2
- RetryableClient.NewL2BlockV2 wraps the new authclient method with
  exponential backoff retry; excludes WrongBlockNumberError and
  ParentNotFoundError from retry (permanent errors)
- RetryableClient.AssembleL2BlockV2 added for parent-hash-based block
  assembly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add BlockHashMismatchError and InvalidNextL1MsgIndexError to the
retryableError() exclusion list so the executor stops re-sending
invalid payloads back to geth.

Made-with: Cursor
… not in PBFT validator set

- Add Syncer()/SetSyncer() accessors to Executor for explicit syncer wiring
- Start L1 syncer eagerly in main.go for separated-deployment / HA sequencers
  that are not PBFT validators (lazy-init path never fires for them)
- Guard Syncer.Start() with atomic flag to prevent duplicate goroutines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Increase blockCh buffer from 200 to 1000 to reduce drops under load.
- Panic on nil onApplied callback in BlockFSM.Apply: this can only happen
  due to a programmer error (forgot to wire SetOnBlockApplied) and would
  otherwise silently succeed on the leader while followers diverge.
- gofmt: realign one-line method bodies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- go-ethereum: v0.0.0-20260508105911-56deb7072ae4
- tendermint: v0.0.0-20260508065906-9e56b04da3c8

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add explicit replace directives in every go.mod to override MVS,
because token-price-oracle indirectly required v1.10.14-..., which
caused all workspace modules to resolve to the older version and
miss new APIs (NewL2BlockV2, AssembleL2BlockV2, SetBlockTags,
MorphTxType, updated AssembleL2Block/NewL2Block signatures).

Drop the local-path replace block from go.work so the pinned
pseudo-version is actually used.

Follow-up: investigate the indirect dep that requires v1.10.14
and bump it so these per-module replaces can be removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull in the persistent-peer ban exemption + sigStore.Close fixes from
morph-l2/tendermint feat/sequencer-optimize (commit c6f7e21e4).

Updated via 'make update' after bumping TENDERMINT_TARGET_VERSION in
the Makefile. All sub-modules tidied. morphnode + tendermint binaries
build cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements SPEC-005 derivation verification:
- verify_local.go: Path B local-rebuild blob verification (rebuild
  blockContext + L2 tx blob from local chain, compare against on-chain
  Rollup batch)
- verify.go: extract verifyBatchRoots, gate stateException on real
  divergence verdicts (not transient errors)
- finalizer.go + reorg.go: derivation-driven finalizer + L1 reorg
  detection (SPEC-005 §4.7.6), rewind-and-reset for canonicality
- tag_advance.go + metrics.go: derivation-driven L2 tag management,
  structured failure diagnostics for Path B

Common: export common/batch.BuildBlockContext for derivation reuse.
go-ethereum: bump submodule to 045be0fdc7ca (NewL2BlockV2 self-heal).
Ops: add second sentry node for derivation validation (4-nodes compose,
node5 key, devnet setup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaced by derivation-driven tag management (see prior commit):
- node/validator/*: removed dedicated validator role
- node/blocktag/*: removed standalone block-tag advancer service
- node/cmd/node/main.go: drop validator/blocktag wiring
- node/flags/flags.go: drop validator-specific CLI flags
- ops-morph/docker-compose-validator.yml: drop validator compose file

Tags are now advanced inline by the derivation loop, eliminating the
extra service and the role-based branching in main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- sync/syncer.go: make Syncer.Start idempotent via sync.Once so
  re-entry on retry no longer leaks goroutines or races on state
- types/retryable_client.go (+test): treat ethereum.NotFound as a
  permanent failure rather than retrying forever
- db/keys.go + db/store.go: derivation-related key helpers used by
  the new finalizer / tag-advance paths

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Shared L1 client: main.go does a single ethclient.Dial and threads
  it into syncer, derivation, l1sequencer Tracker/Verifier/Signer, and
  the rollup binding. Reads l1.rpc directly from CLI flags instead of
  going through derivation.Config first.
* Derivation reorg: blob-hash mismatch now invokes the tendermint
  Node.StopReactorsBeforeReorg → fetch full batch → deriveForce →
  StartReactorsAfterReorg(post-reorg height) flow. HA-mode adds a
  hard-stop guard (cluster invariant violation; logs full context and
  returns instead of self-healing). Mock mode (d.node==nil) skips the
  reactor cycle.
* deriveForce uses the new NewL2BlockV2 (*Header, error) return; the
  redundant HeaderByNumber readback is gone, parent chains via the
  returned header. lastHeader is initialised from the batch's parent
  so it tracks the chain head end-to-end.
* Executor.ApplyBlockV2 + RetryableClient.NewL2BlockV2 updated for the
  new signature. Executor.updateSequencerSet no longer stops the
  syncer when this node ceases to be sequencer — derivation needs it
  running on every node.
* deps: bump tendermint to 6393e1eaad71 (derivation reorg API,
  StopReactorsBeforeReorg / StartReactorsAfterReorg) and go-ethereum
  to 5c5b433f18f2 (NewL2BlockV2 returns header, NextL1MsgIndex
  backfill on isSafe path so writeBlockStateWithoutHead's gate passes
  when callers don't know the per-block index). Replace directives
  for both grouped at the top of every go.mod for review locality.
* docker-sequencer-test: Dockerfile copies common/go.mod alongside
  the others (workspace replace requires it). HA override adds
  L1_ETH_BEACON_RPC env for ha-node-{0,1,2} (derivation refactor
  validates it at startup). run-ha-test.sh service names corrected
  from morph-geth-* / sentry-geth-0 to morph-el-* / sentry-el-0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the L1-derived block insertion path off of NewL2BlockV2(isSafe=true)
onto NewSafeL2Block, which now accepts SafeL2Data.ParentHash for non-head
parents and lets SetCanonical reorg the chain automatically. NewL2BlockV2
becomes sequencer-signed-only (caller supplies pre-computed execution
results, gate validates).

The previous isSafe=true path on NewL2BlockV2 wrote blocks with caller-
supplied StateRoot — for derivation.deriveForce that was the zero hash,
because L1 batch metadata only carries the batch-final PostStateRoot, not
per-block roots. The resulting blocks had header.Root=0 even though their
state was correctly executed and committed, breaking verifyBatchRoots
forever and blocking derivation cursor advance. NewSafeL2Block executes
internally and fills header.Root from stateDB.IntermediateRoot, so the
header on disk is consistent with the state.

* node/derivation/derivation.go: deriveForce builds SafeL2Data with
  ParentHash = lastHeader.Hash() and calls NewSafeL2Block instead of
  NewL2BlockV2(isSafe=true); safeL2DataToExecutable helper deleted.
* node/types/retryable_client.go + node/core/executor.go: drop isSafe
  arg from NewL2BlockV2.
* node/derivation/verify_local.go: outline path got a v0-parent compat
  shim — only reachable on test/devnet where genesis batch is v0 and V1
  is day-1 enabled, so the only v0 batch in the chain is genesis; on
  prod (V1 day-1, V2 layered on V1) the branch is dead. Reorg semantics
  only exist post-V2 anyway, so processing pre-V2 via outline is not a
  load-bearing path. Comment explains the assumption.
* deps: bump tendermint to b1b3a3a1d806 (drop dead reorg-restart test
  harness from Node) and go-ethereum to eb5fbf8f9748 (NewSafeL2Block
  ParentHash, drop NewL2BlockV2 isSafe).
* ops/docker-sequencer-test/Dockerfile.tx-submitter-test: new polyrepo-
  context Dockerfile so tx-submitter builds against local
  ../tendermint and ../go-ethereum siblings — the original
  ops/docker/Dockerfile.submitter only sees the morph repo and can't
  resolve workspace replace directives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: corey <corey.zhang@bitget.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plumb the morph node into the Nitro Enclave signer (separate
morph-enclave-signer repo) so the sequencer can sign blocks without
ever holding the plaintext key. Two flags are mutually exclusive:

  --sequencer.privateKey         existing in-process LocalSigner
  --sequencer.enclaveSignerAddr  new EnclaveSigner over vsock (CID:port)

EnclaveSigner highlights:
  - vsock-only, mdlayher/vsock dep added
  - one persistent connection reused across Sign calls; dial wrapped
    in a goroutine race with dialTimeout (mdlayher/vsock has no
    context-aware Dial)
  - probe + signOnce both bounded by SetDeadline(requestTimeout) so a
    half-open or unresponsive enclave can't stall startup or hold
    Sign()'s mutex on reconnect
  - up to 3 retries per Sign call with reconnect on each failure,
    Error log on exhaustion; caller (tendermint commitBlock) is
    expected to treat that as fatal
  - identity self-test at construction: sign a 32-byte zero hash,
    recover the EVM address from the signature via secp256k1 ECDSA
    recovery, abort node startup if it disagrees with the address
    GetPubkey reported. Catches misconfig like wrong SECRET_ID baked
    into the .eif or vsock-proxy MITM swap before tendermint comes up.

Co-Authored-By: Claude Opus 4.7 (1M context) <<EMAIL_ADDRESS>>
Unify the address format with ops-cli, which already takes
`vsock:CID:port`. parseAddr now accepts both that form and the
legacy bare `CID:port` so existing systemd / k8s configs keep
working through the rollout.

- enclave_signer.go: TrimPrefix("vsock:") in parseAddr
- flags.go: Usage doc reflects both forms
- enclave_signer_test.go: cases for both forms + new error cases
  (vsock:16, vsock:abc:5000)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GenesisBatchHeader produced a v0 (249-byte) header, while every
subsequent commitBatch on a fresh chain went through v1/v2 (257-byte)
codec. Tooling and audit had to special-case batchIndex 0 as a v0
outlier; the storage layout was inconsistent without payoff.

Bump genesis to v2:
  - genesisBatchVersion 0 → 2
  - genesisBatchHeaderLength 249 → 257
  - write lastBlockNumber (=genesisHeader.Number, 0 for fresh chains)
    at offset 249

Rollup.sol's importGenesisBatch is already version-agnostic at the
loader (`_loadBatchHeader` dispatches v1/v2 to BatchHeaderCodecV1
which validates the 257-byte length); field reads use V0 codec
offsets that match across v0/v1/v2 for the leading 249 bytes; the
ZERO_VERSIONED_HASH check on blobVersionedHash holds for v2's
"no blob attached" sentinel. No Solidity changes required.

Mainnet immutability note: chains already imported with a v0 genesis
keep their v0 committedBatches[0] forever — this commit only changes
genesis emission for fresh chains (devnet bootstrap, future mainnets).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@curryxbo curryxbo requested a review from a team as a code owner June 2, 2026 08:14
@curryxbo curryxbo requested review from twcctop and removed request for a team June 2, 2026 08:14
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f74e0cbc-0567-445e-bd8b-b0e4b3ebf984

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/genesis-batch-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@curryxbo curryxbo force-pushed the feat/genesis-batch-v2 branch from 6a7e411 to 6948ba6 Compare June 2, 2026 09:27
@tomatoishealthy
Copy link
Copy Markdown
Contributor

Already merged into feat/sequencer-final using cherry-pick.

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.

2 participants