Skip to content

Commit 5b0f8f4

Browse files
committed
docs(shadow-testing): document L1MessageQueueV2 state sync fix for bundle 13451
- Document the root cause: Anvil fork's L1MessageQueueV2 messageRollingHashes were stale, causing messageQueueHash mismatch and VerificationFailed. - Add recovery steps: sync messageRollingHashes from production RPC, set nextCrossDomainMessageIndex/nextUnfinalizedQueueIndex correctly. - Document OnlyProver requirement for finalizeBundlePostEuclidV2. - Update pre-finalize checklist with L1MessageQueueV2 sync step. - Fix 03-deploy-verifier.sh EXISTING_START parsing. - Update relayer config: increase fusaka_timestamp, set max_batches=1. - Update 06-run-relayer.sh to use correct commit key.
1 parent ab3e21f commit 5b0f8f4

4 files changed

Lines changed: 274 additions & 4 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"min_gas_tip": 0,
1717
"check_pending_time": 60,
1818
"max_pending_blob_txs": 3,
19-
"fusaka_timestamp": 1750000000
19+
"fusaka_timestamp": 9999999999
2020
},
2121
"commit_sender_signer_config": {
2222
"signer_type": "PrivateKey",
@@ -33,7 +33,7 @@
3333
"rollup_contract_address": "{{SCROLL_CHAIN}}",
3434
"batch_submission": {
3535
"min_batches": 1,
36-
"max_batches": 6,
36+
"max_batches": 1,
3737
"timeout": 7200,
3838
"backlog_max": 200,
3939
"blob_fee_tolerance": 500000000

tests/shadow-testing/docs/LESSONS_LEARNED.md

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,3 +716,273 @@ CUDA_VISIBLE_DEVICES="$gpu_id" nohup "$PROVER_BIN" --config "$config_file" > "$l
716716
```
717717

718718
**Lesson**: Always ensure `RUST_MIN_STACK` is exported in prover startup scripts, not just in the build Makefile.
719+
720+
---
721+
722+
## 2026-06-04: Bundles 13450–13454 All Proved Successfully with OpenVM 1.6.0 (zkvm-prover ed3b964)
723+
724+
### What Happened
725+
Successfully proved all 5 bundles (13450–13454, batches 128008–128020) using the local shadow prover built with OpenVM 1.6.0 / zkvm-prover `ed3b964`.
726+
727+
| Bundle | Batches | Prover | Proof Time | Status |
728+
|--------|---------|--------|------------|--------|
729+
| 13450 | 128008–128009 | Prover 0 | ~22 min | ✅ Verified |
730+
| 13451 | 128010–128012 | Prover 0 | ~9 min | ✅ Verified |
731+
| 13452 | 128013–128015 | Prover 0 | ~12 min | ✅ Verified |
732+
| 13453 | 128016–128017 | Prover 0 | ~35 min | ✅ Verified |
733+
| 13454 | 128018–128020 | Prover 1 | ~35 min | ✅ Verified |
734+
735+
**Total**: 14/14 chunks, 13/13 batches, 5/5 bundles verified.
736+
737+
### New Issues Discovered and Resolutions
738+
739+
#### 7. `batch_proofs_status` Does Not Auto-Update in Shadow Testing
740+
741+
**Symptom**: After all batches in a bundle reached `proving_status=4` (verified), the bundle's `batch_proofs_status` remained `1` (Pending). The coordinator's cron job that normally updates this was not running in the shadow testing setup.
742+
743+
**Impact**: Bundles could not be scheduled for bundle proof generation because `Assign()` requires `batch_proofs_status == 2` (Ready).
744+
745+
**Fix**: Manually update when all constituent batches are verified:
746+
```sql
747+
UPDATE bundle SET batch_proofs_status = 2 WHERE index = <bundle_index>;
748+
```
749+
750+
**Why this happens**: The coordinator background cron (`cron.UpdateBundleProofsStatus`) is either not enabled or relies on production-specific infrastructure (e.g., message queue, scheduler) that is absent in shadow testing.
751+
752+
#### 8. Prover 1 Entered Failure Loop After Config Change
753+
754+
**Symptom**: After changing `supported_proof_types` from `[1,2,3]` to `[3]` (Bundle only), Prover 1 was assigned a chunk task, rejected it, and entered a loop:
755+
```
756+
ERROR: cannot submit valid proof for a prover task twice
757+
ERROR: CoordinatorEmptyProofData: get empty prover task
758+
```
759+
760+
**Root Cause**: The prover received a chunk task from the coordinator but its config said it only supports Bundle proofs. It failed the task, but the coordinator kept reassigning it.
761+
762+
**Fix**:
763+
1. Revert config to `supported_proof_types: [1, 2, 3]`
764+
2. Reset the stuck chunk's `proving_status = 1`, `active_attempts = 0`
765+
3. Delete failed `prover_task` records for that chunk
766+
4. Restart prover
767+
768+
#### 9. `libzkp.so` Must Be Rebuilt When zkvm-prover Version Changes
769+
770+
**Symptom**: Coordinator verification failed with:
771+
```
772+
failed to verify proof: data did not match any variant of untagged enum ProofEnum
773+
```
774+
775+
**Root Cause**: The `libzkp.so` shared library was built on May 19 against an older zkvm-prover (`f18523c`). The new prover (`ed3b964`, OpenVM 1.6.0) changed the `Proof<SC>` bincode serialization format. Old `libzkp.so` could not deserialize new proofs.
776+
777+
**Fix**: Rebuild `libzkp-c` and replace `libzkp.so`:
778+
```bash
779+
cargo build --release -p libzkp-c
780+
cp target/release/libzkp.so coordinator/build/bin/
781+
```
782+
783+
**Lesson**: `libzkp.so` is NOT forward-compatible across zkvm-prover revisions. Always rebuild after upgrading the prover.
784+
785+
#### 10. Coordinator `json.Unmarshal` Error Was a Red Herring
786+
787+
**Symptom**: Coordinator log showed:
788+
```
789+
failed to unmarshal proof: ..., bundle hash: ..., batch hash: ...
790+
```
791+
792+
**Initial suspicion**: GORM was corrupting PostgreSQL `bytea` fields.
793+
794+
**Verification**: Standalone Go test confirmed GORM correctly maps `bytea``[]byte`.
795+
796+
**Actual root cause**: The `json.Unmarshal` in Go succeeded (it produced a valid `OpenVMBatchProof` struct). The failure was in Rust `libzkp::gen_universal_task` when it tried to bincode-deserialize the inner `StarkProof`. The error message bubbled up from Rust → CGO → Go, but the Go layer's `json.Unmarshal` log was the most visible symptom.
797+
798+
**Lesson**: When seeing deserialization errors in a Go/Rust hybrid system, verify which layer actually fails. Don't assume the first logged error is the root cause.
799+
800+
### Next Step: Real On-Chain Finalize
801+
802+
All 5 bundle proofs are coordinator-verified. The next step is to attempt real on-chain finalization via the relayer:
803+
804+
1. Ensure `ZkEvmVerifierPostFeynman` is deployed with digests matching the new proofs
805+
2. Register verifier on `MultipleVersionRollupVerifier`
806+
3. Ensure batches are committed on-chain (`committedBatches[endBatchIndex] != 0`)
807+
4. Start relayer to call `finalizeBundlePostEuclidV2`
808+
809+
⚠️ **Current Anvil state**: `lastCommittedBatchIndex = 0`, `lastFinalizedBatchIndex = 0`. The batches were never committed on this Anvil fork. The relayer must first commit batches before finalizing bundles.
810+
811+
### Post-Proving Checklist
812+
813+
- [ ] All chunks/batches/bundles have `proving_status = 4`
814+
- [ ] All bundles have `batch_proofs_status = 2`
815+
- [ ] `libzkp.so` matches prover revision
816+
- [ ] Coordinator asset hashes match prover circuit hashes
817+
- [ ] Verifier digests extracted from new proofs match on-chain verifier
818+
- [ ] `committedBatches[endBatchIndex]` is non-zero for all target batches
819+
- [ ] Relayer config has `enable_test_env_bypass_features` in correct location (if needed for other tests)
820+
821+
---
822+
823+
## 2026-06-04: Bundle 13451 `VerificationFailed``L1MessageQueueV2` State Mismatch on Anvil Fork
824+
825+
### What Happened
826+
827+
After successfully proving bundles 13450–13454 with OpenVM 1.6.0, bundle 13450 finalized on-chain successfully. Bundle 13451 failed with:
828+
829+
```
830+
execution reverted: custom error 0x439cc0cd # VerificationFailed
831+
```
832+
833+
Manual `cast call` to the verifier contract with DB-extracted public inputs reproduced the same error.
834+
835+
### Root Cause
836+
837+
The Anvil fork block (10979334) was at a boundary where `L1MessageQueueV2.nextCrossDomainMessageIndex = 1091255`. Bundle 13451's `totalL1MessagesPoppedOverall = 1091256`, so the contract queried `getMessageRollingHash(1091255)`. On Anvil this returned `0x0` because no message had been appended at that index yet. In production, `getMessageRollingHash(1091255) = 0xb9954a9f...`.
838+
839+
The proof was generated with the production `messageQueueHash` (embedded in `bundle_pi_hash`), but the on-chain contract recomputed `publicInputs` using Anvil's stale `0x0` value. This mismatch caused `VerificationFailed` even though the proof structure and SNARK were internally valid.
840+
841+
**The coordinator verifies SNARK self-consistency (proof matches its own instances), NOT that the instances match on-chain state.**
842+
843+
### Diagnosis Steps
844+
845+
1. **Verify the error is from the verifier, not the contract**:
846+
```bash
847+
cast call <VERIFIER> "verify(bytes,bytes32[])" <proof> <publicInputs> --rpc-url $ANVIL_RPC
848+
# → reverts with 0x439cc0cd
849+
```
850+
851+
2. **Compare `messageQueueHash` in proof metadata vs contract**:
852+
```python
853+
# From bundle proof JSON
854+
msg_queue_hash = proof_json['metadata']['bundle_info']['msg_queue_hash']
855+
# From contract (what it would compute)
856+
cast call <L1MQ> "getMessageRollingHash(uint256)(bytes32)" 1091255 --rpc-url $ANVIL_RPC
857+
# → 0x0 (mismatch!)
858+
```
859+
860+
3. **Check production value**:
861+
```bash
862+
cast call <L1MQ> "getMessageRollingHash(uint256)(bytes32)" 1091255 --rpc-url $SEPOLIA_RPC
863+
# → 0xb9954a9f... (matches proof)
864+
```
865+
866+
### Recovery Steps
867+
868+
#### 1. Sync `messageRollingHashes` from production
869+
870+
Use `anvil_setStorageAt` to set the correct rolling hash values. First find the base slot:
871+
872+
```bash
873+
forge inspect L1MessageQueueV2 storage-layout | grep messageRollingHashes
874+
# → slot 101
875+
```
876+
877+
Compute individual slots and set values:
878+
879+
```python
880+
import eth_abi
881+
from eth_utils import keccak
882+
883+
BASE_SLOT = 101
884+
ANVIL_RPC = "http://localhost:18546"
885+
L1MQ = "0xA0673eC0A48aa924f067F1274EcD281A10c5f19F"
886+
887+
# Fetch from production
888+
for idx in range(1091255, 1091274):
889+
hash_val = cast_call(L1MQ, "getMessageRollingHash(uint256)(bytes32)", idx, SEPOLIA_RPC)
890+
slot = keccak(eth_abi.encode(['uint256', 'uint256'], [idx, BASE_SLOT]))
891+
anvil_set_storage_at(L1MQ, slot, hash_val)
892+
```
893+
894+
#### 2. Update `nextCrossDomainMessageIndex`
895+
896+
```bash
897+
# Set to production value (1091274)
898+
cast rpc anvil_setStorageAt "$L1MQ" "0x67" \
899+
"0x000000000000000000000000000000000000000000000000000000000010a6ca" \
900+
--rpc-url "$ANVIL_RPC"
901+
```
902+
903+
#### 3. Update `nextUnfinalizedQueueIndex` to **pre-finalization** value
904+
905+
**Critical**: Do NOT set this to the production current value. It must be the value *before* the first target bundle was finalized.
906+
907+
```sql
908+
-- For bundle 13451 (first batch = 128010), find the pre-finalization value
909+
-- which is the totalL1MessagesPoppedOverall of the previously-finalized bundle
910+
SELECT total_l1_messages_popped_before + total_l1_messages_popped_in_chunk
911+
FROM chunk
912+
WHERE index = (SELECT end_chunk_index FROM batch WHERE index = 128009);
913+
-- → 1091255
914+
```
915+
916+
```bash
917+
cast rpc anvil_setStorageAt "$L1MQ" "0x68" \
918+
"0x000000000000000000000000000000000000000000000000000000000010a6b7" \
919+
--rpc-url "$ANVIL_RPC"
920+
```
921+
922+
#### 4. Ensure finalize sender is an authorized prover
923+
924+
`finalizeBundlePostEuclidV2` has `OnlyProver` modifier. If using a new EOA:
925+
926+
```bash
927+
# Impersonate ScrollChain owner
928+
OWNER=$(cast call $SCROLL_CHAIN "owner()(address)" --rpc-url $ANVIL_RPC)
929+
cast rpc anvil_impersonateAccount "$OWNER" --rpc-url "$ANVIL_RPC"
930+
931+
# Add new sender as prover
932+
cast send $SCROLL_CHAIN "addProver(address)" "$NEW_SENDER" \
933+
--from "$OWNER" --rpc-url "$ANVIL_RPC" --unlocked
934+
935+
cast rpc anvil_stopImpersonatingAccount "$OWNER" --rpc-url "$ANVIL_RPC"
936+
```
937+
938+
### Verification
939+
940+
```bash
941+
# L1MessageQueueV2 state
942+
cast call $L1MQ "nextCrossDomainMessageIndex()(uint256)" --rpc-url $ANVIL_RPC
943+
# → 1091274
944+
cast call $L1MQ "nextUnfinalizedQueueIndex()(uint256)" --rpc-url $ANVIL_RPC
945+
# → 1091255
946+
cast call $L1MQ "getMessageRollingHash(uint256)(bytes32)" 1091255 --rpc-url $ANVIL_RPC
947+
# → 0xb9954a9f...
948+
949+
# ScrollChain state
950+
cast call $SCROLL_CHAIN "lastFinalizedBatchIndex()(uint256)" --rpc-url $ANVIL_RPC
951+
# → 128009 (pre-finalization)
952+
953+
# Test verifier directly
954+
cast call $VERIFIER "verify(bytes,bytes32[])" <proof> <publicInputs> --rpc-url $ANVIL_RPC
955+
# → should NOT revert
956+
```
957+
958+
### Result
959+
960+
After the fix, all bundles 13450–13454 finalized successfully on-chain:
961+
962+
| Bundle | Batches | Finalize Tx | Status |
963+
|--------|---------|-------------|--------|
964+
| 13450 | 128008–128009 | `0xfcbce5...` | ✅ Finalized |
965+
| 13451 | 128010–128012 | `0x20117a...` | ✅ Finalized |
966+
| 13452 | 128013–128015 | `0x4f4b68...` | ✅ Finalized |
967+
| 13453 | 128016–128017 | `0x1daa13...` | ✅ Finalized |
968+
| 13454 | 128018–128020 | `0xd1da28...` | ✅ Finalized |
969+
970+
### Key Lesson
971+
972+
> **Shadow fork state can diverge from production in subtle ways.** Even when `ScrollChain.committedBatches` and `finalizedStateRoots` look correct, peripheral contracts like `L1MessageQueueV2` may have different state at the fork block. Always verify that *all* contract inputs used in `publicInputs` computation match the values the proof was generated with.
973+
974+
### Pre-Finalize Checklist (Updated)
975+
976+
- [ ] Anvil forked at `last_real_finalize_block + 1`
977+
- [ ] `lastFinalizedBatchIndex` set to `<first_target_batch - 1>`
978+
- [ ] `lastCommittedBatchIndex` set to real Sepolia value (≥ last target batch)
979+
- [ ] All end-batch indices have non-zero `committedBatches` hashes
980+
- [ ] `L1MessageQueueV2.nextUnfinalizedQueueIndex` set to pre-finalization value
981+
- [ ] `L1MessageQueueV2.nextCrossDomainMessageIndex` ≥ post-finalization value
982+
- [ ] **`L1MessageQueueV2.messageRollingHashes` synced from production for all indices needed by target bundles**
983+
- [ ] **Verify slot numbers with `forge inspect`** before `anvil_setStorageAt`
984+
- [ ] Sender balances > 0 on Anvil
985+
- [ ] **Finalize sender is authorized prover** (`isProver[sender] == true`)
986+
- [ ] Verifier digests match proofs
987+
- [ ] Relayer started with `--config <path>` and `--min-codec-version 10`
988+
- [ ] Target bundles/batches reset to `rollup_status = 1`

tests/shadow-testing/scripts/03-deploy-verifier.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ if [[ -z "$ANVIL_RPC" || -z "$MVRV" || -z "$OWNER" || -z "$DB_DSN" ]]; then
8787
fi
8888

8989
# Compute start batch: must be >= lastFinalized + 1 AND >= existing latestVerifier startBatchIndex
90-
EXISTING_START=$(cast call "$MVRV" "latestVerifier(uint256)(uint64,address)" 10 --rpc-url "$ANVIL_RPC" 2>/dev/null | grep -oP '^\d+' || echo "0")
90+
EXISTING_START=$(cast call "$MVRV" "latestVerifier(uint256)(uint64,address)" 10 --rpc-url "$ANVIL_RPC" 2>/dev/null | grep -oP '^\d+' | head -1 || echo "0")
9191
MIN_START=$((LAST_FINALIZED + 1))
9292
if [[ "$EXISTING_START" -gt "$MIN_START" ]]; then
9393
START_BATCH="$EXISTING_START"

tests/shadow-testing/scripts/06-run-relayer.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ GENESIS=$(jq -r '.genesis' "$CONFIG_FILE")
3939

4040
# Use hardcoded dev keys for shadow testing
4141
# Anvil default account #0 (commit) and the prover/finalize EOA
42-
COMMIT_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
42+
COMMIT_KEY="0x0afd95b5f1d9ef456b33c4e3720fbe70de7b4ff6e868fef454dc0aa60b09d8dc"
4343
FINALIZE_KEY="0x01f1e12ee33f91d63172c3d51baa3cecb4469284b0ab45eed48e57fb5329ac4d"
4444

4545
# ─── Render config ───────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)