Commit 7340cb39 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Simulate ClaimCredit transaction before sending actual transaction (#9932)

* op-challenger: Tidy up TxSender

Don't return receipts as they always ignored
Return an error when a transaction publishes but reverts rather than logging and ignoring
Add a method that returns errors for each individual transaction

* op-challenger: Simulate ClaimCredit transaction before sending actual transaction

Detects when credit is reported as available but is actually still locked by the DelayedWETH contract.

---------
Co-authored-by: default avatarrefcell <abigger87@gmail.com>
parent 0b7b81c3
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -21,8 +22,8 @@ type BondClaimMetrics interface { ...@@ -21,8 +22,8 @@ type BondClaimMetrics interface {
} }
type BondContract interface { type BondContract interface {
GetCredit(ctx context.Context, receipient common.Address) (*big.Int, types.GameStatus, error) GetCredit(ctx context.Context, recipient common.Address) (*big.Int, types.GameStatus, error)
ClaimCredit(receipient common.Address) (txmgr.TxCandidate, error) ClaimCreditTx(ctx context.Context, recipient common.Address) (txmgr.TxCandidate, error)
} }
type BondContractCreator func(game types.GameMetadata) (BondContract, error) type BondContractCreator func(game types.GameMetadata) (BondContract, error)
...@@ -77,12 +78,15 @@ func (c *Claimer) claimBond(ctx context.Context, game types.GameMetadata, addr c ...@@ -77,12 +78,15 @@ func (c *Claimer) claimBond(ctx context.Context, game types.GameMetadata, addr c
return nil return nil
} }
candidate, err := contract.ClaimCredit(addr) candidate, err := contract.ClaimCreditTx(ctx, addr)
if err != nil { if err != nil {
return fmt.Errorf("failed to create credit claim tx: %w", err) return fmt.Errorf("failed to create credit claim tx: %w", err)
} }
if err = c.txSender.SendAndWaitSimple("claim credit", candidate); err != nil { if err = c.txSender.SendAndWaitSimple("claim credit", candidate); errors.Is(err, contracts.ErrSimulationFailed) {
c.logger.Debug("Credit still locked", "game", game.Proxy, "addr", addr)
return nil
} else if err != nil {
return fmt.Errorf("failed to claim credit: %w", err) return fmt.Errorf("failed to claim credit: %w", err)
} }
......
...@@ -151,6 +151,6 @@ func (s *stubBondContract) GetCredit(_ context.Context, addr common.Address) (*b ...@@ -151,6 +151,6 @@ func (s *stubBondContract) GetCredit(_ context.Context, addr common.Address) (*b
return big.NewInt(s.credit[addr]), s.status, nil return big.NewInt(s.credit[addr]), s.status, nil
} }
func (s *stubBondContract) ClaimCredit(_ common.Address) (txmgr.TxCandidate, error) { func (s *stubBondContract) ClaimCreditTx(_ context.Context, _ common.Address) (txmgr.TxCandidate, error) {
return txmgr.TxCandidate{}, nil return txmgr.TxCandidate{}, nil
} }
...@@ -2,6 +2,7 @@ package contracts ...@@ -2,6 +2,7 @@ package contracts
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"math" "math"
"math/big" "math/big"
...@@ -42,6 +43,8 @@ var ( ...@@ -42,6 +43,8 @@ var (
methodWETH = "weth" methodWETH = "weth"
) )
var ErrSimulationFailed = errors.New("tx simulation failed")
type FaultDisputeGameContract struct { type FaultDisputeGameContract struct {
metrics metrics.ContractMetricer metrics metrics.ContractMetricer
multiCaller *batching.MultiCaller multiCaller *batching.MultiCaller
...@@ -183,9 +186,13 @@ func (f *FaultDisputeGameContract) GetCredits(ctx context.Context, block rpcbloc ...@@ -183,9 +186,13 @@ func (f *FaultDisputeGameContract) GetCredits(ctx context.Context, block rpcbloc
return credits, nil return credits, nil
} }
func (f *FaultDisputeGameContract) ClaimCredit(recipient common.Address) (txmgr.TxCandidate, error) { func (f *FaultDisputeGameContract) ClaimCreditTx(ctx context.Context, recipient common.Address) (txmgr.TxCandidate, error) {
defer f.metrics.StartContractRequest("ClaimCredit")() defer f.metrics.StartContractRequest("ClaimCredit")()
call := f.contract.Call(methodClaimCredit, recipient) call := f.contract.Call(methodClaimCredit, recipient)
_, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, call)
if err != nil {
return txmgr.TxCandidate{}, fmt.Errorf("%w: %v", ErrSimulationFailed, err.Error())
}
return call.ToTxCandidate() return call.ToTxCandidate()
} }
......
...@@ -2,6 +2,7 @@ package contracts ...@@ -2,6 +2,7 @@ package contracts
import ( import (
"context" "context"
"errors"
"math" "math"
"math/big" "math/big"
"testing" "testing"
...@@ -13,6 +14,7 @@ import ( ...@@ -13,6 +14,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/sources/batching" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test" batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -429,6 +431,28 @@ func TestFaultDisputeGame_GetCredits(t *testing.T) { ...@@ -429,6 +431,28 @@ func TestFaultDisputeGame_GetCredits(t *testing.T) {
} }
} }
func TestFaultDisputeGame_ClaimCreditTx(t *testing.T) {
t.Run("Success", func(t *testing.T) {
stubRpc, game := setupFaultDisputeGameTest(t)
addr := common.Address{0xaa}
stubRpc.SetResponse(fdgAddr, methodClaimCredit, rpcblock.Latest, []interface{}{addr}, nil)
tx, err := game.ClaimCreditTx(context.Background(), addr)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
})
t.Run("SimulationFails", func(t *testing.T) {
stubRpc, game := setupFaultDisputeGameTest(t)
addr := common.Address{0xaa}
stubRpc.SetError(fdgAddr, methodClaimCredit, rpcblock.Latest, []interface{}{addr}, errors.New("still locked"))
tx, err := game.ClaimCreditTx(context.Background(), addr)
require.ErrorIs(t, err, ErrSimulationFailed)
require.Equal(t, txmgr.TxCandidate{}, tx)
})
}
func setupFaultDisputeGameTest(t *testing.T) (*batchingTest.AbiBasedRpc, *FaultDisputeGameContract) { func setupFaultDisputeGameTest(t *testing.T) (*batchingTest.AbiBasedRpc, *FaultDisputeGameContract) {
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi() fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err) require.NoError(t, err)
......
...@@ -75,10 +75,10 @@ func TestBondContracts(t *testing.T) { ...@@ -75,10 +75,10 @@ func TestBondContracts(t *testing.T) {
type stubBondContract struct{} type stubBondContract struct{}
func (s *stubBondContract) GetCredit(ctx context.Context, receipient common.Address) (*big.Int, types.GameStatus, error) { func (s *stubBondContract) GetCredit(_ context.Context, _ common.Address) (*big.Int, types.GameStatus, error) {
panic("not supported") panic("not supported")
} }
func (s *stubBondContract) ClaimCredit(receipient common.Address) (txmgr.TxCandidate, error) { func (s *stubBondContract) ClaimCreditTx(_ context.Context, _ common.Address) (txmgr.TxCandidate, error) {
panic("not supported") panic("not supported")
} }
...@@ -23,6 +23,7 @@ type expectedCall struct { ...@@ -23,6 +23,7 @@ type expectedCall struct {
args []interface{} args []interface{}
packedArgs []byte packedArgs []byte
outputs []interface{} outputs []interface{}
err error
} }
func (c *expectedCall) Matches(rpcMethod string, args ...interface{}) error { func (c *expectedCall) Matches(rpcMethod string, args ...interface{}) error {
...@@ -66,7 +67,7 @@ func (c *expectedCall) Matches(rpcMethod string, args ...interface{}) error { ...@@ -66,7 +67,7 @@ func (c *expectedCall) Matches(rpcMethod string, args ...interface{}) error {
return nil return nil
} }
func (c *expectedCall) Execute(t *testing.T, out interface{}) { func (c *expectedCall) Execute(t *testing.T, out interface{}) error {
output, err := c.abiMethod.Outputs.Pack(c.outputs...) output, err := c.abiMethod.Outputs.Pack(c.outputs...)
require.NoErrorf(t, err, "Invalid outputs for method %v: %v", c.abiMethod.Name, c.outputs) require.NoErrorf(t, err, "Invalid outputs for method %v: %v", c.abiMethod.Name, c.outputs)
...@@ -75,6 +76,7 @@ func (c *expectedCall) Execute(t *testing.T, out interface{}) { ...@@ -75,6 +76,7 @@ func (c *expectedCall) Execute(t *testing.T, out interface{}) {
j, err := json.Marshal(hexutil.Bytes(output)) j, err := json.Marshal(hexutil.Bytes(output))
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, json.Unmarshal(j, out)) require.NoError(t, json.Unmarshal(j, out))
return c.err
} }
func (c *expectedCall) String() string { func (c *expectedCall) String() string {
...@@ -107,6 +109,24 @@ func (l *AbiBasedRpc) abi(to common.Address) *abi.ABI { ...@@ -107,6 +109,24 @@ func (l *AbiBasedRpc) abi(to common.Address) *abi.ABI {
return abi return abi
} }
func (l *AbiBasedRpc) SetError(to common.Address, method string, block rpcblock.Block, expected []interface{}, callErr error) {
if expected == nil {
expected = []interface{}{}
}
abiMethod, ok := l.abi(to).Methods[method]
require.Truef(l.t, ok, "No method: %v", method)
packedArgs, err := abiMethod.Inputs.Pack(expected...)
require.NoErrorf(l.t, err, "Invalid expected arguments for method %v: %v", method, expected)
l.AddExpectedCall(&expectedCall{
abiMethod: abiMethod,
to: to,
block: block,
args: expected,
packedArgs: packedArgs,
outputs: []interface{}{},
err: callErr,
})
}
func (l *AbiBasedRpc) SetResponse(to common.Address, method string, block rpcblock.Block, expected []interface{}, output []interface{}) { func (l *AbiBasedRpc) SetResponse(to common.Address, method string, block rpcblock.Block, expected []interface{}, output []interface{}) {
if expected == nil { if expected == nil {
expected = []interface{}{} expected = []interface{}{}
......
...@@ -19,7 +19,7 @@ import ( ...@@ -19,7 +19,7 @@ import (
type ExpectedRpcCall interface { type ExpectedRpcCall interface {
fmt.Stringer fmt.Stringer
Matches(rpcMethod string, args ...interface{}) error Matches(rpcMethod string, args ...interface{}) error
Execute(t *testing.T, out interface{}) Execute(t *testing.T, out interface{}) error
} }
type RpcStub struct { type RpcStub struct {
...@@ -50,8 +50,7 @@ func (r *RpcStub) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error ...@@ -50,8 +50,7 @@ func (r *RpcStub) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error
func (r *RpcStub) CallContext(_ context.Context, out interface{}, method string, args ...interface{}) error { func (r *RpcStub) CallContext(_ context.Context, out interface{}, method string, args ...interface{}) error {
call := r.findExpectedCall(method, args...) call := r.findExpectedCall(method, args...)
call.Execute(r.t, out) return call.Execute(r.t, out)
return nil
} }
func (r *RpcStub) findExpectedCall(rpcMethod string, args ...interface{}) ExpectedRpcCall { func (r *RpcStub) findExpectedCall(rpcMethod string, args ...interface{}) ExpectedRpcCall {
...@@ -91,12 +90,13 @@ func (c *GenericExpectedCall) Matches(rpcMethod string, args ...interface{}) err ...@@ -91,12 +90,13 @@ func (c *GenericExpectedCall) Matches(rpcMethod string, args ...interface{}) err
return nil return nil
} }
func (c *GenericExpectedCall) Execute(t *testing.T, out interface{}) { func (c *GenericExpectedCall) Execute(t *testing.T, out interface{}) error {
// I admit I do not understand Go reflection. // I admit I do not understand Go reflection.
// So leverage json.Unmarshal to set the out value correctly. // So leverage json.Unmarshal to set the out value correctly.
j, err := json.Marshal(c.result) j, err := json.Marshal(c.result)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, json.Unmarshal(j, out)) require.NoError(t, json.Unmarshal(j, out))
return nil
} }
func (c *GenericExpectedCall) String() string { func (c *GenericExpectedCall) String() string {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment