Commit d9dcc280 authored by George Knee's avatar George Knee Committed by GitHub

op-proposer: ensure uniform proposal interval across restarts (#11417)

* op-proposer: ensure uniform proposal interval across restarts

closes https://github.com/ethereum-optimism/optimism/issues/11193

* move DGF query logic into FetchDGFOutput

* merge loopL2OO and loopDGF

* tweak comments

* Apply suggestions from code review
Co-authored-by: default avatarSebastian Stammler <seb@oplabs.co>

* return err instead of defaulting to sending a proposak

flatten out control flow, remove shouldPropose var

* defer l.Log.Info("loop returning")

* improve error handling and logging

* fix logging syntax error

* make DGFContract interface

harmonize how network contexts are constructed

* modify test for new DGF behavior

* fix bugs in test code

* remove OutputRetryInterval flag

* handle gameCount = 0

* finish removing OutputRetryInterval

* driver waits one proposal interval for the first ever proposal

* do not create mock unecessarily

* do not create mockL2OOContract unecessarily

* wrap and return errors instead of logging and returning

* op-proposer: Switch to modern binding style for dispute game factory (#11472)

---------
Co-authored-by: default avatarSebastian Stammler <seb@oplabs.co>
Co-authored-by: default avatarAdrian Sutton <adrian@oplabs.co>
parent 5a5dd8f4
......@@ -7,6 +7,7 @@ import (
"math/big"
"time"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
......@@ -84,7 +85,6 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
PollInterval: time.Second,
NetworkTimeout: time.Second,
ProposalInterval: cfg.ProposalInterval,
OutputRetryInterval: cfg.ProposalRetryInterval,
L2OutputOracleAddr: cfg.OutputOracleAddr,
DisputeGameFactoryAddr: cfg.DisputeGameFactoryAddr,
DisputeGameType: cfg.DisputeGameType,
......@@ -98,6 +98,7 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
Cfg: proposerConfig,
Txmgr: fakeTxMgr{from: crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey)},
L1Client: l1,
Multicaller: batching.NewMultiCaller(l1.Client(), batching.DefaultBatchSize),
RollupProvider: rollupProvider,
}
......@@ -214,8 +215,8 @@ func toCallArg(msg ethereum.CallMsg) interface{} {
func (p *L2Proposer) fetchNextOutput(t Testing) (*eth.OutputResponse, bool, error) {
if e2eutils.UseFaultProofs() {
output, err := p.driver.FetchDGFOutput(t.Ctx())
if err != nil {
output, shouldPropose, err := p.driver.FetchDGFOutput(t.Ctx())
if err != nil || !shouldPropose {
return nil, false, err
}
encodedBlockNumber := make([]byte, 32)
......@@ -250,8 +251,9 @@ func (p *L2Proposer) ActMakeProposalTx(t Testing) {
var txData []byte
if e2eutils.UseFaultProofs() {
txData, _, err = p.driver.ProposeL2OutputDGFTxData(output)
tx, err := p.driver.ProposeL2OutputDGFTxCandidate(context.Background(), output)
require.NoError(t, err)
txData = tx.TxData
} else {
txData, err = p.driver.ProposeL2OutputTxData(output)
require.NoError(t, err)
......
......@@ -165,7 +165,7 @@ func DefaultSystemConfig(t testing.TB) SystemConfig {
RoleVerif: testlog.Logger(t, log.LevelInfo).New("role", RoleVerif),
RoleSeq: testlog.Logger(t, log.LevelInfo).New("role", RoleSeq),
"batcher": testlog.Logger(t, log.LevelInfo).New("role", "batcher"),
"proposer": testlog.Logger(t, log.LevelCrit).New("role", "proposer"),
"proposer": testlog.Logger(t, log.LevelInfo).New("role", "proposer"),
},
GethOptions: map[string][]geth.GethOption{},
P2PTopology: nil, // no P2P connectivity by default
......@@ -855,15 +855,14 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
var proposerCLIConfig *l2os.CLIConfig
if e2eutils.UseFaultProofs() {
proposerCLIConfig = &l2os.CLIConfig{
L1EthRpc: sys.EthInstances[RoleL1].WSEndpoint(),
RollupRpc: sys.RollupNodes[RoleSeq].HTTPEndpoint(),
DGFAddress: config.L1Deployments.DisputeGameFactoryProxy.Hex(),
ProposalInterval: 6 * time.Second,
DisputeGameType: 254, // Fast game type
PollInterval: 50 * time.Millisecond,
OutputRetryInterval: 10 * time.Millisecond,
TxMgrConfig: newTxMgrConfig(sys.EthInstances[RoleL1].WSEndpoint(), cfg.Secrets.Proposer),
AllowNonFinalized: cfg.NonFinalizedProposals,
L1EthRpc: sys.EthInstances[RoleL1].WSEndpoint(),
RollupRpc: sys.RollupNodes[RoleSeq].HTTPEndpoint(),
DGFAddress: config.L1Deployments.DisputeGameFactoryProxy.Hex(),
ProposalInterval: 6 * time.Second,
DisputeGameType: 254, // Fast game type
PollInterval: 50 * time.Millisecond,
TxMgrConfig: newTxMgrConfig(sys.EthInstances[RoleL1].WSEndpoint(), cfg.Secrets.Proposer),
AllowNonFinalized: cfg.NonFinalizedProposals,
LogConfig: oplog.CLIConfig{
Level: log.LvlInfo,
Format: oplog.FormatText,
......@@ -871,13 +870,12 @@ func (cfg SystemConfig) Start(t *testing.T, _opts ...SystemConfigOption) (*Syste
}
} else {
proposerCLIConfig = &l2os.CLIConfig{
L1EthRpc: sys.EthInstances[RoleL1].WSEndpoint(),
RollupRpc: sys.RollupNodes[RoleSeq].HTTPEndpoint(),
L2OOAddress: config.L1Deployments.L2OutputOracleProxy.Hex(),
PollInterval: 50 * time.Millisecond,
OutputRetryInterval: 10 * time.Millisecond,
TxMgrConfig: newTxMgrConfig(sys.EthInstances[RoleL1].WSEndpoint(), cfg.Secrets.Proposer),
AllowNonFinalized: cfg.NonFinalizedProposals,
L1EthRpc: sys.EthInstances[RoleL1].WSEndpoint(),
RollupRpc: sys.RollupNodes[RoleSeq].HTTPEndpoint(),
L2OOAddress: config.L1Deployments.L2OutputOracleProxy.Hex(),
PollInterval: 50 * time.Millisecond,
TxMgrConfig: newTxMgrConfig(sys.EthInstances[RoleL1].WSEndpoint(), cfg.Secrets.Proposer),
AllowNonFinalized: cfg.NonFinalizedProposals,
LogConfig: oplog.CLIConfig{
Level: log.LvlInfo,
Format: oplog.FormatText,
......
This diff is collapsed.
package contracts
import (
"context"
"fmt"
"math/big"
"time"
"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/txmgr"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
const (
methodGameCount = "gameCount"
methodGameAtIndex = "gameAtIndex"
methodInitBonds = "initBonds"
methodCreateGame = "create"
methodVersion = "version"
methodClaim = "claimData"
)
type gameMetadata struct {
GameType uint32
Timestamp time.Time
Address common.Address
Proposer common.Address
}
type DisputeGameFactory struct {
caller *batching.MultiCaller
contract *batching.BoundContract
gameABI *abi.ABI
networkTimeout time.Duration
}
func NewDisputeGameFactory(addr common.Address, caller *batching.MultiCaller, networkTimeout time.Duration) *DisputeGameFactory {
factoryABI := snapshots.LoadDisputeGameFactoryABI()
gameABI := snapshots.LoadFaultDisputeGameABI()
return &DisputeGameFactory{
caller: caller,
contract: batching.NewBoundContract(factoryABI, addr),
gameABI: gameABI,
networkTimeout: networkTimeout,
}
}
func (f *DisputeGameFactory) Version(ctx context.Context) (string, error) {
cCtx, cancel := context.WithTimeout(ctx, f.networkTimeout)
defer cancel()
result, err := f.caller.SingleCall(cCtx, rpcblock.Latest, f.contract.Call(methodVersion))
if err != nil {
return "", fmt.Errorf("failed to get version: %w", err)
}
return result.GetString(0), nil
}
// HasProposedSince attempts to find a game with the specified game type created by the specified proposer after the
// given cut off time. If one is found, returns true and the time the game was created at.
// If no matching proposal is found, returns false, time.Time{}, nil
func (f *DisputeGameFactory) HasProposedSince(ctx context.Context, proposer common.Address, cutoff time.Time, gameType uint32) (bool, time.Time, error) {
gameCount, err := f.gameCount(ctx)
if err != nil {
return false, time.Time{}, fmt.Errorf("failed to get dispute game count: %w", err)
}
if gameCount == 0 {
return false, time.Time{}, nil
}
for idx := gameCount - 1; ; idx-- {
game, err := f.gameAtIndex(ctx, idx)
if err != nil {
return false, time.Time{}, fmt.Errorf("failed to get dispute game %d: %w", idx, err)
}
if game.Timestamp.Before(cutoff) {
// Reached a game that is before the expected cutoff, so we haven't found a suitable proposal
return false, time.Time{}, nil
}
if game.GameType == gameType && game.Proposer == proposer {
// Found a matching proposal
return true, game.Timestamp, nil
}
if idx == 0 { // Need to check here rather than in the for condition to avoid underflow
// Checked every game and didn't find a match
return false, time.Time{}, nil
}
}
}
func (f *DisputeGameFactory) ProposalTx(ctx context.Context, gameType uint32, outputRoot common.Hash, l2BlockNum uint64) (txmgr.TxCandidate, error) {
cCtx, cancel := context.WithTimeout(ctx, f.networkTimeout)
defer cancel()
result, err := f.caller.SingleCall(cCtx, rpcblock.Latest, f.contract.Call(methodInitBonds, gameType))
if err != nil {
return txmgr.TxCandidate{}, fmt.Errorf("failed to fetch init bond: %w", err)
}
initBond := result.GetBigInt(0)
call := f.contract.Call(methodCreateGame, gameType, outputRoot, common.BigToHash(big.NewInt(int64(l2BlockNum))).Bytes())
candidate, err := call.ToTxCandidate()
if err != nil {
return txmgr.TxCandidate{}, err
}
candidate.Value = initBond
return candidate, err
}
func (f *DisputeGameFactory) gameCount(ctx context.Context) (uint64, error) {
cCtx, cancel := context.WithTimeout(ctx, f.networkTimeout)
defer cancel()
result, err := f.caller.SingleCall(cCtx, rpcblock.Latest, f.contract.Call(methodGameCount))
if err != nil {
return 0, fmt.Errorf("failed to load game count: %w", err)
}
return result.GetBigInt(0).Uint64(), nil
}
func (f *DisputeGameFactory) gameAtIndex(ctx context.Context, idx uint64) (gameMetadata, error) {
cCtx, cancel := context.WithTimeout(ctx, f.networkTimeout)
defer cancel()
result, err := f.caller.SingleCall(cCtx, rpcblock.Latest, f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(idx)))
if err != nil {
return gameMetadata{}, fmt.Errorf("failed to load game %v: %w", idx, err)
}
gameType := result.GetUint32(0)
timestamp := result.GetUint64(1)
address := result.GetAddress(2)
gameContract := batching.NewBoundContract(f.gameABI, address)
cCtx, cancel = context.WithTimeout(ctx, f.networkTimeout)
defer cancel()
result, err = f.caller.SingleCall(cCtx, rpcblock.Latest, gameContract.Call(methodClaim, big.NewInt(0)))
if err != nil {
return gameMetadata{}, fmt.Errorf("failed to load root claim of game %v: %w", idx, err)
}
// We don't need most of the claim data, only the claimant which is the game proposer
claimant := result.GetAddress(2)
return gameMetadata{
GameType: gameType,
Timestamp: time.Unix(int64(timestamp), 0),
Address: address,
Proposer: claimant,
}, nil
}
package contracts
import (
"context"
"math"
"math/big"
"testing"
"time"
"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/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
var factoryAddr = common.Address{0xff, 0xff}
var proposerAddr = common.Address{0xaa, 0xbb}
func TestHasProposedSince(t *testing.T) {
cutOffTime := time.Unix(1000, 0)
t.Run("NoProposals", func(t *testing.T) {
stubRpc, factory := setupDisputeGameFactoryTest(t)
withClaims(stubRpc)
proposed, proposalTime, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0)
require.NoError(t, err)
require.False(t, proposed)
require.Equal(t, time.Time{}, proposalTime)
})
t.Run("NoMatchingProposal", func(t *testing.T) {
stubRpc, factory := setupDisputeGameFactoryTest(t)
withClaims(
stubRpc,
gameMetadata{
GameType: 0,
Timestamp: time.Unix(1600, 0),
Address: common.Address{0x22},
Proposer: common.Address{0xee}, // Wrong proposer
},
gameMetadata{
GameType: 1, // Wrong game type
Timestamp: time.Unix(1700, 0),
Address: common.Address{0x33},
Proposer: proposerAddr,
},
)
proposed, proposalTime, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0)
require.NoError(t, err)
require.False(t, proposed)
require.Equal(t, time.Time{}, proposalTime)
})
t.Run("MatchingProposalBeforeCutOff", func(t *testing.T) {
stubRpc, factory := setupDisputeGameFactoryTest(t)
withClaims(
stubRpc,
gameMetadata{
GameType: 0,
Timestamp: time.Unix(999, 0),
Address: common.Address{0x11},
Proposer: proposerAddr,
},
gameMetadata{
GameType: 0,
Timestamp: time.Unix(1600, 0),
Address: common.Address{0x22},
Proposer: common.Address{0xee}, // Wrong proposer
},
gameMetadata{
GameType: 1, // Wrong game type
Timestamp: time.Unix(1700, 0),
Address: common.Address{0x33},
Proposer: proposerAddr,
},
)
proposed, proposalTime, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0)
require.NoError(t, err)
require.False(t, proposed)
require.Equal(t, time.Time{}, proposalTime)
})
t.Run("MatchingProposalAtCutOff", func(t *testing.T) {
stubRpc, factory := setupDisputeGameFactoryTest(t)
withClaims(
stubRpc,
gameMetadata{
GameType: 0,
Timestamp: cutOffTime,
Address: common.Address{0x11},
Proposer: proposerAddr,
},
gameMetadata{
GameType: 0,
Timestamp: time.Unix(1600, 0),
Address: common.Address{0x22},
Proposer: common.Address{0xee}, // Wrong proposer
},
gameMetadata{
GameType: 1, // Wrong game type
Timestamp: time.Unix(1700, 0),
Address: common.Address{0x33},
Proposer: proposerAddr,
},
)
proposed, proposalTime, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0)
require.NoError(t, err)
require.True(t, proposed)
require.Equal(t, cutOffTime, proposalTime)
})
t.Run("MatchingProposalAfterCutOff", func(t *testing.T) {
stubRpc, factory := setupDisputeGameFactoryTest(t)
expectedProposalTime := time.Unix(1100, 0)
withClaims(
stubRpc,
gameMetadata{
GameType: 0,
Timestamp: expectedProposalTime,
Address: common.Address{0x11},
Proposer: proposerAddr,
},
gameMetadata{
GameType: 0,
Timestamp: time.Unix(1600, 0),
Address: common.Address{0x22},
Proposer: common.Address{0xee}, // Wrong proposer
},
gameMetadata{
GameType: 1, // Wrong game type
Timestamp: time.Unix(1700, 0),
Address: common.Address{0x33},
Proposer: proposerAddr,
},
)
proposed, proposalTime, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0)
require.NoError(t, err)
require.True(t, proposed)
require.Equal(t, expectedProposalTime, proposalTime)
})
t.Run("MultipleMatchingProposalAfterCutOff", func(t *testing.T) {
stubRpc, factory := setupDisputeGameFactoryTest(t)
expectedProposalTime := time.Unix(1600, 0)
withClaims(
stubRpc,
gameMetadata{
GameType: 0,
Timestamp: time.Unix(1400, 0),
Address: common.Address{0x11},
Proposer: proposerAddr,
},
gameMetadata{
GameType: 0,
Timestamp: time.Unix(1500, 0),
Address: common.Address{0x22},
Proposer: proposerAddr,
},
gameMetadata{
GameType: 0,
Timestamp: expectedProposalTime,
Address: common.Address{0x33},
Proposer: proposerAddr,
},
)
proposed, proposalTime, err := factory.HasProposedSince(context.Background(), proposerAddr, cutOffTime, 0)
require.NoError(t, err)
require.True(t, proposed)
// Should find the most recent proposal
require.Equal(t, expectedProposalTime, proposalTime)
})
}
func TestProposalTx(t *testing.T) {
stubRpc, factory := setupDisputeGameFactoryTest(t)
traceType := uint32(123)
outputRoot := common.Hash{0x01}
l2BlockNum := common.BigToHash(big.NewInt(456)).Bytes()
bond := big.NewInt(49284294829)
stubRpc.SetResponse(factoryAddr, methodInitBonds, rpcblock.Latest, []interface{}{traceType}, []interface{}{bond})
stubRpc.SetResponse(factoryAddr, methodCreateGame, rpcblock.Latest, []interface{}{traceType, outputRoot, l2BlockNum}, nil)
tx, err := factory.ProposalTx(context.Background(), traceType, outputRoot, uint64(456))
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
require.NotNil(t, tx.Value)
require.Truef(t, bond.Cmp(tx.Value) == 0, "Expected bond %v but was %v", bond, tx.Value)
}
func withClaims(stubRpc *batchingTest.AbiBasedRpc, games ...gameMetadata) {
gameAbi := snapshots.LoadFaultDisputeGameABI()
stubRpc.SetResponse(factoryAddr, methodGameCount, rpcblock.Latest, nil, []interface{}{big.NewInt(int64(len(games)))})
for i, game := range games {
stubRpc.SetResponse(factoryAddr, methodGameAtIndex, rpcblock.Latest, []interface{}{big.NewInt(int64(i))}, []interface{}{
game.GameType,
uint64(game.Timestamp.Unix()),
game.Address,
})
stubRpc.AddContract(game.Address, gameAbi)
stubRpc.SetResponse(game.Address, methodClaim, rpcblock.Latest, []interface{}{big.NewInt(0)}, []interface{}{
uint32(math.MaxUint32), // Parent address (none for root claim)
common.Address{}, // Countered by
game.Proposer, // Claimant
big.NewInt(1000), // Bond
common.Hash{0xdd}, // Claim
big.NewInt(1), // Position (gindex 1 for root position)
big.NewInt(100), // Clock
})
}
}
func setupDisputeGameFactoryTest(t *testing.T) (*batchingTest.AbiBasedRpc, *DisputeGameFactory) {
fdgAbi := snapshots.LoadDisputeGameFactoryABI()
stubRpc := batchingTest.NewAbiBasedRpc(t, factoryAddr, fdgAbi)
caller := batching.NewMultiCaller(stubRpc, batching.DefaultBatchSize)
factory := NewDisputeGameFactory(factoryAddr, caller, time.Minute)
return stubRpc, factory
}
......@@ -60,12 +60,6 @@ var (
Usage: "Interval between submitting L2 output proposals when the dispute game factory address is set",
EnvVars: prefixEnvVars("PROPOSAL_INTERVAL"),
}
OutputRetryIntervalFlag = &cli.DurationFlag{
Name: "output-retry-interval",
Usage: "Interval between retrying output fetching (DGF)",
Value: 12 * time.Second,
EnvVars: prefixEnvVars("OUTPUT_RETRY_INTERVAL"),
}
DisputeGameTypeFlag = &cli.UintFlag{
Name: "game-type",
Usage: "Dispute game type to create via the configured DisputeGameFactory",
......@@ -101,7 +95,6 @@ var optionalFlags = []cli.Flag{
L2OutputHDPathFlag,
DisputeGameFactoryAddressFlag,
ProposalIntervalFlag,
OutputRetryIntervalFlag,
DisputeGameTypeFlag,
ActiveSequencerCheckDurationFlag,
WaitNodeSyncFlag,
......
......@@ -11,7 +11,6 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
......@@ -46,23 +45,6 @@ func setupL2OutputOracle() (common.Address, *bind.TransactOpts, *backends.Simula
return from, opts, backend, contract, nil
}
// setupDisputeGameFactory deploys the DisputeGameFactory contract to a simulated backend
func setupDisputeGameFactory() (common.Address, *bind.TransactOpts, *backends.SimulatedBackend, *bindings.DisputeGameFactory, error) {
_, from, opts, backend, err := simulatedBackend()
if err != nil {
return common.Address{}, nil, nil, nil, err
}
_, _, contract, err := bindings.DeployDisputeGameFactory(
opts,
backend,
)
if err != nil {
return common.Address{}, nil, nil, nil, err
}
return from, opts, backend, contract, nil
}
// TestManualABIPacking ensure that the manual ABI packing is the same as going through the bound contract.
// We don't use the contract to transact because it does not fit our transaction management scheme, but
// we want to make sure that we don't incorrectly create the transaction data.
......@@ -92,28 +74,4 @@ func TestManualABIPacking(t *testing.T) {
require.NoError(t, err)
require.Equal(t, txData, tx.Data())
// DGF
_, opts, _, dgf, err := setupDisputeGameFactory()
require.NoError(t, err)
rng = rand.New(rand.NewSource(1234))
dgfAbi, err := bindings.DisputeGameFactoryMetaData.GetAbi()
require.NoError(t, err)
output = testutils.RandomOutputResponse(rng)
txData, err = proposeL2OutputDGFTxData(dgfAbi, uint32(0), output)
require.NoError(t, err)
opts.GasLimit = 100_000
dgfTx, err := dgf.Create(
opts,
uint32(0),
output.OutputRoot,
math.U256Bytes(new(big.Int).SetUint64(output.BlockRef.Number)),
)
require.NoError(t, err)
require.Equal(t, txData, dgfTx.Data())
}
......@@ -53,9 +53,6 @@ type CLIConfig struct {
// ProposalInterval is the delay between submitting L2 output proposals when the DGFAddress is set.
ProposalInterval time.Duration
// OutputRetryInterval is the delay between retrying output fetch if one fails.
OutputRetryInterval time.Duration
// DisputeGameType is the type of dispute game to create when submitting an output proposal.
DisputeGameType uint32
......@@ -113,7 +110,6 @@ func NewConfig(ctx *cli.Context) *CLIConfig {
PprofConfig: oppprof.ReadCLIConfig(ctx),
DGFAddress: ctx.String(flags.DisputeGameFactoryAddressFlag.Name),
ProposalInterval: ctx.Duration(flags.ProposalIntervalFlag.Name),
OutputRetryInterval: ctx.Duration(flags.OutputRetryIntervalFlag.Name),
DisputeGameType: uint32(ctx.Uint(flags.DisputeGameTypeFlag.Name)),
ActiveSequencerCheckDuration: ctx.Duration(flags.ActiveSequencerCheckDurationFlag.Name),
WaitNodeSync: ctx.Bool(flags.WaitNodeSyncFlag.Name),
......
This diff is collapsed.
......@@ -13,6 +13,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum-optimism/optimism/op-service/testutils"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
txmgrmocks "github.com/ethereum-optimism/optimism/op-service/txmgr/mocks"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
......@@ -37,6 +38,23 @@ func (m *MockL2OOContract) NextBlockNumber(opts *bind.CallOpts) (*big.Int, error
return args.Get(0).(*big.Int), args.Error(1)
}
type StubDGFContract struct {
hasProposedCount int
}
func (m *StubDGFContract) HasProposedSince(_ context.Context, _ common.Address, _ time.Time, _ uint32) (bool, time.Time, error) {
m.hasProposedCount++
return false, time.Unix(1000, 0), nil
}
func (m *StubDGFContract) ProposalTx(_ context.Context, _ uint32, _ common.Hash, _ uint64) (txmgr.TxCandidate, error) {
panic("not implemented")
}
func (m *StubDGFContract) Version(_ context.Context) (string, error) {
panic("not implemented")
}
type mockRollupEndpointProvider struct {
rollupClient *testutils.MockRollupClient
rollupClientErr error
......@@ -54,16 +72,15 @@ func (p *mockRollupEndpointProvider) RollupClient(context.Context) (dial.RollupC
func (p *mockRollupEndpointProvider) Close() {}
func setup(t *testing.T) (*L2OutputSubmitter, *mockRollupEndpointProvider, *MockL2OOContract, *txmgrmocks.TxManager, *testlog.CapturingHandler) {
func setup(t *testing.T, testName string) (*L2OutputSubmitter, *mockRollupEndpointProvider, *MockL2OOContract, *StubDGFContract, *txmgrmocks.TxManager, *testlog.CapturingHandler) {
ep := newEndpointProvider()
l2OutputOracleAddr := common.HexToAddress("0x3F8A862E63E759a77DA22d384027D21BF096bA9E")
proposerConfig := ProposerConfig{
PollInterval: time.Microsecond,
ProposalInterval: time.Microsecond,
OutputRetryInterval: time.Microsecond,
L2OutputOracleAddr: &l2OutputOracleAddr,
PollInterval: time.Microsecond,
ProposalInterval: time.Microsecond,
L2OutputOracleAddr: &l2OutputOracleAddr,
}
txmgr := txmgrmocks.NewTxManager(t)
......@@ -81,14 +98,22 @@ func setup(t *testing.T) (*L2OutputSubmitter, *mockRollupEndpointProvider, *Mock
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
l2ooContract := new(MockL2OOContract)
l2OutputSubmitter := L2OutputSubmitter{
DriverSetup: setup,
done: make(chan struct{}),
l2ooContract: l2ooContract,
l2ooABI: parsed,
ctx: ctx,
cancel: cancel,
DriverSetup: setup,
done: make(chan struct{}),
l2ooABI: parsed,
ctx: ctx,
cancel: cancel,
}
var mockDGFContract *StubDGFContract
var mockL2OOContract *MockL2OOContract
if testName == "DGF" {
mockDGFContract = new(StubDGFContract)
l2OutputSubmitter.dgfContract = mockDGFContract
} else {
mockL2OOContract = new(MockL2OOContract)
l2OutputSubmitter.l2ooContract = mockL2OOContract
}
txmgr.On("BlockNumber", mock.Anything).Return(uint64(100), nil).Once()
......@@ -101,7 +126,7 @@ func setup(t *testing.T) (*L2OutputSubmitter, *mockRollupEndpointProvider, *Mock
close(l2OutputSubmitter.done)
})
return &l2OutputSubmitter, ep, l2ooContract, txmgr, logs
return &l2OutputSubmitter, ep, mockL2OOContract, mockDGFContract, txmgr, logs
}
func TestL2OutputSubmitter_OutputRetry(t *testing.T) {
......@@ -112,10 +137,11 @@ func TestL2OutputSubmitter_OutputRetry(t *testing.T) {
{name: "DGF"},
}
proposerAddr := common.Address{0xab}
const numFails = 3
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ps, ep, l2ooContract, txmgr, logs := setup(t)
ps, ep, l2ooContract, dgfContract, txmgr, logs := setup(t, tt.name)
ep.rollupClient.On("SyncStatus").Return(&eth.SyncStatus{FinalizedL2: eth.L2BlockRef{Number: 42}}, nil).Times(numFails + 1)
ep.rollupClient.ExpectOutputAtBlock(42, nil, fmt.Errorf("TEST: failed to fetch output")).Times(numFails)
......@@ -132,19 +158,24 @@ func TestL2OutputSubmitter_OutputRetry(t *testing.T) {
nil,
)
if tt.name == "DGF" {
ps.loopDGF(ps.ctx)
} else {
txmgr.On("From").Return(common.Address{}).Times(numFails + 1)
txmgr.On("From").Return(proposerAddr).Times(numFails + 1)
if tt.name == "L2OO" {
l2ooContract.On("NextBlockNumber", mock.AnythingOfType("*bind.CallOpts")).Return(big.NewInt(42), nil).Times(numFails + 1)
ps.loopL2OO(ps.ctx)
}
ps.wg.Add(1)
ps.loop()
ep.rollupClient.AssertExpectations(t)
l2ooContract.AssertExpectations(t)
require.Len(t, logs.FindLogs(testlog.NewMessageContainsFilter("Error getting "+tt.name)), numFails)
if tt.name == "L2OO" {
l2ooContract.AssertExpectations(t)
} else {
require.Equal(t, numFails+1, dgfContract.hasProposedCount)
}
require.Len(t, logs.FindLogs(testlog.NewMessageContainsFilter("Error getting output")), numFails)
require.NotNil(t, logs.FindLog(testlog.NewMessageFilter("Proposer tx successfully published")))
require.NotNil(t, logs.FindLog(testlog.NewMessageFilter("loop"+tt.name+" returning")))
require.NotNil(t, logs.FindLog(testlog.NewMessageFilter("loop returning")))
})
}
}
......@@ -18,6 +18,7 @@ import (
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/oppprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
......@@ -32,9 +33,6 @@ type ProposerConfig struct {
PollInterval time.Duration
NetworkTimeout time.Duration
// How frequently to retry fetching an output if one fails
OutputRetryInterval time.Duration
// How frequently to post L2 outputs when the DisputeGameFactory is configured
ProposalInterval time.Duration
......@@ -92,7 +90,6 @@ func (ps *ProposerService) initFromCLIConfig(ctx context.Context, version string
ps.initMetrics(cfg)
ps.PollInterval = cfg.PollInterval
ps.OutputRetryInterval = cfg.OutputRetryInterval
ps.NetworkTimeout = cfg.TxMgrConfig.NetworkTimeout
ps.AllowNonFinalized = cfg.AllowNonFinalized
ps.WaitNodeSync = cfg.WaitNodeSync
......@@ -234,6 +231,7 @@ func (ps *ProposerService) initDriver() error {
Cfg: ps.ProposerConfig,
Txmgr: ps.TxManager,
L1Client: ps.L1Client,
Multicaller: batching.NewMultiCaller(ps.L1Client.Client(), batching.DefaultBatchSize),
RollupProvider: ps.RollupProvider,
})
if err != nil {
......
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