Skip to content

Commit 3d27a3c

Browse files
committed
add op-challenger
1 parent b59244e commit 3d27a3c

File tree

2 files changed

+340
-0
lines changed

2 files changed

+340
-0
lines changed

L2/op-challenger.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Tools
2+
A [Python script](./scripts/play-op-challenger.py) is provided to facilitate listing games in progress and simulating a dishonest actor attacking the game up to maxGameDepth.
3+
To see all available commands and options, run: `python3 ./scripts/play-op-challenger.py -h`
4+
5+
## Environment setup
6+
- Install `cast` and `make op-challenger`
7+
- Replace `OP_CHALLENGER`, `L1_RPC`, and `DISPUTE_GAME_FACTORY_PROXY` in `play-op-challenger.py` with your configurations or provide these options as command-line arguments (see help for details).
8+
9+
> In the following tutorials, `--l1-rpc`, `--fdg-addr`, and `--binpath` are optional if `OP_CHALLENGER`, `L1_RPC`, and `DISPUTE_GAME_FACTORY_PROXY` have been provided in the code.
10+
11+
## List games with absolute prestates
12+
13+
```sh
14+
python3 ./play-op-challenger.py list-games --status 1 --l1-rpc $L1_RPC --fdg-addr $DISPUTE_GAME_FACTORY_PROXY --binpath $OP_CHALLENGER_BINARY_PATH
15+
```
16+
17+
This command differs from `op-challenger`'s default `list-games` command in that you can filter games by `--status` (see help for its meaning) and the absolute prestate is queried, allowing you to check if the game's absolute prestate matches the current implementation.
18+
19+
## 1v1 actor to attack a game to maxGameDepth
20+
21+
### Attacking a game with an honest root Claim
22+
The following script simulates a dishonest actor attacking an existing game with random claims against every honest claim, itll wait any honest actor to respond to every attack using `op-challenger` after each attack, and repeat attacking until the specified maxGameDepth.
23+
24+
```sh
25+
python3 ./play-op-challenger.py attack-all --game-addr $ADDR --parent-index $INDEX --maxGameDepth 73 --pk $PRIVATE_KEY --l1-rpc $L1_RPC --fdg-addr $DISPUTE_GAME_FACTORY_PROXY --binpath $OP_CHALLENGER_BINARY_PATH
26+
```
27+
The `parent-index` is the index of the honest claim you want to start attacking. If no one has attacked it before, the default index is 0.
28+
29+
### Create a game with a dishonest root claim and attack honest challenger's Claims
30+
- First, create a game with a dishonest root claim:
31+
32+
```sh
33+
python3 test.py create-game --output-root "any value" --l2-block-num $NUMBER --pk $PK --l1-rpc $L1_RPC --fdg-addr $DISPUTE_GAME_FACTORY_PROXY --binpath $OP_CHALLENGER_BINARY_PATH
34+
```
35+
36+
- Then, attack the game after receiving the first response from any honest challenger:
37+
38+
```sh
39+
python3 ./play-op-challenger.py attack-all --game-addr $ADDR --parent-index 1 --pk $PRIVATE_KEY --l1-rpc $L1_RPC --fdg-addr $DISPUTE_GAME_FACTORY_PROXY --binpath $OP_CHALLENGER_BINARY_PATH
40+
```
41+
42+
If the game reaches `maxGameDepth` (default is 73), a dishonest actor attempting to win would need to call the game contract's `step` function themselves. However, this call will always revert, assuming the game contract is functioning correctly and enforces the rules properly.
43+
44+
## List claims
45+
46+
```sh
47+
python3 ./play-op-challenger.py list-claims --game-addr $ADDR
48+
```
49+
50+
# Grafana Monitor
51+
Incorrect Forecast Value Scenarios:
52+
53+
- Warning: Dishonest actors are attacking the game
54+
- disagree_challenger_ahead (danger): The root claim of a game is dishonest, but the honest `op-challenger` hasn't responded to the dishonest claims for a while. This might cause an incorrect output root to be updated in the `AnchorStateRegistry`.
55+
- agree_challenger_ahead: The root claim of a game is honest, but dishonest actors are challenging the game and the honest `op-challenger` hasn't responded to the dishonest actors for a while.
56+
57+
- Fatal: Dishonest actors have won the game
58+
- disagree_defender_wins: The dishonest actors have won the game and the honest actors have forfeited their bonds. Besides, the dishonest root claims have been updated in the `AnchorStateRegistry`!
59+
- agree_challenger_wins: The dishonest actors have won the game, and the honest actors have lost the game and forfeited their bonds.

