Skip to content

Commit 7316c37

Browse files
committed
feat: update withdrawal command wip
1 parent 198df93 commit 7316c37

File tree

3 files changed

+247
-7
lines changed

3 files changed

+247
-7
lines changed

op-chain-ops/cmd/withdrawal/README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,22 @@ valid proposal is made for a L2 block at or after the initiating transaction was
2727
go run . prove --l1 <l1-el-rpc> --l2 <l2-el-rpc> --tx <init-tx-hash> --portal-address <portal-addr> --private-key <private-key>
2828
```
2929

30-
When proving super roots, you'll need to provide additional flags:
30+
When proving super roots, you have two options:
3131

32+
**Option 1: Using dispute game address** (recommended - reads proof from on-chain):
33+
34+
```shell
35+
go run . prove --l1 <l1-el-rpc> --l2 <l2-el-rpc> --tx <init-tx-hash> --portal-address <portal-addr> --private-key <private-key> \
36+
--dispute-game <dispute-game-proxy-address> --rollup.config <path-to-rollup-config>
3237
```
33-
shell
34-
go run . prove --l1 <l1-el-rpc> --l2 <l2-el-rpc> --tx <init-tx-hash> --portal-address <portal-addr> --private-key <private-key>\
38+
39+
This option reads the super root proof directly from the `SuperFaultDisputeGame` contract's `extraData()`,
40+
eliminating the need for supervisor-rpc and depset flags.
41+
42+
**Option 2: Using supervisor RPC** (legacy - requires supervisor, rollup config, and depset):
43+
44+
```shell
45+
go run . prove --l1 <l1-el-rpc> --l2 <l2-el-rpc> --tx <init-tx-hash> --portal-address <portal-addr> --private-key <private-key> \
3546
--supervisor <supervisor-rpc> --rollup.config <path-to-rollup-config> --depset <path-to-dependency-set-json>
3647
```
3748

op-chain-ops/cmd/withdrawal/prove.go

Lines changed: 195 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"math/big"
78

89
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
910
opnode_bindings "github.com/ethereum-optimism/optimism/op-node/bindings"
1011
bindingspreview "github.com/ethereum-optimism/optimism/op-node/bindings/preview"
12+
"github.com/ethereum-optimism/optimism/op-node/rollup"
1113
"github.com/ethereum-optimism/optimism/op-node/withdrawals"
1214
op_service "github.com/ethereum-optimism/optimism/op-service"
1315
"github.com/ethereum-optimism/optimism/op-service/apis"
1416
oplog "github.com/ethereum-optimism/optimism/op-service/log"
1517
"github.com/ethereum-optimism/optimism/op-service/txintent/bindings"
1618
"github.com/ethereum-optimism/optimism/op-service/txintent/contractio"
1719
"github.com/ethereum-optimism/optimism/op-service/txmgr"
20+
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
21+
"github.com/ethereum/go-ethereum"
1822
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1923
"github.com/ethereum/go-ethereum/common"
2024
"github.com/ethereum/go-ethereum/ethclient"
@@ -57,6 +61,11 @@ var (
5761
Usage: "Path to the rollup config of the target chain. Only required for proving using super roots.",
5862
EnvVars: op_service.PrefixEnvVar(EnvVarPrefix, "ROLLUP_CONFIG"),
5963
}
64+
DisputeGameFlag = &cli.StringFlag{
65+
Name: "dispute-game",
66+
Usage: "Address of SuperFaultDisputeGame. When provided, reads super root proof from on-chain extraData instead of supervisor-rpc. Requires --rollup.config but not --supervisor or --depset.",
67+
EnvVars: op_service.PrefixEnvVar(EnvVarPrefix, "DISPUTE_GAME"),
68+
}
6069
)
6170

6271
func ProveWithdrawal(ctx *cli.Context) error {
@@ -130,10 +139,39 @@ func ProveWithdrawal(ctx *cli.Context) error {
130139
return err
131140
}
132141
} else {
133-
logger.Info("Proving withdrawal using super root proof")
134-
txData, err = txDataForSuperRootProof(ctx, l1EthClient, proofClient, l2Client, txHash, factory, portal)
135-
if err != nil {
136-
return err
142+
// Check if --dispute-game flag is provided for the new flow
143+
if disputeGameStr := ctx.String(DisputeGameFlag.Name); disputeGameStr != "" {
144+
logger.Info("Proving withdrawal using super root from dispute game extraData")
145+
disputeGameAddr := common.HexToAddress(disputeGameStr)
146+
147+
// Load rollup config (still required for timestamp→block number conversion)
148+
rollupCfg, err := loadRollupConfig(ctx, RollupConfigFlag.Name)
149+
if err != nil {
150+
return fmt.Errorf("failed to load rollup config: %w", err)
151+
}
152+
153+
txData, err = txDataForSuperRootProofFromGame(
154+
ctx.Context,
155+
l1Client,
156+
l1EthClient,
157+
proofClient,
158+
l2Client,
159+
txHash,
160+
disputeGameAddr,
161+
portalAddr,
162+
portal,
163+
rollupCfg,
164+
)
165+
if err != nil {
166+
return err
167+
}
168+
} else {
169+
// Existing supervisor-based flow
170+
logger.Info("Proving withdrawal using super root from supervisor")
171+
txData, err = txDataForSuperRootProof(ctx, l1EthClient, proofClient, l2Client, txHash, factory, portal)
172+
if err != nil {
173+
return err
174+
}
137175
}
138176
}
139177

@@ -242,6 +280,158 @@ func txDataForSuperRootProof(ctx *cli.Context, l1EthClient apis.EthClient, proof
242280
return txData, nil
243281
}
244282

283+
// txDataForSuperRootProofFromGame builds withdrawal proof tx data by reading the super root proof
284+
// directly from a SuperFaultDisputeGame's extraData, without requiring supervisor-rpc or depset.
285+
func txDataForSuperRootProofFromGame(
286+
ctx context.Context,
287+
l1Client *ethclient.Client,
288+
l1EthClient apis.EthClient,
289+
proofClient *gethclient.Client,
290+
l2Client *ethclient.Client,
291+
txHash common.Hash,
292+
disputeGameAddr common.Address,
293+
portalAddr common.Address,
294+
portal *bindingspreview.OptimismPortal2,
295+
rollupCfg *rollup.Config,
296+
) ([]byte, error) {
297+
// Load ABI and prepare contract calls
298+
gameABI := snapshots.LoadSuperFaultDisputeGameABI()
299+
300+
// Call extraData() to get encoded super root proof
301+
extraDataCallData, err := gameABI.Pack("extraData")
302+
if err != nil {
303+
return nil, fmt.Errorf("failed to pack extraData call: %w", err)
304+
}
305+
extraDataResult, err := l1Client.CallContract(ctx, ethereum.CallMsg{
306+
To: &disputeGameAddr,
307+
Data: extraDataCallData,
308+
}, nil)
309+
if err != nil {
310+
return nil, fmt.Errorf("failed to call extraData: %w", err)
311+
}
312+
313+
// Unpack extraData result (returns bytes)
314+
unpackedExtra, err := gameABI.Unpack("extraData", extraDataResult)
315+
if err != nil {
316+
return nil, fmt.Errorf("failed to unpack extraData: %w", err)
317+
}
318+
if len(unpackedExtra) == 0 {
319+
return nil, errors.New("extraData returned empty result")
320+
}
321+
extraDataBytes, ok := unpackedExtra[0].([]byte)
322+
if !ok {
323+
return nil, errors.New("extraData result is not []byte")
324+
}
325+
326+
// Decode super root proof from extraData
327+
superRootProof, err := withdrawals.DecodeSuperRootProof(extraDataBytes)
328+
if err != nil {
329+
return nil, fmt.Errorf("failed to decode super root proof: %w", err)
330+
}
331+
332+
// Get target L2 chain ID from portal
333+
targetChainID, err := l2ChainIDForPortal(ctx, l1EthClient, portal)
334+
if err != nil {
335+
return nil, fmt.Errorf("failed to get target chain ID from portal: %w", err)
336+
}
337+
338+
// Find output root index for target chain in the super root proof
339+
var outputRootIndex *big.Int
340+
for i, outputRoot := range superRootProof.OutputRoots {
341+
if outputRoot.ChainID.Uint64() == targetChainID {
342+
outputRootIndex = big.NewInt(int64(i))
343+
break
344+
}
345+
}
346+
if outputRootIndex == nil {
347+
return nil, fmt.Errorf("target chain ID %d not found in super root proof", targetChainID)
348+
}
349+
350+
// Get L2 sequence number (timestamp) from the dispute game
351+
seqNumCallData, err := gameABI.Pack("l2SequenceNumber")
352+
if err != nil {
353+
return nil, fmt.Errorf("failed to pack l2SequenceNumber call: %w", err)
354+
}
355+
seqNumResult, err := l1Client.CallContract(ctx, ethereum.CallMsg{
356+
To: &disputeGameAddr,
357+
Data: seqNumCallData,
358+
}, nil)
359+
if err != nil {
360+
return nil, fmt.Errorf("failed to call l2SequenceNumber: %w", err)
361+
}
362+
unpackedSeq, err := gameABI.Unpack("l2SequenceNumber", seqNumResult)
363+
if err != nil {
364+
return nil, fmt.Errorf("failed to unpack l2SequenceNumber: %w", err)
365+
}
366+
if len(unpackedSeq) == 0 {
367+
return nil, errors.New("l2SequenceNumber returned empty result")
368+
}
369+
l2SequenceNumber, ok := unpackedSeq[0].(*big.Int)
370+
if !ok {
371+
return nil, errors.New("l2SequenceNumber result is not *big.Int")
372+
}
373+
374+
// Convert sequence number (timestamp) to L2 block number using rollup config
375+
l2BlockNumber, err := rollupCfg.TargetBlockNumber(l2SequenceNumber.Uint64())
376+
if err != nil {
377+
return nil, fmt.Errorf("failed to get L2 block number from sequence number: %w", err)
378+
}
379+
380+
// Fetch the L2 header at that block
381+
l2Header, err := l2Client.HeaderByNumber(ctx, new(big.Int).SetUint64(l2BlockNumber))
382+
if err != nil {
383+
return nil, fmt.Errorf("failed to get L2 header: %w", err)
384+
}
385+
386+
// Get withdrawal receipt and parse MessagePassed event
387+
receipt, err := l2Client.TransactionReceipt(ctx, txHash)
388+
if err != nil {
389+
return nil, fmt.Errorf("failed to get withdrawal receipt: %w", err)
390+
}
391+
ev, err := withdrawals.ParseMessagePassed(receipt)
392+
if err != nil {
393+
return nil, fmt.Errorf("failed to parse withdrawal event: %w", err)
394+
}
395+
396+
// Build the withdrawal storage proof
397+
withdrawalProof, storageRoot, err := withdrawals.GetWithdrawalProof(ctx, proofClient, ev, l2Header)
398+
if err != nil {
399+
return nil, fmt.Errorf("failed to get withdrawal proof: %w", err)
400+
}
401+
402+
// Pack the proveWithdrawalTransaction call data
403+
txData, err := w3.MustNewFunc("proveWithdrawalTransaction("+
404+
"(uint256 Nonce, address Sender, address Target, uint256 Value, uint256 GasLimit, bytes Data),"+
405+
"address DisputeGameProxy,"+
406+
"uint256 OutputRootIndex,"+
407+
"(bytes1 Version, uint64 Timestamp, (uint256 ChainID, bytes32 Root)[] OutputRoots),"+
408+
"(bytes32 Version, bytes32 StateRoot, bytes32 MessagePasserStorageRoot, bytes32 LatestBlockhash),"+
409+
"bytes[])", "").EncodeArgs(
410+
bindingspreview.TypesWithdrawalTransaction{
411+
Nonce: ev.Nonce,
412+
Sender: ev.Sender,
413+
Target: ev.Target,
414+
Value: ev.Value,
415+
GasLimit: ev.GasLimit,
416+
Data: ev.Data,
417+
},
418+
disputeGameAddr,
419+
outputRootIndex,
420+
superRootProof,
421+
opnode_bindings.TypesOutputRootProof{
422+
Version: [32]byte{},
423+
StateRoot: l2Header.Root,
424+
MessagePasserStorageRoot: storageRoot,
425+
LatestBlockhash: l2Header.Hash(),
426+
},
427+
withdrawalProof,
428+
)
429+
if err != nil {
430+
return nil, fmt.Errorf("failed to pack prove withdrawal transaction: %w", err)
431+
}
432+
return txData, nil
433+
}
434+
245435
func l2ChainIDForPortal(ctx context.Context, l1EthClient apis.EthClient, portal *bindingspreview.OptimismPortal2) (uint64, error) {
246436
systemConfigAddr, err := portal.SystemConfig(&bind.CallOpts{Context: ctx})
247437
if err != nil {
@@ -265,6 +455,7 @@ func proveFlags() []cli.Flag {
265455
SupervisorFlag,
266456
DepSetFlag,
267457
RollupConfigFlag,
458+
DisputeGameFlag,
268459
}
269460
cliFlags = append(cliFlags, txmgr.CLIFlagsWithDefaults(EnvVarPrefix, txmgr.DefaultChallengerFlagValues)...)
270461
cliFlags = append(cliFlags, oplog.CLIFlags(EnvVarPrefix)...)

op-node/withdrawals/utils.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package withdrawals
33
import (
44
"bytes"
55
"context"
6+
"encoding/binary"
67
"errors"
78
"fmt"
89
"math/big"
@@ -66,6 +67,43 @@ type SuperRootProof struct {
6667
OutputRoots []SuperRootProofOutputRoot
6768
}
6869

70+
// DecodeSuperRootProof decodes SuperFaultDisputeGame extraData bytes into SuperRootProof.
71+
// Format: 1 byte version (0x01) + 8 bytes timestamp (big-endian) + N*(32 bytes chainId + 32 bytes root)
72+
func DecodeSuperRootProof(data []byte) (SuperRootProof, error) {
73+
if len(data) < 9 {
74+
return SuperRootProof{}, errors.New("extraData too short: must be at least 9 bytes")
75+
}
76+
if data[0] != 0x01 {
77+
return SuperRootProof{}, fmt.Errorf("invalid super root version: expected 0x01, got 0x%02x", data[0])
78+
}
79+
80+
timestamp := binary.BigEndian.Uint64(data[1:9])
81+
remaining := data[9:]
82+
83+
if len(remaining) == 0 {
84+
return SuperRootProof{}, errors.New("extraData contains no output roots")
85+
}
86+
if len(remaining)%64 != 0 {
87+
return SuperRootProof{}, fmt.Errorf("invalid extraData length: %d bytes after header not divisible by 64", len(remaining))
88+
}
89+
90+
numRoots := len(remaining) / 64
91+
outputRoots := make([]SuperRootProofOutputRoot, numRoots)
92+
for i := 0; i < numRoots; i++ {
93+
off := i * 64
94+
outputRoots[i] = SuperRootProofOutputRoot{
95+
ChainID: new(big.Int).SetBytes(remaining[off : off+32]),
96+
Root: common.BytesToHash(remaining[off+32 : off+64]),
97+
}
98+
}
99+
100+
return SuperRootProof{
101+
Version: [1]byte{0x01},
102+
Timestamp: timestamp,
103+
OutputRoots: outputRoots,
104+
}, nil
105+
}
106+
69107
type ProvenWithdrawalParametersSuperRoots struct {
70108
Nonce *big.Int
71109
Sender common.Address

0 commit comments

Comments
 (0)