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,6 +19,7 @@ import ( ...@@ -19,6 +19,7 @@ import (
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots" "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "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` // The maximum number of children that will be processed during a call to `resolveClaim`
...@@ -50,6 +51,8 @@ var ( ...@@ -50,6 +51,8 @@ var (
methodClaimCredit = "claimCredit" methodClaimCredit = "claimCredit"
methodCredit = "credit" methodCredit = "credit"
methodWETH = "weth" methodWETH = "weth"
methodL2BlockNumberChallenged = "l2BlockNumberChallenged"
methodChallengeRootL2Block = "challengeRootL2Block"
) )
var ( var (
...@@ -68,6 +71,14 @@ type Proposal struct { ...@@ -68,6 +71,14 @@ type Proposal struct {
OutputRoot common.Hash 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) { func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMetricer, addr common.Address, caller *batching.MultiCaller) (FaultDisputeGameContract, error) {
contractAbi := snapshots.LoadFaultDisputeGameABI() contractAbi := snapshots.LoadFaultDisputeGameABI()
...@@ -87,6 +98,16 @@ func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMe ...@@ -87,6 +98,16 @@ func NewFaultDisputeGameContract(ctx context.Context, metrics metrics.ContractMe
contract: batching.NewBoundContract(legacyAbi, addr), contract: batching.NewBoundContract(legacyAbi, addr),
}, },
}, nil }, 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 { } else {
return &FaultDisputeGameContractLatest{ return &FaultDisputeGameContractLatest{
metrics: metrics, metrics: metrics,
...@@ -414,11 +435,25 @@ func (f *FaultDisputeGameContractLatest) vm(ctx context.Context) (*VMContract, e ...@@ -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) { 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) { 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) { 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 ...@@ -132,3 +132,11 @@ func (f *FaultDisputeGameContract080) ResolveClaimTx(claimIdx uint64) (txmgr.TxC
func (f *FaultDisputeGameContract080) resolveClaimCall(claimIdx uint64) *batching.ContractCall { func (f *FaultDisputeGameContract080) resolveClaimCall(claimIdx uint64) *batching.ContractCall {
return f.contract.Call(methodResolveClaim, new(big.Int).SetUint64(claimIdx)) 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 ...@@ -3,21 +3,26 @@ package contracts
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"math" "math"
"math/big" "math/big"
"math/rand"
"testing" "testing"
"time" "time"
contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics" contractMetrics "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" 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-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"
"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/testutils"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots" "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -34,7 +39,8 @@ type contractVersion struct { ...@@ -34,7 +39,8 @@ type contractVersion struct {
const ( const (
vers080 = "0.8.0" vers080 = "0.8.0"
versLatest = "0.18.0" vers0180 = "0.18.0"
versLatest = "1.1.0"
) )
var versions = []contractVersion{ var versions = []contractVersion{
...@@ -44,6 +50,12 @@ var versions = []contractVersion{ ...@@ -44,6 +50,12 @@ var versions = []contractVersion{
return mustParseAbi(faultDisputeGameAbi020) return mustParseAbi(faultDisputeGameAbi020)
}, },
}, },
{
version: vers0180,
loadAbi: func() *abi.ABI {
return mustParseAbi(faultDisputeGameAbi0180)
},
},
{ {
version: versLatest, version: versLatest,
loadAbi: snapshots.LoadFaultDisputeGameABI, loadAbi: snapshots.LoadFaultDisputeGameABI,
...@@ -655,23 +667,63 @@ func TestFaultDisputeGame_IsResolved(t *testing.T) { ...@@ -655,23 +667,63 @@ func TestFaultDisputeGame_IsResolved(t *testing.T) {
func TestFaultDisputeGameContractLatest_IsL2BlockNumberChallenged(t *testing.T) { func TestFaultDisputeGameContractLatest_IsL2BlockNumberChallenged(t *testing.T) {
for _, version := range versions { for _, version := range versions {
version := version version := version
t.Run(version.version, func(t *testing.T) { for _, expected := range []bool{true, false} {
_, game := setupFaultDisputeGameTest(t, version) expected := expected
challenged, err := game.IsL2BlockNumberChallenged(context.Background(), rpcblock.Latest) 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.NoError(t, err)
require.False(t, challenged) require.Equal(t, expected, challenged)
}) })
} }
}
} }
func TestFaultDisputeGameContractLatest_ChallengeL2BlockNumberTx(t *testing.T) { func TestFaultDisputeGameContractLatest_ChallengeL2BlockNumberTx(t *testing.T) {
for _, version := range versions { for _, version := range versions {
version := version version := version
t.Run(version.version, func(t *testing.T) { t.Run(version.version, func(t *testing.T) {
_, game := setupFaultDisputeGameTest(t, version) rng := rand.New(rand.NewSource(0))
tx, err := game.ChallengeL2BlockNumberTx(&faultTypes.InvalidL2BlockNumberChallenge{}) 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.ErrorIs(t, err, ErrChallengeL2BlockNotSupported)
require.Equal(t, txmgr.TxCandidate{}, tx) require.Equal(t, txmgr.TxCandidate{}, tx)
}
}) })
} }
} }
......
...@@ -442,6 +442,19 @@ func (g *OutputGameHelper) WaitForInactivity(ctx context.Context, numInactiveBlo ...@@ -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 // Mover is a function that either attacks or defends the claim at parentClaimIdx
type Mover func(parent *ClaimHelper) *ClaimHelper type Mover func(parent *ClaimHelper) *ClaimHelper
......
...@@ -784,3 +784,35 @@ func TestInvalidateProposalForFutureBlock(t *testing.T) { ...@@ -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