Sitelet https://github.com/ethereum-optimism/optimism/commit/3d72c1aba8ff64a007adce4484761d2d7be7794d
Skip to content

Commit 3d72c1a

Browse files
authored
op-dispute-mon: normalize ZK bond accounting (#22228)
Co-authored-by: wwared <541936+wwared@users.noreply.github.com>
1 parent cba37e8 commit 3d72c1a

23 files changed

Lines changed: 1306 additions & 21 deletions

‎op-challenger/game/fault/contracts/delayed_weth.go‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package contracts
33
import (
44
"context"
55
"fmt"
6+
"math"
67
"math/big"
78
"time"
89

@@ -51,10 +52,13 @@ func (d *DelayedWETHContract) GetBalanceAndDelay(ctx context.Context, block rpcb
5152
if err != nil {
5253
return nil, 0, fmt.Errorf("failed to retrieve game balance: %w", err)
5354
}
55+
if err := validateResultCount(2, len(results)); err != nil {
56+
return nil, 0, err
57+
}
5458
balance := results[0].GetBigInt(0)
5559
delaySeconds := results[1].GetBigInt(0)
56-
if !delaySeconds.IsInt64() {
57-
return nil, 0, fmt.Errorf("withdrawal delay too big for int64 %v", delaySeconds)
60+
if !delaySeconds.IsInt64() || delaySeconds.Int64() > math.MaxInt64/int64(time.Second) {
61+
return nil, 0, fmt.Errorf("withdrawal delay too big for time.Duration %v", delaySeconds)
5862
}
5963
delay := time.Duration(delaySeconds.Int64()) * time.Second
6064
return balance, delay, nil
@@ -71,7 +75,10 @@ func (d *DelayedWETHContract) GetWithdrawals(ctx context.Context, block rpcblock
7175
if err != nil {
7276
return nil, fmt.Errorf("failed to fetch withdrawals: %w", err)
7377
}
74-
withdrawals := make([]*WithdrawalRequest, len(recipients))
78+
if err := validateResultCount(len(recipients), len(results)); err != nil {
79+
return nil, err
80+
}
81+
withdrawals := make([]*WithdrawalRequest, len(results))
7582
for i, result := range results {
7683
withdrawals[i] = &WithdrawalRequest{
7784
Amount: result.GetBigInt(0),
@@ -80,3 +87,10 @@ func (d *DelayedWETHContract) GetWithdrawals(ctx context.Context, block rpcblock
8087
}
8188
return withdrawals, nil
8289
}
90+
91+
func validateResultCount(expected, actual int) error {
92+
if actual != expected {
93+
return fmt.Errorf("expected %d results but got %d", expected, actual)
94+
}
95+
return nil
96+
}

‎op-challenger/game/fault/contracts/delayed_weth_test.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package contracts
22

33
import (
44
"context"
5+
"math"
56
"math/big"
67
"testing"
78
"time"
@@ -58,6 +59,18 @@ func TestDelayedWeth_GetBalanceAndDelay(t *testing.T) {
5859
require.Equal(t, delay, actualDelay)
5960
}
6061

62+
func TestDelayedWeth_GetBalanceAndDelayRejectsDurationOverflow(t *testing.T) {
63+
stubRpc, weth := setupDelayedWethTest(t)
64+
block := rpcblock.ByNumber(482)
65+
tooManySeconds := big.NewInt(math.MaxInt64/int64(time.Second) + 1)
66+
67+
stubRpc.AddExpectedCall(batchingTest.NewGetBalanceCall(delayedWeth, block, big.NewInt(23984)))
68+
stubRpc.SetResponse(delayedWeth, methodDelay, block, nil, []interface{}{tooManySeconds})
69+
70+
_, _, err := weth.GetBalanceAndDelay(t.Context(), block)
71+
require.ErrorContains(t, err, "withdrawal delay too big for time.Duration")
72+
}
73+
6174
func setupDelayedWethTest(t *testing.T) (*batchingTest.AbiBasedRpc, *DelayedWETHContract) {
6275
delayedWethAbi := snapshots.LoadDelayedWETHABI()
6376
stubRpc := batchingTest.NewAbiBasedRpc(t, delayedWeth, delayedWethAbi)

‎op-challenger/game/fault/contracts/zkdisputegame.go‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ var (
5656
methodChallenge = "challenge"
5757
methodChallengerBond = "challengerBond"
5858
methodClaimData = "claimData"
59+
methodGameCreator = "gameCreator"
60+
methodTotalBonds = "totalBonds"
5961
)
6062

6163
type claimData struct {
@@ -73,6 +75,10 @@ type ZKDisputeGameContract interface {
7375
GetProposal(ctx context.Context) (common.Hash, uint64, error)
7476
GetChallengerMetadata(ctx context.Context, block rpcblock.Block) (ChallengerMetadata, error)
7577
GetAnchorStateRegistry(ctx context.Context, block rpcblock.Block) (common.Address, error)
78+
GetBondMetadata(ctx context.Context, block rpcblock.Block) (ZKBondMetadata, error)
79+
GetCredits(ctx context.Context, block rpcblock.Block, recipients ...common.Address) ([]*big.Int, error)
80+
GetWithdrawals(ctx context.Context, block rpcblock.Block, recipients ...common.Address) ([]*WithdrawalRequest, error)
81+
GetBalanceAndDelay(ctx context.Context, block rpcblock.Block) (*big.Int, time.Duration, common.Address, error)
7682
IsClosed(ctx context.Context) (bool, error)
7783
GetCredit(ctx context.Context, recipient common.Address) (*big.Int, gameTypes.GameStatus, error)
7884
ClaimCreditTx(ctx context.Context, recipient common.Address) (txmgr.TxCandidate, error)
@@ -238,6 +244,90 @@ type ChallengerMetadata struct {
238244
Deadline time.Time
239245
}
240246

247+
// ZKBondMetadata contains the pinned values needed to account for ZK game bonds.
248+
type ZKBondMetadata struct {
249+
GameCreator common.Address
250+
TotalBonds *big.Int
251+
ChallengerBond *big.Int
252+
}
253+
254+
func (g *ZKDisputeGameContractLatest) GetBondMetadata(ctx context.Context, block rpcblock.Block) (ZKBondMetadata, error) {
255+
defer g.metrics.StartContractRequest("GetBondMetadata")()
256+
results, err := g.multiCaller.Call(ctx, block,
257+
g.contract.Call(methodGameCreator),
258+
g.contract.Call(methodTotalBonds),
259+
g.contract.Call(methodChallengerBond),
260+
)
261+
if err != nil {
262+
return ZKBondMetadata{}, fmt.Errorf("failed to retrieve ZK bond metadata: %w", err)
263+
}
264+
if err := validateResultCount(3, len(results)); err != nil {
265+
return ZKBondMetadata{}, err
266+
}
267+
return ZKBondMetadata{
268+
GameCreator: results[0].GetAddress(0),
269+
TotalBonds: results[1].GetBigInt(0),
270+
ChallengerBond: results[2].GetBigInt(0),
271+
}, nil
272+
}
273+
274+
func (g *ZKDisputeGameContractLatest) GetCredits(ctx context.Context, block rpcblock.Block, recipients ...common.Address) ([]*big.Int, error) {
275+
defer g.metrics.StartContractRequest("GetCredits")()
276+
if len(recipients) == 0 {
277+
return []*big.Int{}, nil
278+
}
279+
calls := make([]batching.Call, 0, len(recipients))
280+
for _, recipient := range recipients {
281+
calls = append(calls, g.contract.Call(methodCredit, recipient))
282+
}
283+
results, err := g.multiCaller.Call(ctx, block, calls...)
284+
if err != nil {
285+
return nil, fmt.Errorf("failed to retrieve ZK credit: %w", err)
286+
}
287+
if err := validateResultCount(len(recipients), len(results)); err != nil {
288+
return nil, err
289+
}
290+
credits := make([]*big.Int, len(results))
291+
for i, result := range results {
292+
credits[i] = result.GetBigInt(0)
293+
}
294+
return credits, nil
295+
}
296+
297+
func (g *ZKDisputeGameContractLatest) GetWithdrawals(ctx context.Context, block rpcblock.Block, recipients ...common.Address) ([]*WithdrawalRequest, error) {
298+
defer g.metrics.StartContractRequest("GetWithdrawals")()
299+
if len(recipients) == 0 {
300+
return []*WithdrawalRequest{}, nil
301+
}
302+
delayedWETH, err := g.getDelayedWETH(ctx, block)
303+
if err != nil {
304+
return nil, err
305+
}
306+
return delayedWETH.GetWithdrawals(ctx, block, g.contract.Addr(), recipients...)
307+
}
308+
309+
func (g *ZKDisputeGameContractLatest) GetBalanceAndDelay(ctx context.Context, block rpcblock.Block) (*big.Int, time.Duration, common.Address, error) {
310+
defer g.metrics.StartContractRequest("GetBalanceAndDelay")()
311+
delayedWETH, err := g.getDelayedWETH(ctx, block)
312+
if err != nil {
313+
return nil, 0, common.Address{}, err
314+
}
315+
balance, delay, err := delayedWETH.GetBalanceAndDelay(ctx, block)
316+
if err != nil {
317+
return nil, 0, common.Address{}, err
318+
}
319+
return balance, delay, delayedWETH.Addr(), nil
320+
}
321+
322+
func (g *ZKDisputeGameContractLatest) getDelayedWETH(ctx context.Context, block rpcblock.Block) (*DelayedWETHContract, error) {
323+
defer g.metrics.StartContractRequest("GetDelayedWETH")()
324+
result, err := g.multiCaller.SingleCall(ctx, block, g.contract.Call(methodWETH))
325+
if err != nil {
326+
return nil, fmt.Errorf("failed to fetch ZK WETH address: %w", err)
327+
}
328+
return NewDelayedWETHContract(g.metrics, result.GetAddress(0), g.multiCaller), nil
329+
}
330+
241331
func (g *ZKDisputeGameContractLatest) GetChallengerMetadata(ctx context.Context, block rpcblock.Block) (ChallengerMetadata, error) {
242332
defer g.metrics.StartContractRequest("GetChallengerMetadata")()
243333
results, err := g.multiCaller.Call(ctx, block,

‎op-challenger/game/fault/contracts/zkdisputegame_test.go‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package contracts
33
import (
44
"context"
55
"errors"
6+
"math"
67
"math/big"
78
"testing"
89
"time"
@@ -26,6 +27,7 @@ const (
2627

2728
var (
2829
zkGameAddr = common.Address{0x45, 0x44, 0x43}
30+
zkWethAddr = common.Address{0x57, 0x45, 0x54, 0x48}
2931
)
3032

3133
var zkVersions = []contractVersion{
@@ -156,6 +158,141 @@ func TestZKGetAnchorStateRegistryAtPinnedBlock(t *testing.T) {
156158
}
157159
}
158160

161+
func TestZKGetBondMetadataAtPinnedBlock(t *testing.T) {
162+
for _, version := range zkVersions {
163+
t.Run(version.String(), func(t *testing.T) {
164+
stubRpc, contract := setupZKDisputeGameTest(t, version)
165+
block := rpcblock.ByHash(common.Hash{0x91})
166+
expected := ZKBondMetadata{
167+
GameCreator: common.Address{0x01},
168+
TotalBonds: big.NewInt(23),
169+
ChallengerBond: big.NewInt(7),
170+
}
171+
stubRpc.SetResponse(zkGameAddr, methodGameCreator, block, nil, []interface{}{expected.GameCreator})
172+
stubRpc.SetResponse(zkGameAddr, methodTotalBonds, block, nil, []interface{}{expected.TotalBonds})
173+
stubRpc.SetResponse(zkGameAddr, methodChallengerBond, block, nil, []interface{}{expected.ChallengerBond})
174+
175+
actual, err := contract.GetBondMetadata(t.Context(), block)
176+
require.NoError(t, err)
177+
require.Equal(t, expected, actual)
178+
})
179+
}
180+
}
181+
182+
func TestZKGetCreditsAtPinnedBlock(t *testing.T) {
183+
for _, version := range zkVersions {
184+
t.Run(version.String(), func(t *testing.T) {
185+
stubRpc, contract := setupZKDisputeGameTest(t, version)
186+
block := rpcblock.ByNumber(492)
187+
recipients := []common.Address{{0x01}, {0x02}, {0x03}}
188+
expected := []*big.Int{big.NewInt(3), big.NewInt(5), big.NewInt(8)}
189+
for i, recipient := range recipients {
190+
stubRpc.SetResponse(zkGameAddr, methodCredit, block, []interface{}{recipient}, []interface{}{expected[i]})
191+
}
192+
193+
actual, err := contract.GetCredits(t.Context(), block, recipients...)
194+
require.NoError(t, err)
195+
require.Equal(t, expected, actual)
196+
197+
empty, err := contract.GetCredits(t.Context(), block)
198+
require.NoError(t, err)
199+
require.Empty(t, empty)
200+
require.NotNil(t, empty)
201+
})
202+
}
203+
}
204+
205+
func TestZKGetWithdrawalsAtPinnedBlock(t *testing.T) {
206+
for _, version := range zkVersions {
207+
t.Run(version.String(), func(t *testing.T) {
208+
stubRpc, contract := setupZKDisputeGameTest(t, version)
209+
stubRpc.AddContract(zkWethAddr, snapshots.LoadDelayedWETHABI())
210+
block := rpcblock.ByHash(common.Hash{0x92})
211+
recipients := []common.Address{{0x01}, {0x02}}
212+
expected := []*WithdrawalRequest{
213+
{Amount: big.NewInt(11), Timestamp: big.NewInt(101)},
214+
{Amount: big.NewInt(13), Timestamp: big.NewInt(103)},
215+
}
216+
stubRpc.SetResponse(zkGameAddr, methodWETH, block, nil, []interface{}{zkWethAddr})
217+
for i, recipient := range recipients {
218+
stubRpc.SetResponse(zkWethAddr, methodWithdrawals, block, []interface{}{zkGameAddr, recipient}, []interface{}{expected[i].Amount, expected[i].Timestamp})
219+
}
220+
221+
actual, err := contract.GetWithdrawals(t.Context(), block, recipients...)
222+
require.NoError(t, err)
223+
require.Equal(t, expected, actual)
224+
225+
_, emptyContract := setupZKDisputeGameTest(t, version)
226+
empty, err := emptyContract.GetWithdrawals(t.Context(), block)
227+
require.NoError(t, err)
228+
require.Empty(t, empty)
229+
require.NotNil(t, empty)
230+
})
231+
}
232+
}
233+
234+
func TestZKGetBalanceAndDelayAtPinnedBlock(t *testing.T) {
235+
for _, version := range zkVersions {
236+
t.Run(version.String(), func(t *testing.T) {
237+
stubRpc, contract := setupZKDisputeGameTest(t, version)
238+
stubRpc.AddContract(zkWethAddr, snapshots.LoadDelayedWETHABI())
239+
block := rpcblock.ByNumber(493)
240+
balance := big.NewInt(144)
241+
delaySeconds := int64(77)
242+
stubRpc.SetResponse(zkGameAddr, methodWETH, block, nil, []interface{}{zkWethAddr})
243+
stubRpc.AddExpectedCall(batchingTest.NewGetBalanceCall(zkWethAddr, block, balance))
244+
stubRpc.SetResponse(zkWethAddr, methodDelay, block, nil, []interface{}{big.NewInt(delaySeconds)})
245+
246+
actualBalance, actualDelay, actualAddr, err := contract.GetBalanceAndDelay(t.Context(), block)
247+
require.NoError(t, err)
248+
require.Equal(t, balance, actualBalance)
249+
require.Equal(t, time.Duration(delaySeconds)*time.Second, actualDelay)
250+
require.Equal(t, zkWethAddr, actualAddr)
251+
})
252+
}
253+
}
254+
255+
func TestZKGetBalanceAndDelayRejectsDurationOverflow(t *testing.T) {
256+
for _, version := range zkVersions {
257+
t.Run(version.String(), func(t *testing.T) {
258+
stubRpc, contract := setupZKDisputeGameTest(t, version)
259+
stubRpc.AddContract(zkWethAddr, snapshots.LoadDelayedWETHABI())
260+
block := rpcblock.ByNumber(493)
261+
tooManySeconds := big.NewInt(math.MaxInt64/int64(time.Second) + 1)
262+
stubRpc.SetResponse(zkGameAddr, methodWETH, block, nil, []interface{}{zkWethAddr})
263+
stubRpc.AddExpectedCall(batchingTest.NewGetBalanceCall(zkWethAddr, block, big.NewInt(144)))
264+
stubRpc.SetResponse(zkWethAddr, methodDelay, block, nil, []interface{}{tooManySeconds})
265+
266+
_, _, _, err := contract.GetBalanceAndDelay(t.Context(), block)
267+
require.ErrorContains(t, err, "withdrawal delay too big for time.Duration")
268+
})
269+
}
270+
}
271+
272+
func TestZKGetBalanceAndDelayAcceptsMaxDuration(t *testing.T) {
273+
for _, version := range zkVersions {
274+
t.Run(version.String(), func(t *testing.T) {
275+
stubRpc, contract := setupZKDisputeGameTest(t, version)
276+
stubRpc.AddContract(zkWethAddr, snapshots.LoadDelayedWETHABI())
277+
block := rpcblock.ByNumber(493)
278+
maxSeconds := int64(math.MaxInt64 / int64(time.Second))
279+
stubRpc.SetResponse(zkGameAddr, methodWETH, block, nil, []interface{}{zkWethAddr})
280+
stubRpc.AddExpectedCall(batchingTest.NewGetBalanceCall(zkWethAddr, block, big.NewInt(144)))
281+
stubRpc.SetResponse(zkWethAddr, methodDelay, block, nil, []interface{}{big.NewInt(maxSeconds)})
282+
283+
_, actualDelay, _, err := contract.GetBalanceAndDelay(t.Context(), block)
284+
require.NoError(t, err)
285+
require.Equal(t, time.Duration(maxSeconds)*time.Second, actualDelay)
286+
})
287+
}
288+
}
289+
290+
func TestValidateResultCount(t *testing.T) {
291+
require.NoError(t, validateResultCount(2, 2))
292+
require.ErrorContains(t, validateResultCount(2, 1), "expected 2 results but got 1")
293+
require.ErrorContains(t, validateResultCount(2, 3), "expected 2 results but got 3")
294+
}
295+
159296
func TestZKGetGameRange(t *testing.T) {
160297
for _, version := range zkVersions {
161298
version := version

‎op-dispute-mon/mon/bonds/collateral.go‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,22 @@ func requiredCollateralForGame(game monTypes.BondedGame) *big.Int {
4545
}
4646
}
4747

48-
for _, unclaimedCredit := range data.Credits {
49-
required = new(big.Int).Add(required, unclaimedCredit)
48+
if _, ok := game.(*monTypes.ZKGameData); !ok {
49+
for _, unclaimedCredit := range data.Credits {
50+
required = new(big.Int).Add(required, unclaimedCredit)
51+
}
52+
return required
53+
}
54+
for _, recipient := range data.RecipientAddresses() {
55+
credit := data.Credits[recipient]
56+
if credit == nil {
57+
credit = new(big.Int)
58+
}
59+
obligation := credit
60+
if request := data.WithdrawalRequests[recipient]; request != nil && request.Amount != nil && request.Amount.Cmp(obligation) > 0 {
61+
obligation = request.Amount
62+
}
63+
required = new(big.Int).Add(required, obligation)
5064
}
5165
return required
5266
}

0 commit comments

Comments
 (0)