L2/scripts/play-op-challenger.py

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
#!/bin/python3
2+
# dependencies: cast, op-challenger
3+
import argparse
4+
import os
5+
from dataclasses import dataclass
6+
from enum import Enum
7+
import pprint
8+
9+
OP_CHALLENGER = rf"op-challenger binary absolute path"
10+
L1_RPC = "http://88.99.30.186:8545"
11+
DISPUTE_GAME_FACTORY_PROXY = "0x4b2215d682208b2a598cb04270f96562f5ab225f"
12+
13+
14+
class GameStatus(Enum):
15+
IN_PROGRESS = 1
16+
CHALLENGER_WINS = 2
17+
DEFENDER_WINS = 3
18+
19+
20+
@dataclass
21+
class Game:
22+
gameAddr: str
23+
status: GameStatus | str = GameStatus.IN_PROGRESS
24+
gameType: int = 0 # 0:cannon, 1:permissoned, 255:fastgame
25+
index: int | None = None
26+
created: str | None = None
27+
l2BlockNum: int | None = None
28+
rootClaim: str | None = None
29+
claimsCount: int | None = None
30+
prestate: str | None = None
31+
32+
def __post_init__(self):
33+
if type(self.status) == str:
34+
if "IN_PROGRESS" in self.status:
35+
self.status = GameStatus.IN_PROGRESS
36+
if "CHALLENGER_WINS" in self.status:
37+
self.status = GameStatus.CHALLENGER_WINS
38+
if "DEFENDER_WINS" in self.status:
39+
self.status = GameStatus.DEFENDER_WINS
40+
self.setAbsolutePrestate()
41+
42+
def lenClaims(self):
43+
cmd = rf'cast call {self.gameAddr} "claimDataLen()" --rpc-url {L1_RPC}'
44+
res = os.popen(cmd).read()
45+
res = res.strip()
46+
return int(res, 16)
47+
48+
def setAbsolutePrestate(self):
49+
cmd = rf'cast call {self.gameAddr} "absolutePrestate()" --rpc-url {L1_RPC}'
50+
res = os.popen(cmd).read()
51+
res = res.strip()
52+
self.prestate = res
53+
54+
def move(self, claim, pk, parentIndex=None):
55+
if parentIndex == None:
56+
parentIndex = self.lenClaims() - 1
57+
58+
cmd = rf'''{OP_CHALLENGER} move --l1-eth-rpc {L1_RPC} --game-address {self.gameAddr} --attack --parent-index {parentIndex} --claim {claim} --private-key {pk} --mnemonic ""'''
59+
res = os.popen(cmd).read()
60+
print(f"counter {parentIndex} with claim:", claim)
61+
print(f"counter {parentIndex} move resp:", res)
62+
63+
def claimAt(self, index):
64+
cmd = rf'cast call {self.gameAddr} "claimData(uint256)" {index} --rpc-url {L1_RPC}'
65+
res = os.popen(cmd).read()
66+
res = res.strip()[2:]
67+
return {
68+
"parentIndex": res[:64],
69+
"counteredBy": res[64 : 64 * 2],
70+
"claimant": res[64 * 2 : 64 * 3],
71+
# bond:res[64*3:64*4],
72+
"claim": res[64 * 4 : 64 * 5],
73+
# position:res[64*5:64*6],
74+
# clock:res[64*6:64*7]
75+
}
76+
77+
def absolutePrestate(self):
78+
cmd = rf'cast call {self.gameAddr} "absolutePrestate()" --rpc-url {L1_RPC}'
79+
res = os.popen(cmd).read()
80+
return res.strip()
81+
82+
def list_claims(self):
83+
cmd = rf"{OP_CHALLENGER} list-claims --l1-eth-rpc {L1_RPC} --game-address {self.gameAddr}"
84+
res = os.popen(cmd).read()
85+
return res.strip()
86+
87+
def gameType(self):
88+
cmd = rf'cast call {self.gameAddr} "gameType()" --rpc-url {L1_RPC}'
89+
res = os.popen(cmd).read()
90+
return res.strip()
91+
92+
def gameStatus(self):
93+
cmd = rf'cast call {self.gameAddr} "status()" --rpc-url {L1_RPC}'
94+
res = os.popen(cmd).read()
95+
return GameStatus(int(res.strip(), 16))
96+
97+
def maxGameDepth(self):
98+
cmd = rf'cast call {self.gameAddr} "maxGameDepth()" --rpc-url {L1_RPC}'
99+
res = os.popen(cmd).read()
100+
return res.strip()
101+
102+
def attackToMaxDepth(self, parent_index, maxdepth, pk):
103+
# attack with a random false claim when honest challenger responds
104+
maxDepth = maxdepth
105+
depth = parent_index - 1
106+
while depth < maxDepth:
107+
curDepth = self.lenClaims() - 1
108+
if curDepth == depth + 1:
109+
print("got op-challenger's move:", self.claimAt(curDepth))
110+
# the first 2 hex must be 01 or 02 to meet the _verifyExecBisectionRoot requirements
111+
randClaim = f"0x012222222222222222222222222221022222222222222222222222222222{curDepth+10}01"
112+
self.move(randClaim, pk)
113+
depth += 2
114+
print(
115+
"""Max depth reached. Waiting for op-challenger (always honest) to:
116+
1. Call step() and resolve() if rootClaim is honest.
117+
2. Call resolve() (it's the dishonest actor's job to call step(), which will always revert) if rootClaim is dishonest.
118+
"""
119+
)
120+
121+
122+
def list_games(status, l1_rpc, fdg_addr, **kargs):
123+
cmd = rf"{OP_CHALLENGER} list-games --l1-eth-rpc {l1_rpc} --game-factory-address {fdg_addr}"
124+
res = os.popen(cmd).read()
125+
res = res.strip()
126+
res = res.split("\n")
127+
res = res[1:] # remove the header fields and last line
128+
games = []
129+
for line in res:
130+
idx = line[:4].strip()
131+
gameAddr = line[4 : 4 + 43].strip()
132+
gameType = line[4 + 43 : 4 + 43 + 5].strip()
133+
created = line[4 + 43 + 5 : 4 + 43 + 5 + 21].strip()
134+
l2BlockNum = line[4 + 43 + 5 + 21 : 4 + 43 + 5 + 22 + 15].strip()
135+
rootClaim = line[4 + 43 + 5 + 22 + 15 : 4 + 43 + 5 + 22 + 15 + 66].strip()
136+
claimsCount = line[
137+
4 + 43 + 5 + 22 + 15 + 67 : 4 + 43 + 5 + 22 + 15 + 67 + 6
138+
].strip()
139+
status = line[
140+
4 + 43 + 5 + 22 + 15 + 67 + 7 : 4 + 43 + 5 + 22 + 15 + 67 + 7 + 14
141+
].strip()
142+
game = Game(
143+
gameAddr=gameAddr,
144+
gameType=gameType,
145+
status=status,
146+
index=idx,
147+
created=created,
148+
l2BlockNum=l2BlockNum,
149+
rootClaim=rootClaim,
150+
claimsCount=claimsCount,
151+
prestate=None,
152+
)
153+
games.append(game)
154+
if status != 0:
155+
games = list(filter(lambda x: x.status == status, games))
156+
157+
pprint.pprint(games)
158+
159+
160+
def attack_game_to_max_depth(game_addr, parent_index, maxdepth, pk, **kargs):
161+
game = Game(gameAddr=game_addr)
162+
game.attackToMaxDepth(parent_index, maxdepth, pk)
163+
164+
165+
def list_claims(game_addr, **kargs):
166+
game = Game(gameAddr=game_addr)
167+
claims = game.list_claims()
168+
print(claims)
169+
170+
171+
def create_game(output_root, l2_block_num, pk, **kargs):
172+
cmd = rf"{OP_CHALLENGER} create-game --l1-eth-rpc {L1_RPC} --game-factory-address {DISPUTE_GAME_FACTORY_PROXY} --output-root {output_root} --l2-block-num {l2_block_num} --private-key {pk}"
173+
res = os.popen(cmd).read()
174+
print(res)
175+
176+
177+
def main():
178+
global L1_RPC, OP_CHALLENGER, DISPUTE_GAME_FACTORY_PROXY
179+
parser = argparse.ArgumentParser(description="Game management script")
180+
parser.add_argument("--l1-rpc", type=str, default=L1_RPC, help="l1 EL rpc url")
181+
parser.add_argument(
182+
"--fdg-addr",
183+
type=str,
184+
default=DISPUTE_GAME_FACTORY_PROXY,
185+
help="Dispute game factory address",
186+
)
187+
parser.add_argument(
188+
"--binpath",
189+
type=str,
190+
default=OP_CHALLENGER,
191+
help="Op-challenger absolute binary path",
192+
)
193+
subparsers = parser.add_subparsers(dest="command")
194+
195+
# Subparser for the list-games command
196+
parser_list = subparsers.add_parser("list-games", help="List all games")
197+
parser_list.add_argument(
198+
"--status",
199+
type=int,
200+
default=1,
201+
choices=[0, 1, 2, 3],
202+
help="Game status, 0:all, 1:in-progress, 2:challenger-wins, 3:defender-wins",
203+
)
204+
parser_list.set_defaults(func=list_games)
205+
206+
# Subparser for the attack command
207+
parser_attack = subparsers.add_parser(
208+
"attack-all",
209+
help="Attack a game for every counter claim by honest challenger to maxDepth with random claim values",
210+
)
211+
parser_attack.add_argument(
212+
"--game-addr",
213+
type=str,
214+
required=True,
215+
help="Contract address of the game to attack, e.g.:0x11",
216+
)
217+
parser_attack.add_argument(
218+
"--pk", type=str, required=True, help="Private key, e.g.:0x11"
219+
)
220+
parser_attack.add_argument(
221+
"--parent-index",
222+
type=int,
223+
default=0,
224+
help="Parent index to start attacking from, usually claimsCount-1",
225+
)
226+
parser_attack.add_argument(
227+
"--maxdepth", type=int, default=73, help="MaxGameDepth of the attack ending"
228+
)
229+
parser_attack.set_defaults(func=attack_game_to_max_depth)
230+
231+
# Subparser for the list-claims command
232+
parser_claims = subparsers.add_parser(
233+
"list-claims",
234+
help="List claims for a given game",
235+
)
236+
parser_claims.add_argument(
237+
"--game-addr",
238+
type=str,
239+
required=True,
240+
help="Contract address of the game to attack, e.g.:0x11",
241+
)
242+
parser_claims.set_defaults(func=list_claims)
243+
244+
# Subparser for the create-game command
245+
parser_create_game = subparsers.add_parser(
246+
"create-game",
247+
help="create a game with specified root claim and l2 block number",
248+
)
249+
parser_create_game.add_argument(
250+
"--output-root",
251+
type=str,
252+
default="0xffff",
253+
help="The output root for the fault dispute game, e.g.: 0x11",
254+
)
255+
parser_create_game.add_argument(
256+
"--l2-block-num",
257+
type=str,
258+
required=True,
259+
help="The l2 block number for the game",
260+
)
261+
parser_create_game.add_argument(
262+
"--pk", type=str, required=True, help="Private key, e.g.: 0x11"
263+
)
264+
parser_create_game.set_defaults(func=create_game)
265+
266+
args = parser.parse_args()
267+
if args.l1_rpc:
268+
L1_RPC = args.l1_rpc
269+
if args.binpath:
270+
OP_CHALLENGER = args.binpath
271+
if args.fdg_addr:
272+
DISPUTE_GAME_FACTORY_PROXY = args.fdg_addr
273+
274+
if args.command:
275+
args.func(**vars(args))
276+
else:
277+
parser.print_help()
278+
279+
280+
if __name__ == "__main__":
281+
main()

0 commit comments

Comments
 (0)