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 (
"fmt"
"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-service/txmgr"
"github.com/ethereum/go-ethereum/common"
......@@ -21,8 +22,8 @@ type BondClaimMetrics interface {
}
type BondContract interface {
GetCredit(ctx context.Context, receipient common.Address) (*big.Int, types.GameStatus, error)
ClaimCredit(receipient common.Address) (txmgr.TxCandidate, error)
GetCredit(ctx context.Context, recipient common.Address) (*big.Int, types.GameStatus, error)
ClaimCreditTx(ctx context.Context, recipient common.Address) (txmgr.TxCandidate, 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
return nil
}
candidate, err := contract.ClaimCredit(addr)
candidate, err := contract.ClaimCreditTx(ctx, addr)
if err != nil {
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)
}
......
......@@ -151,6 +151,6 @@ func (s *stubBondContract) GetCredit(_ context.Context, addr common.Address) (*b
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
}
......@@ -2,6 +2,7 @@ package contracts
import (
"context"
"errors"
"fmt"
"math"
"math/big"
......@@ -42,6 +43,8 @@ var (
methodWETH = "weth"
)
var ErrSimulationFailed = errors.New("tx simulation failed")
type FaultDisputeGameContract struct {
metrics metrics.ContractMetricer
multiCaller *batching.MultiCaller
......@@ -183,9 +186,13 @@ func (f *FaultDisputeGameContract) GetCredits(ctx context.Context, block rpcbloc
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")()
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()
}
......
......@@ -2,6 +2,7 @@ package contracts
import (
"context"
"errors"
"math"
"math/big"
"testing"
......@@ -13,6 +14,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
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/stretchr/testify/require"
)
......@@ -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) {
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err)
......
......@@ -75,10 +75,10 @@ func TestBondContracts(t *testing.T) {
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")
}
func (s *stubBondContract) ClaimCredit(receipient common.Address) (txmgr.TxCandidate, error) {
func (s *stubBondContract) ClaimCreditTx(_ context.Context, _ common.Address) (txmgr.TxCandidate, error) {
panic("not supported")
}
......@@ -23,6 +23,7 @@ type expectedCall struct {
args []interface{}
packedArgs []byte
outputs []interface{}
err error
}
func (c *expectedCall) Matches(rpcMethod string, args ...interface{}) error {
......@@ -66,7 +67,7 @@ func (c *expectedCall) Matches(rpcMethod string, args ...interface{}) error {
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...)
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{}) {
j, err := json.Marshal(hexutil.Bytes(output))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(j, out))
return c.err
}
func (c *expectedCall) String() string {
......@@ -107,6 +109,24 @@ func (l *AbiBasedRpc) abi(to common.Address) *abi.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{}) {
if expected == nil {
expected = []interface{}{}
......
......@@ -19,7 +19,7 @@ import (
type ExpectedRpcCall interface {
fmt.Stringer
Matches(rpcMethod string, args ...interface{}) error
Execute(t *testing.T, out interface{})
Execute(t *testing.T, out interface{}) error
}
type RpcStub struct {
......@@ -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 {
call := r.findExpectedCall(method, args...)
call.Execute(r.t, out)
return nil
return call.Execute(r.t, out)
}
func (r *RpcStub) findExpectedCall(rpcMethod string, args ...interface{}) ExpectedRpcCall {
......@@ -91,12 +90,13 @@ func (c *GenericExpectedCall) Matches(rpcMethod string, args ...interface{}) err
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.
// So leverage json.Unmarshal to set the out value correctly.
j, err := json.Marshal(c.result)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(j, out))
return nil
}
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