Skip to content

Commit 91e22c0

Browse files
authored
Add game seeding scripts for multiproof testing (#247)
* add game seeding scripts for pre-populating DisputeGameFactory with chained multiproof games * fix: forge fmt
1 parent e0f1cae commit 91e22c0

3 files changed

Lines changed: 439 additions & 0 deletions

File tree

scripts/multiproof/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,84 @@ The deployer address (`finalSystemOwner`) is the owner of `DevTEEProverRegistry`
121121
## Path 2: WithNitro (Dev — Real Attestation)
122122

123123
> **TODO:** Add deployment and registration guide for `DeployDevWithNitro.s.sol`.
124+
125+
---
126+
127+
## Pre-Seeding Games (Post-Deployment)
128+
129+
After deploying via either path, you can pre-seed the `DisputeGameFactory` with a chain of `AggregateVerifier` games. This is useful for testing forward traversal at proposer restart — the proposer can walk the linked list of games to find where to resume.
130+
131+
Games are created using `ProofType.ZK` with the `MockVerifier` (deployed by both WithNitro and NoNitro), which auto-accepts any proof. The output roots themselves are real values fetched from an L2 archive node.
132+
133+
### Step 1: Set the anchor state
134+
135+
Pick an anchor block far enough behind the L2 tip to cover all the games you want to create. Each game covers `BLOCK_INTERVAL` (600) L2 blocks, so for 500 games you need 300,000 blocks of headroom.
136+
137+
```bash
138+
# Calculate an anchor block 300,000 blocks behind the L2 tip
139+
ANCHOR_BLOCK=$(( $(cast block-number --rpc-url $L2_RPC_URL) - 300000 ))
140+
141+
# Get the real output root at that block
142+
OUTPUT_ROOT=$(cast rpc optimism_outputAtBlock $(printf "0x%x" $ANCHOR_BLOCK) \
143+
--rpc-url $L2_RPC_URL | jq -r '.outputRoot')
144+
145+
# Set it on the MockAnchorStateRegistry (no access control — any caller works)
146+
cast send $ANCHOR_STATE_REGISTRY_ADDRESS \
147+
"setAnchorState(bytes32,uint256)" $OUTPUT_ROOT $ANCHOR_BLOCK \
148+
--rpc-url $L1_RPC_URL --private-key $PRIVATE_KEY
149+
```
150+
151+
### Step 2: Generate real output roots
152+
153+
Fetch the real L2 output roots for every intermediate block across all games. This queries `optimism_outputAtBlock` on the L2 archive node (10,000 queries for 500 games, parallelized).
154+
155+
```bash
156+
./scripts/multiproof/generate-roots.sh $ANCHOR_BLOCK $L2_RPC_URL 500
157+
```
158+
159+
Arguments: `<anchor_block> <l2_rpc_url> [game_count] [parallelism] [output_file]`
160+
161+
Defaults: `game_count=500`, `parallelism=20`, `output_file=roots.json`.
162+
163+
### Step 3: Move roots file for Foundry access
164+
165+
Foundry's filesystem sandbox only allows reads from paths listed in `foundry.toml` `fs_permissions`. The `deployments/` directory already has read-write access, so move the file there:
166+
167+
```bash
168+
mv roots.json deployments/roots.json
169+
```
170+
171+
### Step 4: Run the seeding script
172+
173+
Create all games on-chain. Each game is chained to the previous one (game 0's parent is the `AnchorStateRegistry`, game N's parent is game N-1). The account running this needs enough ETH for bonds and gas (500 games at 0.00001 ETH bond = 0.005 ETH + gas).
174+
175+
```bash
176+
ROOTS_FILE=./deployments/roots.json \
177+
FACTORY_ADDRESS=$FACTORY_ADDRESS \
178+
ANCHOR_STATE_REGISTRY_ADDRESS=$ANCHOR_STATE_REGISTRY_ADDRESS \
179+
forge script scripts/multiproof/SeedGames.s.sol \
180+
--rpc-url $L1_RPC_URL --broadcast --private-key $PRIVATE_KEY
181+
```
182+
183+
> **Note:** Use `--private-key` instead of `--ledger` to avoid manually confirming 500 transactions on a hardware wallet.
184+
185+
Optional env vars:
186+
187+
| Variable | Default | Description |
188+
|---|---|---|
189+
| `GAME_COUNT` | 500 | Number of games to create |
190+
| `ROOTS_FILE` | `roots.json` | Path to the output roots JSON |
191+
192+
### Step 5: Verify on-chain
193+
194+
```bash
195+
# Check total game count
196+
cast call $FACTORY_ADDRESS "gameCount()(uint256)" --rpc-url $L1_RPC_URL
197+
198+
# Check first game's parent is the AnchorStateRegistry
199+
FIRST_GAME=$(cast call $FACTORY_ADDRESS \
200+
"gameAtIndex(uint256)(uint32,uint64,address)" 0 --rpc-url $L1_RPC_URL | tail -1)
201+
cast call $FIRST_GAME "parentAddress()(address)" --rpc-url $L1_RPC_URL
202+
```
203+
204+
Output metadata is saved to `deployments/<chainId>-seeded-games.json`.

scripts/multiproof/SeedGames.s.sol

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.15;
3+
4+
/**
5+
* @title SeedGames
6+
* @notice Seeds the DisputeGameFactory with chained AggregateVerifier games using real L2 output roots.
7+
*
8+
* ══════════════════════════════════════════════════════════════════════════════════
9+
* POST-DEPLOYMENT GAME SEEDING
10+
* ══════════════════════════════════════════════════════════════════════════════════
11+
*
12+
* Creates a chain of AggregateVerifier (multiproof) games via the DisputeGameFactory
13+
* deployed by DeployDevWithNitro (or DeployDevNoNitro). Each game's parent is the
14+
* previous game, forming a linked list suitable for testing forward traversal at
15+
* proposer restart.
16+
*
17+
* Games use ProofType.ZK (value 1) because both deployment scripts wire up a
18+
* MockVerifier for ZK that auto-accepts any proof — no real ZK proof needed.
19+
* Output roots, however, are real values fetched from an L2 archive node.
20+
*
21+
* PREREQUISITES:
22+
* 1. Deploy the multiproof stack via DeployDevWithNitro.s.sol (or DeployDevNoNitro.s.sol).
23+
* 2. Set the anchor state on MockAnchorStateRegistry to a recent block (see README.md).
24+
* 3. Generate real output roots using generate-roots.sh:
25+
*
26+
* ./scripts/multiproof/generate-roots.sh <anchor_block> <l2_rpc_url> [game_count]
27+
*
28+
* USAGE:
29+
* FACTORY_ADDRESS=0x... \
30+
* ANCHOR_STATE_REGISTRY_ADDRESS=0x... \
31+
* forge script scripts/multiproof/SeedGames.s.sol \
32+
* --rpc-url $RPC_URL --broadcast --private-key $PRIVATE_KEY
33+
*
34+
* OPTIONAL ENV VARS:
35+
* GAME_COUNT Number of games to create (default: 500)
36+
* ROOTS_FILE Path to the roots JSON from generate-roots.sh (default: roots.json)
37+
*
38+
* NOTE: All transactions must confirm within the 256-block blockhash window
39+
* (~51 min on mainnet/Sepolia) of the L1 origin block captured at simulation time.
40+
* For large game counts, consider using --slow or splitting into batches.
41+
*
42+
* ══════════════════════════════════════════════════════════════════════════════════
43+
*/
44+
45+
import { Script } from "forge-std/Script.sol";
46+
import { console2 as console } from "forge-std/console2.sol";
47+
48+
import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol";
49+
import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol";
50+
import { Claim, GameType, Hash } from "src/dispute/lib/Types.sol";
51+
52+
import { MockAnchorStateRegistry } from "./mocks/MockAnchorStateRegistry.sol";
53+
54+
contract SeedGames is Script {
55+
/// @notice Must match the AggregateVerifier deployment constants from DeployDevWithNitro/NoNitro.
56+
uint256 public constant BLOCK_INTERVAL = 600;
57+
uint256 public constant INTERMEDIATE_BLOCK_INTERVAL = 30;
58+
uint256 public constant INTERMEDIATE_ROOTS_COUNT = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL;
59+
uint32 public constant GAME_TYPE_ID = 621;
60+
61+
/// @dev Stored as state to avoid stack-too-deep in run().
62+
IDisputeGameFactory internal factory;
63+
GameType internal gameType;
64+
uint256 internal initBond;
65+
uint256 internal anchorBlock;
66+
bytes32 internal l1OriginHash;
67+
uint256 internal l1OriginNumber;
68+
bytes32[] internal allRoots;
69+
70+
function run() external {
71+
// ── Configuration
72+
// ──────────────────────────────────────────────
73+
address factoryAddr = vm.envAddress("FACTORY_ADDRESS");
74+
address asrAddr = vm.envAddress("ANCHOR_STATE_REGISTRY_ADDRESS");
75+
uint256 gameCount = vm.envOr("GAME_COUNT", uint256(500));
76+
string memory rootsPath = vm.envOr("ROOTS_FILE", string("roots.json"));
77+
78+
factory = IDisputeGameFactory(factoryAddr);
79+
gameType = GameType.wrap(GAME_TYPE_ID);
80+
initBond = factory.initBonds(gameType);
81+
82+
(, anchorBlock) = MockAnchorStateRegistry(asrAddr).getAnchorRoot();
83+
84+
// L1 origin — must remain within the blockhash window when txns execute on-chain
85+
l1OriginHash = blockhash(block.number - 1);
86+
l1OriginNumber = block.number - 1;
87+
88+
// ── Load real output roots
89+
// ─────────────────────────────────────
90+
string memory rootsJson = vm.readFile(rootsPath);
91+
allRoots = abi.decode(vm.parseJson(rootsJson, ".roots"), (bytes32[]));
92+
93+
uint256 expectedRoots = gameCount * INTERMEDIATE_ROOTS_COUNT;
94+
require(
95+
allRoots.length == expectedRoots,
96+
string.concat(
97+
"Root count mismatch: got ",
98+
vm.toString(allRoots.length),
99+
", expected ",
100+
vm.toString(expectedRoots),
101+
". Re-run generate-roots.sh with matching game count."
102+
)
103+
);
104+
105+
// ── Summary
106+
// ────────────────────────────────────────────────────
107+
console.log("=== Seeding Multiproof Games ===");
108+
console.log("Factory:", factoryAddr);
109+
console.log("AnchorStateRegistry:", asrAddr);
110+
console.log("Roots file:", rootsPath);
111+
console.log("Game count:", gameCount);
112+
console.log("Game type:", uint256(GAME_TYPE_ID));
113+
console.log("Init bond per game:", initBond);
114+
console.log("Anchor block:", anchorBlock);
115+
console.log("Total ETH required:", initBond * gameCount);
116+
117+
// ── Create chained games
118+
// ───────────────────────────────────────
119+
vm.startBroadcast();
120+
121+
(address firstGame, address lastGame) = _createGames(asrAddr, gameCount);
122+
123+
vm.stopBroadcast();
124+
125+
// ── Output
126+
// ─────────────────────────────────────────────────────
127+
uint256 l2Start = anchorBlock + BLOCK_INTERVAL;
128+
uint256 l2End = anchorBlock + BLOCK_INTERVAL * gameCount;
129+
130+
console.log("");
131+
console.log("=== Seeding Complete ===");
132+
console.log("Games created:", gameCount);
133+
console.log("First game:", firstGame);
134+
console.log("Last game:", lastGame);
135+
console.log("L2 block range start:", l2Start);
136+
console.log("L2 block range end:", l2End);
137+
138+
_writeOutput(firstGame, lastGame, gameCount, l2Start, l2End);
139+
}
140+
141+
/// @notice Creates `count` chained games, each parented to the previous one.
142+
/// @param asrAddr The AnchorStateRegistry address (parent of the first game).
143+
/// @param count The number of games to create.
144+
/// @return firstGame The address of the first game created.
145+
/// @return lastGame The address of the last game created.
146+
function _createGames(address asrAddr, uint256 count) internal returns (address firstGame, address lastGame) {
147+
address parentAddr = asrAddr;
148+
149+
for (uint256 i = 0; i < count; i++) {
150+
address game = _createSingleGame(i, parentAddr);
151+
152+
if (i == 0) firstGame = game;
153+
parentAddr = game;
154+
155+
if ((i + 1) % 100 == 0) {
156+
console.log(" Created games:", i + 1);
157+
}
158+
}
159+
160+
lastGame = parentAddr;
161+
}
162+
163+
/// @notice Creates a single game at the given index in the chain.
164+
/// @param index The zero-based index of this game in the chain.
165+
/// @param parentAddr The parent game address (or ASR address for the first game).
166+
/// @return game The address of the newly created game.
167+
function _createSingleGame(uint256 index, address parentAddr) internal returns (address game) {
168+
uint256 l2Block = anchorBlock + BLOCK_INTERVAL * (index + 1);
169+
170+
// Slice this game's intermediate roots from the flat array
171+
uint256 rootsOffset = index * INTERMEDIATE_ROOTS_COUNT;
172+
bytes32 rootClaimHash = allRoots[rootsOffset + INTERMEDIATE_ROOTS_COUNT - 1];
173+
174+
bytes memory intermediateRoots = _sliceRoots(rootsOffset);
175+
bytes memory extraData = abi.encodePacked(l2Block, parentAddr, intermediateRoots);
176+
177+
// ZK proof — MockVerifier auto-accepts any input
178+
bytes memory proof = abi.encodePacked(
179+
uint8(1), // ProofType.ZK
180+
l1OriginHash,
181+
l1OriginNumber,
182+
bytes32(0) // dummy proof payload
183+
);
184+
185+
IDisputeGame created =
186+
factory.createWithInitData{ value: initBond }(gameType, Claim.wrap(rootClaimHash), extraData, proof);
187+
188+
return address(created);
189+
}
190+
191+
/// @notice Packs INTERMEDIATE_ROOTS_COUNT roots from allRoots starting at offset.
192+
/// @param offset The starting index in allRoots.
193+
/// @return roots The abi.encodePacked intermediate roots.
194+
function _sliceRoots(uint256 offset) internal view returns (bytes memory roots) {
195+
for (uint256 j = 0; j < INTERMEDIATE_ROOTS_COUNT; j++) {
196+
roots = abi.encodePacked(roots, allRoots[offset + j]);
197+
}
198+
}
199+
200+
/// @notice Writes seeding metadata to a JSON file.
201+
function _writeOutput(
202+
address firstGame,
203+
address lastGame,
204+
uint256 gameCount,
205+
uint256 l2Start,
206+
uint256 l2End
207+
)
208+
internal
209+
{
210+
string memory key = "seeding";
211+
vm.serializeAddress(key, "firstGame", firstGame);
212+
vm.serializeAddress(key, "lastGame", lastGame);
213+
vm.serializeUint(key, "gameCount", gameCount);
214+
vm.serializeUint(key, "l2BlockStart", l2Start);
215+
string memory json = vm.serializeUint(key, "l2BlockEnd", l2End);
216+
217+
string memory outPath = string.concat("deployments/", vm.toString(block.chainid), "-seeded-games.json");
218+
vm.writeJson(json, outPath);
219+
console.log("Output saved to:", outPath);
220+
}
221+
}

0 commit comments

Comments
 (0)