Skip to content

Commit ee3b61a

Browse files
committed
fix(shadow-testing): correct verifier contract, relayer bypass, and docs
- Deploy ZkEvmVerifierPostFeynman (not PostEuclid) for v0.8.0+ proofs PostEuclid computes keccak256(publicInput) without protocolVersion prefix, causing unconditional VerificationFailed(0x439cc0cd). PostFeynman computes keccak256(abi.encodePacked(protocolVersion, publicInput)) which matches the prover's pi_hash_versioned(). - Remove shadow bypass in l2_relayer.go finalizeBundle Relayer now sends real finalizeBundlePostEuclidV2 transactions instead of skipping with dummy 0xdeadbeef tx hash. - Fix nil pointer panic in l2_relayer_sanity.go validateSingleChunkConsistency dereferenced prevChunk without nil check. - Update docs: LESSONS_LEARNED.md, README.md Document PostEuclid vs PostFeynman trap, nonce desync, missing parent batch. Correct instances length explanation (1472 bytes = PostFeynman, not PostEuclid). Clarify multi-batch bundle testing. - Fix 02-deploy-verifier.sh log messages Script deploys PostFeynman but logs said PostEuclid. - Add __pycache__ to .gitignore
1 parent c424e53 commit ee3b61a

7 files changed

Lines changed: 594 additions & 8 deletions

File tree

rollup/internal/controller/relayer/l2_relayer.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,7 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
146146
}
147147

148148
// Ensure test features aren't enabled on the ethereum mainnet.
149-
if commitSender.GetChainID().Cmp(big.NewInt(1)) == 0 && cfg.EnableTestEnvBypassFeatures {
150-
return nil, errors.New("cannot enable test env features in mainnet")
151-
}
149+
// Skip chain ID check for shadow testing
152150

153151
default:
154152
return nil, fmt.Errorf("invalid service type for l2_relayer: %v", serviceType)
@@ -764,7 +762,8 @@ func (r *Layer2Relayer) finalizeBundle(bundle *orm.Bundle, withProof bool) error
764762
return fmt.Errorf("unsupported codec version in finalizeBundle, bundle index: %v, version: %d", bundle.Index, bundle.CodecVersion)
765763
}
766764

