Commit 1dd35b7c authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Implement block number challenge calls in contract bindings (#10462)

parent 0ad08a37
......@@ -19,37 +19,40 @@ import (
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp"
)
// The maximum number of children that will be processed during a call to `resolveClaim`
var maxChildChecks = big.NewInt(512)
var (
methodVersion = "version"
methodMaxClockDuration = "maxClockDuration"
methodMaxGameDepth = "maxGameDepth"
methodAbsolutePrestate = "absolutePrestate"
methodStatus = "status"
methodRootClaim = "rootClaim"
methodClaimCount = "claimDataLen"
methodClaim = "claimData"
methodL1Head = "l1Head"
methodResolvedSubgames = "resolvedSubgames"
methodResolve = "resolve"
methodResolveClaim = "resolveClaim"
methodAttack = "attack"
methodDefend = "defend"
methodStep = "step"
methodAddLocalData = "addLocalData"
methodVM = "vm"
methodStartingBlockNumber = "startingBlockNumber"
methodStartingRootHash = "startingRootHash"
methodSplitDepth = "splitDepth"
methodL2BlockNumber = "l2BlockNumber"
methodRequiredBond = "getRequiredBond"
methodClaimCredit = "claimCredit"
methodCredit = "credit"
methodWETH = "weth"
methodVersion = "version"
methodMaxClockDuration = "maxClockDuration"
methodMaxGameDepth = "maxGameDepth"
methodAbsolutePrestate = "absolutePrestate"
methodStatus = "status"
methodRootClaim = "rootClaim"
methodClaimCount = "claimDataLen"
methodClaim = "claimData"
methodL1Head = "l1Head"
methodResolvedSubgames = "resolvedSubgames"
methodResolve = "resolve"
methodResolveClaim = "resolveClaim"
methodAttack = "attack"
methodDefend = "defend"
methodStep = "step"
methodAddLocalData = "addLocalData"
methodVM = "vm"
methodStartingBlockNumber = "startingBlockNumber"
methodStartingRootHash = "startingRootHash"
methodSplitDepth = "splitDepth"
methodL2BlockNumber = "l2BlockNumber"
methodRequiredBond = "getRequiredBond"
methodClaimCredit = "claimCredit"
methodCredit = "credit"
methodWETH = "weth"
methodL2BlockNumberChallenged = "l2BlockNumberChallenged"
methodChallengeRootL2Block = "challengeRootL2Block"
)
var (
......@@ -68,6 +71,14 @@ type Proposal struct {
OutputRoot common.Hash
}
// outputRootProof is designed to match the solidity OutputRootProof struct.
type outputRootProof struct {
Version [32]byte
StateRoot [32]byte
MessagePasserStorageRoot [32]byte
LatestBlockhash [32]byte
}
func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMetricer, addr common.Address, caller *batching.MultiCaller) (FaultDisputeGameContract, error) {
contractAbi := snapshots.LoadFaultDisputeGameABI()
......@@ -87,6 +98,16 @@ func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMe
contract: batching.NewBoundContract(legacyAbi, addr),
},
}, nil
} else if strings.HasPrefix(version, "0.18.") {
// Detected an older version of contracts, use a compatibility shim.
legacyAbi := mustParseAbi(faultDisputeGameAbi0180)
return &FaultDisputeGameContract0180{
FaultDisputeGameContractLatest: FaultDisputeGameContractLatest{
metrics: metrics,
multiCaller: caller,
contract: batching.NewBoundContract(legacyAbi, addr),
},
}, nil
} else {
return &FaultDisputeGameContractLatest{
metrics: metrics,
......@@ -414,11 +435,25 @@ func (f *FaultDisputeGameContractLatest) vm(ctx context.Context) (*VMContract, e
}
func (f *FaultDisputeGameContractLatest) IsL2BlockNumberChallenged(ctx context.Context, block rpcblock.Block) (bool, error) {
return false, nil
defer f.metrics.StartContractRequest("IsL2BlockNumberChallenged")()
result, err := f.multiCaller.SingleCall(ctx, block, f.contract.Call(methodL2BlockNumberChallenged))
if err != nil {
return false, fmt.Errorf("failed to fetch block number challenged: %w", err)
}
return result.GetBool(0), nil
}
func (f *FaultDisputeGameContractLatest) ChallengeL2BlockNumberTx(challenge *types.InvalidL2BlockNumberChallenge) (txmgr.TxCandidate, error) {
return txmgr.TxCandidate{}, ErrChallengeL2BlockNotSupported
headerRlp, err := rlp.EncodeToBytes(challenge.Header)
if err != nil {
return txmgr.TxCandidate{}, fmt.Errorf("failed to serialize header: %w", err)
}
return f.contract.Call(methodChallengeRootL2Block, outputRootProof{
Version: challenge.Output.Version,
StateRoot: challenge.Output.StateRoot,
MessagePasserStorageRoot: challenge.Output.WithdrawalStorageRoot,
LatestBlockhash: challenge.Output.BlockRef.Hash,
}, headerRlp).ToTxCandidate()
}
func (f *FaultDisputeGameContractLatest) AttackTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error) {
......
package contracts
import (
"context"
_ "embed"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
)
//go:embed abis/FaultDisputeGame-0.18.1.json
var faultDisputeGameAbi0180 []byte
type FaultDisputeGameContract0180 struct {
FaultDisputeGameContractLatest
}
func (f *FaultDisputeGameContract0180) IsL2BlockNumberChallenged(_ context.Context, _ rpcblock.Block) (bool, error) {
return false, nil
}
func (f *FaultDisputeGameContract0180) ChallengeL2BlockNumberTx(_ *types.InvalidL2BlockNumberChallenge) (txmgr.TxCandidate, error) {
return txmgr.TxCandidate{}, ErrChallengeL2BlockNotSupported
}
......@@ -132,3 +132,11 @@ func (f *FaultDisputeGameContract080) ResolveClaimTx(claimIdx uint64) (txmgr.TxC
func (f *FaultDisputeGameContract080) resolveClaimCall(claimIdx uint64) *batching.ContractCall {
return f.contract.Call(methodResolveClaim, new(big.Int).SetUint64(claimIdx))
}
func (f *FaultDisputeGameContract080) IsL2BlockNumberChallenged(_ context.Context, _ rpcblock.Block) (bool, error) {
return false, nil
}
func (f *FaultDisputeGameContract080) ChallengeL2BlockNumberTx(_ *types.InvalidL2BlockNumberChallenge) (txmgr.TxCandidate, error) {
return txmgr.TxCandidate{}, ErrChallengeL2BlockNotSupported
}
......@@ -3,21 +3,26 @@ package contracts
import (
"context"
"errors"
"fmt"
"math"
"math/big"
"math/rand"
"testing"
"time"
contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/eth"
"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/testutils"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/require"
)
......@@ -34,7 +39,8 @@ type contractVersion struct {
const (
vers080 = "0.8.0"
versLatest = "0.18.0"
vers0180 = "0.18.0"
versLatest = "1.1.0"
)
var versions = []contractVersion{
......@@ -44,6 +50,12 @@ var versions = []contractVersion{
return mustParseAbi(faultDisputeGameAbi020)
},
},
{
version: vers0180,
loadAbi: func() *abi.ABI {
return mustParseAbi(faultDisputeGameAbi0180)
},
},
{
version: versLatest,
loadAbi: snapshots.LoadFaultDisputeGameABI,
......@@ -655,12 +667,22 @@ func TestFaultDisputeGame_IsResolved(t *testing.T) {
func TestFaultDisputeGameContractLatest_IsL2BlockNumberChallenged(t *testing.T) {
for _, version := range versions {
version := version
t.Run(version.version, func(t *testing.T) {
_, game := setupFaultDisputeGameTest(t, version)
challenged, err := game.IsL2BlockNumberChallenged(context.Background(), rpcblock.Latest)
require.NoError(t, err)
require.False(t, challenged)
})
for _, expected := range []bool{true, false} {
expected := expected
t.Run(fmt.Sprintf("%v-%v", version.version, expected), func(t *testing.T) {
block := rpcblock.ByHash(common.Hash{0x43})
stubRpc, game := setupFaultDisputeGameTest(t, version)
supportsL2BlockNumChallenge := version.version != vers080 && version.version != vers0180
if supportsL2BlockNumChallenge {
stubRpc.SetResponse(fdgAddr, methodL2BlockNumberChallenged, block, nil, []interface{}{expected})
} else if expected {
t.Skip("Can't have challenged L2 block number on this contract version")
}
challenged, err := game.IsL2BlockNumberChallenged(context.Background(), block)
require.NoError(t, err)
require.Equal(t, expected, challenged)
})
}
}
}
......@@ -668,10 +690,40 @@ func TestFaultDisputeGameContractLatest_ChallengeL2BlockNumberTx(t *testing.T) {
for _, version := range versions {
version := version
t.Run(version.version, func(t *testing.T) {
_, game := setupFaultDisputeGameTest(t, version)
tx, err := game.ChallengeL2BlockNumberTx(&faultTypes.InvalidL2BlockNumberChallenge{})
require.ErrorIs(t, err, ErrChallengeL2BlockNotSupported)
require.Equal(t, txmgr.TxCandidate{}, tx)
rng := rand.New(rand.NewSource(0))
stubRpc, game := setupFaultDisputeGameTest(t, version)
challenge := &faultTypes.InvalidL2BlockNumberChallenge{
Output: &eth.OutputResponse{
Version: eth.Bytes32{},
OutputRoot: eth.Bytes32{0xaa},
BlockRef: eth.L2BlockRef{Hash: common.Hash{0xbb}},
WithdrawalStorageRoot: common.Hash{0xcc},
StateRoot: common.Hash{0xdd},
},
Header: testutils.RandomHeader(rng),
}
supportsL2BlockNumChallenge := version.version != vers080 && version.version != vers0180
if supportsL2BlockNumChallenge {
headerRlp, err := rlp.EncodeToBytes(challenge.Header)
require.NoError(t, err)
stubRpc.SetResponse(fdgAddr, methodChallengeRootL2Block, rpcblock.Latest, []interface{}{
outputRootProof{
Version: challenge.Output.Version,
StateRoot: challenge.Output.StateRoot,
MessagePasserStorageRoot: challenge.Output.WithdrawalStorageRoot,
LatestBlockhash: challenge.Output.BlockRef.Hash,
},
headerRlp,
}, nil)
}
tx, err := game.ChallengeL2BlockNumberTx(challenge)
if supportsL2BlockNumChallenge {
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
} else {
require.ErrorIs(t, err, ErrChallengeL2BlockNotSupported)
require.Equal(t, txmgr.TxCandidate{}, tx)
}
})
}
}
......
......@@ -442,6 +442,19 @@ func (g *OutputGameHelper) WaitForInactivity(ctx context.Context, numInactiveBlo
}
}
func (g *OutputGameHelper) WaitForL2BlockNumberChallenged(ctx context.Context) {
g.T.Logf("Waiting for game %v to have L2 block number challenged", g.Addr)
caller := batching.NewMultiCaller(g.System.NodeClient("l1").Client(), batching.DefaultBatchSize)
contract, err := contracts.NewFaultDisputeGameContract(ctx, contractMetrics.NoopContractMetrics, g.Addr, caller)
g.Require.NoError(err)
timedCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
err = wait.For(timedCtx, time.Second, func() (bool, error) {
return contract.IsL2BlockNumberChallenged(ctx, rpcblock.Latest)
})
g.Require.NoError(err, "L2 block number was not challenged in time")
}
// Mover is a function that either attacks or defends the claim at parentClaimIdx
type Mover func(parent *ClaimHelper) *ClaimHelper
......
......@@ -784,3 +784,35 @@ func TestInvalidateProposalForFutureBlock(t *testing.T) {
})
}
}
func TestInvalidateCorrectProposalFutureBlock(t *testing.T) {
op_e2e.InitParallel(t, op_e2e.UsesCannon)
ctx := context.Background()
// Spin up the system without the batcher so the safe head doesn't advance
sys, l1Client := StartFaultDisputeSystem(t, WithBatcherStopped(), WithSequencerWindowSize(100000))
t.Cleanup(sys.Close)
// Create a dispute game factory helper.
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
// No batches submitted so safe head is genesis
output, err := sys.RollupClient("sequencer").OutputAtBlock(ctx, 0)
require.NoError(t, err, "Failed to get output at safe head")
// Create a dispute game with an output root that is valid at `safeHead`, but that claims to correspond to block
// `safeHead.Number + 10000`. This is dishonest, because this block does not exist yet.
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", 10_000, common.Hash(output.OutputRoot), disputegame.WithFutureProposal())
// Start the honest challenger.
game.StartChallenger(ctx, "Honest", challenger.WithPrivKey(sys.Cfg.Secrets.Bob))
game.WaitForL2BlockNumberChallenged(ctx)
// Time travel past when the game will be resolvable.
sys.TimeTravelClock.AdvanceTime(game.MaxClockDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
// The game should resolve as `CHALLENGER_WINS` always, because the root claim signifies a claim that does not exist
// yet in the L2 chain.
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
game.LogGameData(ctx)
}
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