Skip to content

Commit 877fa44

Browse files
committed
merge sync-queue-hashes
1 parent 509e757 commit 877fa44

7 files changed

Lines changed: 37 additions & 13 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Key hard-won rules:
4848
For continuously proving mainnet bundles in real time (poll-syncing the mainnet DB into the shadow DB), three silent starvation traps apply — see `tests/shadow-testing/docs/TROUBLESHOOTING.md` Trap 22/23:
4949
- Poll sync must also maintain `l2_block.chunk_hash` links (UPDATE, not INSERT) and `chunk.batch_hash`/`batch.bundle_hash` parent links, or tasks become invisible to the coordinator with no ERROR logged.
5050
- `sweep-stale-proving.sh` must reset `total_attempts`, not just `proving_status` — the coordinator skips tasks with `total_attempts >= 5`.
51-
- Bundles popping L1 messages enqueued after the Anvil fork block fail with `VerificationFailed(0x439cc0cd)`: run `tests/shadow-testing/scripts/sync-queue-hashes.py` (cron, 10 min) to mirror `messageRollingHashes` + `nextCrossDomainMessageIndex` from mainnet onto the fork.
51+
- Bundles popping L1 messages enqueued after the Anvil fork block fail with `VerificationFailed(0x439cc0cd)`: `sync-mainnet-db.py` poll mode runs `tests/shadow-testing/scripts/sync-queue-hashes.py` every cycle to mirror `messageRollingHashes` + `nextCrossDomainMessageIndex` from mainnet onto the fork.
5252

5353
### Sepolia Shadow Fork — Additional Rules
5454

tests/shadow-testing/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ range), disable the relayer's local proposers and run the polling sync:
6666

6767
```bash
6868
python3 scripts/sync-mainnet-db.py --poll-interval 60 # DB rows + l2_block/parent links
69-
python3 scripts/sync-queue-hashes.py # L1 queue rolling hashes (cron every 10 min)
69+
python3 scripts/sync-queue-hashes.py # L1 queue rolling hashes (also runs inside sync-mainnet-db.py poll loop)
7070
scripts/sweep-stale-proving.sh # stale task sweeper (cron every 10 min)
7171
```
7272