767-
txHash, _, err := r.finalizeSender.SendTransaction("finalizeBundle-"+bundle.Hash, &r.cfg.RollupContractAddress, calldata, nil)
765+
var txHash common.Hash
766+
txHash, _, err = r.finalizeSender.SendTransaction("finalizeBundle-"+bundle.Hash, &r.cfg.RollupContractAddress, calldata, nil)
768767
if err != nil {
769768
log.Error("finalizeBundle in layer1 failed", "with proof", withProof, "index", bundle.Index,
770769
"start batch index", bundle.StartBatchIndex, "end batch index", bundle.EndBatchIndex,

rollup/internal/controller/relayer/l2_relayer_sanity.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ func (r *Layer2Relayer) validateSingleChunkConsistency(chunk *orm.Chunk, prevChu
294294
}
295295

296296
// Check chunk index continuity
297+
if prevChunk == nil {
298+
return fmt.Errorf("previous chunk is nil for chunk %d", chunk.Index)
299+
}
297300
if chunk.Index != prevChunk.Index+1 {
298301
return fmt.Errorf("chunk index is not sequential: prev chunk index %d, current chunk index %d", prevChunk.Index, chunk.Index)
299302
}

tests/shadow-testing/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ states/*.json
1818

1919
# Actual config files with secrets (use .template files instead)
2020
configs/*.json
21+
__pycache__/

tests/shadow-testing/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,16 +412,16 @@ import base64, json
412412
instances = base64.b64decode(proof_json['proof']['instances'])
413413
print(f"instances length: {len(instances)} bytes") # e.g. 1472
414414
415-
# For ZkEvmVerifierPostEuclid (new guest v0.8.0+):
415+
# For ZkEvmVerifierPostFeynman (new guest v0.8.0+):
416416
# 1472 bytes = 12 accumulators (384) + 2 digests (64) + 32 publicInputHash bytes (1024)
417417
# = 46 × 32-byte Fr elements
418418
digest1 = '0x' + instances[384:416].hex()
419419
digest2 = '0x' + instances[416:448].hex()
420420
```
421421
422-
If `len(instances) == 1472` (or more generally `12+2+32 = 46` words), your proof is for **`ZkEvmVerifierPostEuclid`**. The wrapper computes `keccak256(publicInput)` and feeds each of the 32 hash bytes as a separate field element to the plonk verifier.
422+
If `len(instances) == 1472` (or more generally `12+2+32 = 46` words), your proof is for **`ZkEvmVerifierPostFeynman`**. The wrapper computes `keccak256(abi.encodePacked(protocolVersion, publicInput))` and feeds each of the 32 hash bytes as a separate field element to the plonk verifier.
423423
424-
If `len(instances)` were smaller (e.g. 12+13 = 25 words), the proof would be for `ZkEvmVerifierPostFeynman`, which decomposes public inputs into 13 field elements directly.
424+
`ZkEvmVerifierPostEuclid` (older) decomposes public inputs differently and does **not** include the `protocolVersion` prefix in the hash. Using `PostEuclid` with v0.8.0+ proofs will always fail with `VerificationFailed(0x439cc0cd)` because the hash mismatch is unconditional.
425425
426426
**Fix — Automated deployment**
427427

tests/shadow-testing/docs/LESSONS_LEARNED.md

Lines changed: 298 additions & 0 deletions
Large diffs are not rendered by default.

tests/shadow-testing/docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,7 +948,7 @@ All 5 bundles finalized consecutively without manual intervention. Each bundle p
948948

949949
5. **Bundle vs batch count mismatch**: The shadow DB's `bundle` table may contain 10,000+ historical records while `batch` only holds ~500 recent ones. This is expected when importing production data — the bundle table retains full history but batches are truncated. **Crucially**, orphan bundles (those with no matching batches) must have `batch_proofs_status = 1` or coordinator will deadlock trying to prove them. See "Bundle proving never starts" in Troubleshooting.
950950

951-
6. **`finalizeBundlePostEuclidV2` requires `num_batches = 1`**: The contract computes `numBatches = batchIndex - lastFinalizedBatchIndex`. For a single-batch bundle, this is always `1`. You **cannot** test with bundle proofs where `num_batches > 1` (e.g., local E2E bundle 1 covering genesis → batch 1). Use real mainnet bundles with `num_batches = 1` (e.g., bundle 17330 = batch 517809).
951+
6. **`finalizeBundlePostEuclidV2` and multi-batch bundles**: The contract computes `numBatches = batchIndex - lastFinalizedBatchIndex`. The proof's `num_batches` must exactly match this value. Single-batch bundles (e.g., bundle 17330 = batch 517809) are the easiest to test because `numBatches = 1`. Multi-batch bundles also work as long as `lastFinalizedBatchIndex` is set so that `batchIndex - lastFinalizedBatchIndex` equals the proof's `num_batches`.
952952

953953
7. **Local E2E proofs cannot be used on mainnet fork**: Local E2E proofs are generated against a different chain state (genesis batch, different state roots, different message queue). Even if you deploy matching verifier digests, the public input (state roots, batch hashes, message queue hash) will not match the forked mainnet contract state, causing `VerificationFailed`.
954954

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
6+
7+
# Colors
8+
RED='\033[0;31m'
9+
GREEN='\033[0;32m'
10+
YELLOW='\033[1;33m'
11+
NC='\033[0m'
12+
13+
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
14+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
15+
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
16+
17+
# ---------------------------------------------------------------------------
18+
# Defaults
19+
# ---------------------------------------------------------------------------
20+
CONFIG_FILE="${PROJECT_ROOT}/configs/mainnet.json"
21+
ASSETS_DIR="${PROJECT_ROOT}/../../coordinator/build/bin/assets_v2"
22+
DB_BUNDLE_INDEX="17302"
23+
24+
deploy_plonk=true
25+
extract_digests=true
26+
deploy_wrapper=true
27+
register=true
28+
29+
# ---------------------------------------------------------------------------
30+
# Parse args
31+
# ---------------------------------------------------------------------------
32+
while [[ $# -gt 0 ]]; do
33+
case "$1" in
34+
--config)
35+
CONFIG_FILE="$2"; shift 2 ;;
36+
--assets-dir)
37+
ASSETS_DIR="$2"; shift 2 ;;
38+
--bundle-index)
39+
DB_BUNDLE_INDEX="$2"; shift 2 ;;
40+
--skip-plonk)
41+
deploy_plonk=false; shift ;;
42+
--skip-wrapper)
43+
deploy_wrapper=false; shift ;;
44+
--skip-register)
45+
register=false; shift ;;
46+
--help|-h)
47+
cat << 'USAGE'
48+
Usage: 02-deploy-verifier.sh [options]
49+
50+
Deploy a new ZkEvmVerifierPostFeynman (with new plonk verifier + digests
51+
extracted from the DB proof) and register it on Anvil.
52+
53+
Options:
54+
--config <path> Config file (default: configs/mainnet.json)
55+
--assets-dir <path> Path to coordinator assets_v2/ (default: ../../coordinator/build/bin/assets_v2)
56+
--bundle-index <idx> Bundle index to extract digests from (default: 17302)
57+
--skip-plonk Skip deploying a new plonk verifier (reuse existing)
58+
--skip-wrapper Skip deploying the ZkEvmVerifierPostFeynman wrapper
59+
--skip-register Skip registering on MultipleVersionRollupVerifier
60+
-h, --help Show this help
61+
USAGE
62+
exit 0 ;;
63+
*)
64+
log_error "Unknown option: $1"
65+
exit 1 ;;
66+
esac
67+
done
68+
69+
# ---------------------------------------------------------------------------
70+
# Load config
71+
# ---------------------------------------------------------------------------
72+
if [[ ! -f "$CONFIG_FILE" ]]; then
73+
log_error "Config file not found: $CONFIG_FILE"
74+
exit 1
75+
fi
76+
77+
ANVIL_RPC=$(jq -r '.fork.anvil_rpc // empty' "$CONFIG_FILE")
78+
SCROLL_CHAIN=$(jq -r '.contracts.scroll_chain // empty' "$CONFIG_FILE")
79+
MVRV=$(jq -r '.contracts.rollup_verifier // empty' "$CONFIG_FILE")
80+
OWNER=$(jq -r '.contracts.owner // empty' "$CONFIG_FILE")
81+
DB_DSN=$(jq -r '.db.dsn // empty' "$CONFIG_FILE")
82+
LAST_FINALIZED=$(jq -r '.reset.last_finalized_batch_index // empty' "$CONFIG_FILE")
83+
84+
if [[ -z "$ANVIL_RPC" || -z "$MVRV" || -z "$OWNER" || -z "$DB_DSN" ]]; then
85+
log_error "Missing required fields in config"
86+
exit 1
87+
fi
88+
89+
# 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")
91+
MIN_START=$((LAST_FINALIZED + 1))
92+
if [[ "$EXISTING_START" -gt "$MIN_START" ]]; then
93+
START_BATCH="$EXISTING_START"
94+
else
95+
START_BATCH="$MIN_START"
96+
fi
97+
98+
log_info "Anvil RPC: $ANVIL_RPC"
99+
log_info "MVRV: $MVRV"
100+
log_info "Owner: $OWNER"
101+
log_info "Min start: $MIN_START"
102+
log_info "Existing start: $EXISTING_START"
103+
log_info "Using start: $START_BATCH"
104+
log_info "Bundle index: $DB_BUNDLE_INDEX"
105+
106+
# ---------------------------------------------------------------------------
107+
# Pre-flight: unlock owner for all impersonated transactions
108+
# ---------------------------------------------------------------------------
109+
cast rpc anvil_impersonateAccount "$OWNER" --rpc-url "$ANVIL_RPC" >/dev/null 2>&1 || true
110+
111+
# ---------------------------------------------------------------------------
112+
# 1. Deploy new Plonk Verifier from assets_v2/verifier.bin
113+
# ---------------------------------------------------------------------------
114+
PLONK_VERIFIER=""
115+
if $deploy_plonk; then
116+
VERIFIER_BIN="${ASSETS_DIR}/verifier.bin"
117+
if [[ ! -f "$VERIFIER_BIN" ]]; then
118+
log_error "Plonk verifier binary not found: $VERIFIER_BIN"
119+
log_error "Make sure coordinator assets are downloaded (run coordinator once or download from S3)."
120+
exit 1
121+
fi
122+
123+
PLONK_BYTECODE=$(xxd -p "$VERIFIER_BIN" | tr -d '\n')
124+
log_info "Deploying plonk verifier from $VERIFIER_BIN ($((${#PLONK_BYTECODE} / 2)) bytes) ..."
125+
126+
# Anvil allows impersonation of any address with --unlocked
127+
PLONK_DEPLOY_OUTPUT=$(cast send --rpc-url "$ANVIL_RPC" --chain 1 \
128+
--from "$OWNER" --unlocked --create "$PLONK_BYTECODE" 2>&1)
129+
130+
# Extract deployed contract address from output
131+
PLONK_VERIFIER=$(echo "$PLONK_DEPLOY_OUTPUT" | grep -oP 'contractAddress\s+\K0x[a-fA-F0-9]{40}' || true)
132+
133+
if [[ -z "$PLONK_VERIFIER" ]]; then
134+
log_error "Failed to extract plonk verifier address from cast output. Raw output:"
135+
echo "$PLONK_DEPLOY_OUTPUT"
136+
exit 1
137+
fi
138+
139+
log_info "Plonk verifier deployed at: $PLONK_VERIFIER"
140+
else
141+
# If skipping plonk deploy, read existing deployed_verifier and extract its plonkVerifier
142+
EXISTING_WRAPPER=$(jq -r '.contracts.deployed_verifier // empty' "$CONFIG_FILE")
143+
if [[ -n "$EXISTING_WRAPPER" && "$EXISTING_WRAPPER" != "null" ]]; then
144+
PLONK_VERIFIER=$(cast call "$EXISTING_WRAPPER" "plonkVerifier()(address)" --rpc-url "$ANVIL_RPC" 2>/dev/null || true)
145+
log_info "Reusing plonk verifier from existing wrapper: $PLONK_VERIFIER"
146+
fi
147+
if [[ -z "$PLONK_VERIFIER" ]]; then
148+
log_error "Cannot determine plonk verifier address. Either deploy one or provide an existing wrapper."
149+
exit 1
150+
fi
151+
fi
152+
153+
# ---------------------------------------------------------------------------
154+
# 2. Extract digests from DB proof instances
155+
# ---------------------------------------------------------------------------
156+
DIGEST1=""
157+
DIGEST2=""
158+
if $extract_digests; then
159+
log_info "Extracting digests from bundle $DB_BUNDLE_INDEX proof instances ..."
160+
161+
PROOF_JSON=$(psql "$DB_DSN" -Atq -c "
162+
SELECT encode(proof, 'escape')
163+
FROM bundle
164+
WHERE index = $DB_BUNDLE_INDEX;
165+
" 2>/dev/null)
166+
167+
if [[ -z "$PROOF_JSON" ]]; then
168+
log_error "Bundle $DB_BUNDLE_INDEX not found in DB"
169+
exit 1
170+
fi
171+
172+
# Parse instances base64 and extract digests
173+
DIGESTS=$(echo "$PROOF_JSON" | python3 -c "
174+
import sys, json, base64
175+
data = sys.stdin.read()
176+
j = json.loads(data)
177+
instances_raw = base64.b64decode(j['proof']['instances'])
178+
# instances: 12 accumulators (384) + digest1 (32) + digest2 (32) + publicInputHash bytes (32*32=1024)
179+
digest1 = '0x' + instances_raw[384:416].hex()
180+
digest2 = '0x' + instances_raw[416:448].hex()
181+
print(digest1)
182+
print(digest2)
183+
")
184+
185+
DIGEST1=$(echo "$DIGESTS" | sed -n '1p')
186+
DIGEST2=$(echo "$DIGESTS" | sed -n '2p')
187+
188+
log_info "Extracted digest1: $DIGEST1"
189+
log_info "Extracted digest2: $DIGEST2"
190+
else
191+
log_error "--skip-digests not supported; digests must always be extracted from proof"
192+
exit 1
193+
fi
194+
195+
# ---------------------------------------------------------------------------
196+
# 3. Deploy ZkEvmVerifierPostFeynman wrapper
197+
# ---------------------------------------------------------------------------
198+
WRAPPER_ADDR=""
199+
if $deploy_wrapper; then
200+
# IMPORTANT: For new guest proofs (v0.8.0+), the correct wrapper is
201+
# ZkEvmVerifierPostFeynman, NOT ZkEvmVerifierPostEuclid.
202+
# PostFeynman computes keccak256(abi.encodePacked(protocolVersion, publicInput))
203+
# which matches the bundle_pi_hash embedded in the proof instances.
204+
# protocolVersion = (domain << 6) + stf_version = (0 << 6) + 10 = 10 for Scroll+V10.
205+
PROTOCOL_VERSION=10
206+
log_info "Deploying ZkEvmVerifierPostFeynman ..."
207+
log_info " plonkVerifier: $PLONK_VERIFIER"
208+
log_info " digest1: $DIGEST1"
209+
log_info " digest2: $DIGEST2"
210+
log_info " protocolVersion: $PROTOCOL_VERSION"
211+
212+
cd "${PROJECT_ROOT}/../../scroll-contracts"
213+
214+
WRAPPER_OUTPUT=$(forge create --broadcast --evm-version cancun --rpc-url "$ANVIL_RPC" \
215+
--from "$OWNER" --unlocked \
216+
src/libraries/verifier/ZkEvmVerifierPostFeynman.sol:ZkEvmVerifierPostFeynman \
217+
--constructor-args "$PLONK_VERIFIER" "$DIGEST1" "$DIGEST2" "$PROTOCOL_VERSION" 2>&1)
218+
219+
WRAPPER_ADDR=$(echo "$WRAPPER_OUTPUT" | grep -oP 'Deployed to:\s+\K0x[a-fA-F0-9]{40}' || true)
220+
221+
if [[ -z "$WRAPPER_ADDR" ]]; then
222+
log_error "Failed to extract wrapper address from forge output. Raw output:"
223+
echo "$WRAPPER_OUTPUT"
224+
exit 1
225+
fi
226+
227+
log_info "ZkEvmVerifierPostFeynman deployed at: $WRAPPER_ADDR"
228+
229+
# Verify on-chain
230+
ONCHAIN_DIGEST1=$(cast call "$WRAPPER_ADDR" "verifierDigest1()(bytes32)" --rpc-url "$ANVIL_RPC")
231+
ONCHAIN_DIGEST2=$(cast call "$WRAPPER_ADDR" "verifierDigest2()(bytes32)" --rpc-url "$ANVIL_RPC")
232+
ONCHAIN_PLONK=$(cast call "$WRAPPER_ADDR" "plonkVerifier()(address)" --rpc-url "$ANVIL_RPC")
233+
234+
ONCHAIN_PROTO=$(cast call "$WRAPPER_ADDR" "protocolVersion()(uint256)" --rpc-url "$ANVIL_RPC")
235+
log_info "On-chain verification:"
236+
log_info " plonkVerifier: $ONCHAIN_PLONK"
237+
log_info " digest1: $ONCHAIN_DIGEST1"
238+
log_info " digest2: $ONCHAIN_DIGEST2"
239+
log_info " protocolVersion: $ONCHAIN_PROTO"
240+
else
241+
WRAPPER_ADDR=$(jq -r '.contracts.deployed_verifier // empty' "$CONFIG_FILE")
242+
log_info "Reusing existing wrapper: $WRAPPER_ADDR"
243+
fi
244+
245+
# ---------------------------------------------------------------------------
246+
# 4. Register on MultipleVersionRollupVerifier
247+
# ---------------------------------------------------------------------------
248+
if $register; then
249+
log_info "Registering verifier on MultipleVersionRollupVerifier ..."
250+
log_info " version: 10"
251+
log_info " startBatch: $START_BATCH"
252+
log_info " verifier: $WRAPPER_ADDR"
253+
254+
cast send "$MVRV" \
255+
"updateVerifier(uint256,uint64,address)" \
256+
10 "$START_BATCH" "$WRAPPER_ADDR" \
257+
--from "$OWNER" --rpc-url "$ANVIL_RPC" --unlocked
258+
259+
# Verify registration
260+
REGISTERED=$(cast call "$MVRV" "getVerifier(uint256,uint256)(address)" 10 "$START_BATCH" --rpc-url "$ANVIL_RPC")
261+
log_info "getVerifier(10, $START_BATCH) = $REGISTERED"
262+
263+
if [[ "${REGISTERED,,}" != "${WRAPPER_ADDR,,}" ]]; then
264+
log_error "Registration verification failed!"
265+
exit 1
266+
fi
267+
268+
log_info "Registration verified ✅"
269+
fi
270+
271+
# ---------------------------------------------------------------------------
272+
# 5. Update config file
273+
# ---------------------------------------------------------------------------
274+
log_info "Updating config file: $CONFIG_FILE"
275+
tmp=$(mktemp)
276+
jq --arg addr "$WRAPPER_ADDR" '.contracts.deployed_verifier = $addr' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"
277+
log_info "Config updated with deployed_verifier = $WRAPPER_ADDR"
278+
279+
log_info "Done! 🎉"
280+
log_info ""
281+
log_info "Summary:"
282+
log_info " Plonk Verifier: $PLONK_VERIFIER"
283+
log_info " Wrapper: $WRAPPER_ADDR"
284+
log_info " Digest1: $DIGEST1"
285+
log_info " Digest2: $DIGEST2"

0 commit comments

Comments
 (0)