tests/shadow-testing/configs/coordinator.json.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"provers_per_session": 1,
44
"session_attempts": 5,
55
"external_prover_threshold": 32,
6-
"bundle_collection_time_sec": 180,
6+
"bundle_collection_time_sec": 7200,
77
"batch_collection_time_sec": 180,
88
"chunk_collection_time_sec": 3600,
99
"verifier": {

tests/shadow-testing/docs/GUIDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,15 +1023,15 @@ Critical behavior of the sync (do not bypass):
10231023

10241024
Watch item: the first bundle whose batches were committed on mainnet **after** the fork block exercises the relayer's commit path on Anvil (blob-carrying `commitBatches` tx). Keep `fusaka_timestamp: 2000000000` in the relayer config so Anvil accepts the blob sidecar.
10251025

1026-
**L1 message queue follow-along (mandatory for long runs)**: bundles that pop L1 messages enqueued after the fork block fail finalization (`VerificationFailed` / `ErrorFinalizedIndexTooLarge`, Trap 23). Run `scripts/sync-queue-hashes.py` periodically — it copies `getMessageRollingHash(i)` from a mainnet RPC into the fork's `messageRollingHashes` mapping (slot 101) and aligns `nextCrossDomainMessageIndex` (slot 103) with mainnet.
1026+
**L1 message queue follow-along (mandatory for long runs)**: bundles that pop L1 messages enqueued after the fork block fail finalization (`VerificationFailed` / `ErrorFinalizedIndexTooLarge`, Trap 23). `sync-mainnet-db.py` poll mode runs `scripts/sync-queue-hashes.py` every cycle — it copies `getMessageRollingHash(i)` from a mainnet RPC into the fork's `messageRollingHashes` mapping (slot 101) and aligns `nextCrossDomainMessageIndex` (slot 103) with mainnet.
10271027

10281028
**Recommended automation** (what the 48-hour test ran with):
10291029

10301030
| Cadence | Job | Purpose |
10311031
|---------|-----|---------|
10321032
| 60s loop | `sync-mainnet-db.py --poll-interval 60` | DB row sync + l2_block/parent-link repair |
10331033
| 10 min cron | `scripts/sweep-stale-proving.sh` | Reset stale proving rows **and `total_attempts`** (attempt exhaustion silently starves tasks, Trap 22) |
1034-
| 10 min cron | `scripts/sync-queue-hashes.py` | L1 queue rolling hashes + cursor follow mainnet (Trap 23) |
1034+
| every poll cycle (in `sync-mainnet-db.py`) | `scripts/sync-queue-hashes.py` | L1 queue rolling hashes + cursor follow mainnet (Trap 23) |
10351035
| 1 h cron | `scripts/monitor-catchup.py >> .work/catchup-metrics.log` | Hourly metrics snapshot for the final report |
10361036

10371037
Final report: `SHADOW_REPORT_START=<ISO8601> python3 scripts/generate-catchup-report.py` — the env var windows the report to the current run (the metrics log accumulates across runs).

tests/shadow-testing/docs/TROUBLESHOOTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ Before executing a single command:
261261
3. Call the wrapper's `verify(bundleProof, publicInput)` directly with the metadata-derived publicInput. **If it succeeds**, proof/digests are fine and the mismatch is contract-side — ScrollChain computes `messageQueueHash` itself via `L1MessageQueueV2.getMessageRollingHash()`.
262262
4. Compare `getMessageRollingHash(i)` on fork vs mainnet (public RPC e.g. `https://ethereum-rpc.publicnode.com` works; Alchemy demo key is 429-rate-limited).
263263
- **Root cause**: `L1MessageQueueV2` keeps `mapping(uint256=>bytes32) messageRollingHashes` at **storage slot 101** (value = rolling hash with low 32 bits overwritten by enqueue timestamp; the getter clears those bits). Entries for messages enqueued after the fork block are zero on the fork, so the contract builds a publicInput whose `messageQueueHash` differs from the prover's → plonk rejects. Note the **off-by-one**: rh[i] is the hash *after* message i, so a bundle popping to index N needs rh[N-1]; DB `prev/post_l1_message_queue_hash` at popped_before=N equals `getMessageRollingHash(N-1)`.
264-
- **Fix**: `scripts/sync-queue-hashes.py` copies `getMessageRollingHash(i)` from a mainnet RPC into the fork via `anvil_setStorageAt(queue, keccak256(pad32(i)‖pad32(101)), hash)` and also bumps `nextCrossDomainMessageIndex` (slot 103 = 0x67) to mainnet's value (this is what clears the follow-up `ErrorFinalizedIndexTooLarge`). It is idempotent and runs on a 10-minute cron during catch-up mode. Run it manually after any Anvil restart/re-fork, and whenever a finalize reverts with either selector.
264+
- **Fix**: `scripts/sync-queue-hashes.py` copies `getMessageRollingHash(i)` from a mainnet RPC into the fork via `anvil_setStorageAt(queue, keccak256(pad32(i)‖pad32(101)), hash)` and also bumps `nextCrossDomainMessageIndex` (slot 103 = 0x67) to mainnet's value (this is what clears the follow-up `ErrorFinalizedIndexTooLarge`). It is idempotent and runs inside the `sync-mainnet-db.py` poll loop during catch-up mode (failures there only log a warning, never kill the DB sync). Run it manually after any Anvil restart/re-fork, and whenever a finalize reverts with either selector.
265265
- **Rule of thumb**: if finalization fails only for bundles whose `post_l1_message_queue_hash != prev_l1_message_queue_hash` (i.e. the batch pops messages), suspect this trap before touching proofs or the verifier wrapper.
266266

267267
## Step-by-Step Checklist

tests/shadow-testing/scripts/sync-mainnet-db.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
"""
1717

1818
import argparse
19+
import importlib.util
1920
import json
2021
import logging
2122
import os
2223
import subprocess
2324
import time
2425
import urllib.request
26+
from pathlib import Path
2527

2628
import psycopg2
2729

@@ -31,6 +33,14 @@
3133
)
3234
log = logging.getLogger("sync-mainnet-db")
3335

36+
# sync-queue-hashes.py (Trap 23) is loaded as a module so each poll cycle can
37+
# also mirror L1MessageQueueV2 rolling hashes onto the Anvil fork.
38+
_sqh_spec = importlib.util.spec_from_file_location(
39+
"sync_queue_hashes", Path(__file__).parent / "sync-queue-hashes.py"
40+
)
41+
_sqh = importlib.util.module_from_spec(_sqh_spec)
42+
_sqh_spec.loader.exec_module(_sqh)
43+
3444
SRC_DSN = os.environ.get(
3545
"MAINNET_DSN",
3646
"postgresql://mainnet_infra_team_read_only:AuexDUuaarskbG6tr9CH9gXsJqp4at67mddAbMrt@localhost:15432/mainnet_rollup",
@@ -581,6 +591,13 @@ def _ensure_cursors():
581591
finally:
582592
src_cur.close()
583593
dst_cur.close()
594+
# Trap 23: mirror post-fork L1 queue rolling hashes onto the fork
595+
# so bundles popping new L1 messages can be finalized. Failures
596+
# here must never kill the DB sync loop.
597+
try:
598+
_sqh.run_once(log_fn=lambda m: log.info("queue-hash: %s", m))
599+
except Exception as e:
600+
log.warning("queue-hash sync failed: %s", e)
584601
time.sleep(interval)
585602
finally:
586603
src.close()

tests/shadow-testing/scripts/sync-queue-hashes.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,36 +89,38 @@ def set_storage(index, value32):
8989
)
9090

9191

92-
def main():
93-
dry_run = "--dry-run" in sys.argv
92+
def run_once(dry_run=False, log_fn=print):
93+
"""One sync pass. Returns (synced_hashes, (old_next, new_next) or None)."""
9494
start = next_index(FORK_RPC) - 1 # last index guaranteed present on fork
95-
print(f"fork nextCrossDomainMessageIndex-1 = {start}")
95+
log_fn(f"fork nextCrossDomainMessageIndex-1 = {start}")
9696
synced = 0
9797
i = start + 1
9898
while synced < MAX_PER_RUN:
9999
try:
100100
mainnet_hash = rolling_hash(MAINNET_RPC, i)
101101
except Exception as e:
102-
print(f"mainnet RPC error at index {i}: {e}; stopping")
102+
log_fn(f"mainnet RPC error at index {i}: {e}; stopping")
103103
break
104104
if mainnet_hash == b"\x00" * 32:
105105
break # not enqueued on mainnet yet
106106
fork_hash = rolling_hash(FORK_RPC, i)
107107
if fork_hash != mainnet_hash:
108-
print(f"index {i}: fork={fork_hash.hex() or '0'} -> mainnet={mainnet_hash.hex()}")
108+
log_fn(f"index {i}: fork={fork_hash.hex() or '0'} -> mainnet={mainnet_hash.hex()}")
109109
if not dry_run:
110110
set_storage(i, mainnet_hash)
111111
synced += 1
112112
i += 1
113-
print(f"done; {'would sync' if dry_run else 'synced'} {synced} rolling hashes")
113+
log_fn(f"done; {'would sync' if dry_run else 'synced'} {synced} rolling hashes")
114114

115115
# Keep the fork's enqueue cursor in line with mainnet: finalizing a bundle
116116
# that pops messages beyond the fork's nextCrossDomainMessageIndex reverts
117117
# with ErrorFinalizedIndexTooLarge (0x16465978). Slot 103 = 0x67.
118+
bumped = None
118119
mainnet_next = next_index(MAINNET_RPC)
119120
fork_next = next_index(FORK_RPC)
120121
if mainnet_next > fork_next:
121-
print(f"nextCrossDomainMessageIndex: fork={fork_next} -> mainnet={mainnet_next}")
122+
log_fn(f"nextCrossDomainMessageIndex: fork={fork_next} -> mainnet={mainnet_next}")
123+
bumped = (fork_next, mainnet_next)
122124
if not dry_run:
123125
subprocess.run(
124126
[
@@ -129,6 +131,11 @@ def main():
129131
check=True,
130132
capture_output=True,
131133
)
134+
return synced, bumped
135+
136+
137+
def main():
138+
run_once(dry_run="--dry-run" in sys.argv)
132139

133140

134141
if __name__ == "__main__":

0 commit comments

Comments
 (0)