Commit d066dd49 authored by clabby's avatar clabby Committed by GitHub

feat(op-proposer): `DisputeGameFactory` support (#8689)

* draft: `op-proposer` `DisputeGameFactory` support

* Add `ProposalInterval` option

flag names

* add game type configuration

* @sebastianst: Use `errors.New` over `fmt.Errorf`

* @sebastianst: var name nit

* @sebastianst: l2oo addr ptr consistency

* @sebastianst: error bubble

* @sebastianst: break out nests

* @sebastianst: defer cancel

* lint

* Hide DGF flags

* fix service init
parent 8a3b808d
......@@ -26,9 +26,12 @@ import (
)
type ProposerCfg struct {
OutputOracleAddr common.Address
ProposerKey *ecdsa.PrivateKey
AllowNonFinalized bool
OutputOracleAddr *common.Address
DisputeGameFactoryAddr *common.Address
ProposalInterval time.Duration
DisputeGameType uint8
ProposerKey *ecdsa.PrivateKey
AllowNonFinalized bool
}
type L2Proposer struct {
......@@ -49,21 +52,27 @@ type fakeTxMgr struct {
func (f fakeTxMgr) From() common.Address {
return f.from
}
func (f fakeTxMgr) BlockNumber(_ context.Context) (uint64, error) {
panic("unimplemented")
}
func (f fakeTxMgr) Send(_ context.Context, _ txmgr.TxCandidate) (*types.Receipt, error) {
panic("unimplemented")
}
func (f fakeTxMgr) Close() {
}
func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Client, rollupCl *sources.RollupClient) *L2Proposer {
proposerConfig := proposer.ProposerConfig{
PollInterval: time.Second,
NetworkTimeout: time.Second,
L2OutputOracleAddr: cfg.OutputOracleAddr,
AllowNonFinalized: cfg.AllowNonFinalized,
PollInterval: time.Second,
NetworkTimeout: time.Second,
ProposalInterval: cfg.ProposalInterval,
L2OutputOracleAddr: cfg.OutputOracleAddr,
DisputeGameFactoryAddr: cfg.DisputeGameFactoryAddr,
DisputeGameType: cfg.DisputeGameType,
AllowNonFinalized: cfg.AllowNonFinalized,
}
rollupProvider, err := dial.NewStaticL2RollupProviderFromExistingRollup(rollupCl)
require.NoError(t, err)
......@@ -76,9 +85,13 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
RollupProvider: rollupProvider,
}
if cfg.OutputOracleAddr == nil {
panic("L2OutputOracle address must be set in op-e2e test harness. The DisputeGameFactory is not yet supported as a proposal destination.")
}
dr, err := proposer.NewL2OutputSubmitter(driverSetup)
require.NoError(t, err)
contract, err := bindings.NewL2OutputOracleCaller(cfg.OutputOracleAddr, l1)
contract, err := bindings.NewL2OutputOracleCaller(*cfg.OutputOracleAddr, l1)
require.NoError(t, err)
address := crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey)
......@@ -93,7 +106,7 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
contract: contract,
address: address,
privKey: cfg.ProposerKey,
contractAddr: cfg.OutputOracleAddr,
contractAddr: *cfg.OutputOracleAddr,
}
}
......
......@@ -55,7 +55,7 @@ func RunProposerTest(gt *testing.T, deltaTimeOffset *hexutil.Uint64) {
}, rollupSeqCl, miner.EthClient(), seqEngine.EthClient(), seqEngine.EngineClient(t, sd.RollupCfg))
proposer := NewL2Proposer(t, log, &ProposerCfg{
OutputOracleAddr: sd.DeploymentsL1.L2OutputOracleProxy,
OutputOracleAddr: &sd.DeploymentsL1.L2OutputOracleProxy,
ProposerKey: dp.Secrets.Proposer,
AllowNonFinalized: false,
}, miner.EthClient(), sequencer.RollupClient())
......
......@@ -72,7 +72,7 @@ func runCrossLayerUserTest(gt *testing.T, test hardforkScheduledTest) {
BatcherKey: dp.Secrets.Batcher,
}, seq.RollupClient(), miner.EthClient(), seqEngine.EthClient(), seqEngine.EngineClient(t, sd.RollupCfg))
proposer := NewL2Proposer(t, log, &ProposerCfg{
OutputOracleAddr: sd.DeploymentsL1.L2OutputOracleProxy,
OutputOracleAddr: &sd.DeploymentsL1.L2OutputOracleProxy,
ProposerKey: dp.Secrets.Proposer,
AllowNonFinalized: true,
}, miner.EthClient(), seq.RollupClient())
......
......@@ -32,13 +32,13 @@ var (
Usage: "HTTP provider URL for the rollup node. A comma-separated list enables the active rollup provider.",
EnvVars: prefixEnvVars("ROLLUP_RPC"),
}
// Optional flags
L2OOAddressFlag = &cli.StringFlag{
Name: "l2oo-address",
Usage: "Address of the L2OutputOracle contract",
EnvVars: prefixEnvVars("L2OO_ADDRESS"),
}
// Optional flags
PollIntervalFlag = &cli.DurationFlag{
Name: "poll-interval",
Usage: "How frequently to poll L2 for new blocks",
......@@ -50,6 +50,25 @@ var (
Usage: "Allow the proposer to submit proposals for L2 blocks derived from non-finalized L1 blocks.",
EnvVars: prefixEnvVars("ALLOW_NON_FINALIZED"),
}
DisputeGameFactoryAddressFlag = &cli.StringFlag{
Name: "dgf-address",
Usage: "Address of the DisputeGameFactory contract",
EnvVars: prefixEnvVars("DGF_ADDRESS"),
Hidden: true,
}
ProposalIntervalFlag = &cli.DurationFlag{
Name: "proposal-interval",
Usage: "Interval between submitting L2 output proposals when the DGFAddress is set",
EnvVars: prefixEnvVars("PROPOSAL_INTERVAL"),
Hidden: true,
}
DisputeGameTypeFlag = &cli.UintFlag{
Name: "dg-type",
Usage: "Dispute game type to create via the configured DisputeGameFactory",
Value: 0,
EnvVars: prefixEnvVars("DG_TYPE"),
Hidden: true,
}
// Legacy Flags
L2OutputHDPathFlag = txmgr.L2OutputHDPathFlag
)
......@@ -57,13 +76,16 @@ var (
var requiredFlags = []cli.Flag{
L1EthRpcFlag,
RollupRpcFlag,
L2OOAddressFlag,
}
var optionalFlags = []cli.Flag{
L2OOAddressFlag,
PollIntervalFlag,
AllowNonFinalizedFlag,
L2OutputHDPathFlag,
DisputeGameFactoryAddressFlag,
ProposalIntervalFlag,
DisputeGameTypeFlag,
}
func init() {
......
package proposer
import (
"crypto/ecdsa"
"math/big"
"math/rand"
"testing"
......@@ -10,24 +11,35 @@ 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"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"
)
// setupL2OutputOracle deploys the L2 Output Oracle contract to a simulated backend
func setupL2OutputOracle() (common.Address, *bind.TransactOpts, *backends.SimulatedBackend, *bindings.L2OutputOracle, error) {
privateKey, err := crypto.GenerateKey()
func simulatedBackend() (privateKey *ecdsa.PrivateKey, address common.Address, opts *bind.TransactOpts, backend *backends.SimulatedBackend, err error) {
privateKey, err = crypto.GenerateKey()
if err != nil {
return common.Address{}, nil, nil, nil, err
return nil, common.Address{}, nil, nil, err
}
from := crypto.PubkeyToAddress(privateKey.PublicKey)
opts, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1337))
opts, err = bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1337))
if err != nil {
return nil, common.Address{}, nil, nil, err
}
backend = backends.NewSimulatedBackend(core.GenesisAlloc{from: {Balance: big.NewInt(params.Ether)}}, 50_000_000)
return privateKey, from, opts, backend, nil
}
// setupL2OutputOracle deploys the L2 Output Oracle contract to a simulated backend
func setupL2OutputOracle() (common.Address, *bind.TransactOpts, *backends.SimulatedBackend, *bindings.L2OutputOracle, error) {
_, from, opts, backend, err := simulatedBackend()
if err != nil {
return common.Address{}, nil, nil, nil, err
}
backend := backends.NewSimulatedBackend(core.GenesisAlloc{from: {Balance: big.NewInt(params.Ether)}}, 50_000_000)
_, _, contract, err := bindings.DeployL2OutputOracle(
opts,
backend,
......@@ -44,26 +56,44 @@ 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.
func TestManualABIPacking(t *testing.T) {
_, opts, _, contract, err := setupL2OutputOracle()
// L2OO
_, opts, _, l2oo, err := setupL2OutputOracle()
require.NoError(t, err)
rng := rand.New(rand.NewSource(1234))
abi, err := bindings.L2OutputOracleMetaData.GetAbi()
l2ooAbi, err := bindings.L2OutputOracleMetaData.GetAbi()
require.NoError(t, err)
output := testutils.RandomOutputResponse(rng)
txData, err := proposeL2OutputTxData(abi, output)
txData, err := proposeL2OutputTxData(l2ooAbi, output)
require.NoError(t, err)
// set a gas limit to disable gas estimation. The invariantes that the L2OO tries to uphold
// are not maintained in this test.
opts.GasLimit = 100_000
tx, err := contract.ProposeL2Output(
tx, err := l2oo.ProposeL2Output(
opts,
output.OutputRoot,
new(big.Int).SetUint64(output.BlockRef.Number),
......@@ -72,4 +102,28 @@ 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, uint8(0), output)
require.NoError(t, err)
opts.GasLimit = 100_000
dgfTx, err := dgf.Create(
opts,
uint8(0),
output.OutputRoot,
math.U256Bytes(new(big.Int).SetUint64(output.BlockRef.Number)),
)
require.NoError(t, err)
require.Equal(t, txData, dgfTx.Data())
}
package proposer
import (
"errors"
"time"
"github.com/urfave/cli/v2"
......@@ -45,6 +46,15 @@ type CLIConfig struct {
MetricsConfig opmetrics.CLIConfig
PprofConfig oppprof.CLIConfig
// DGFAddress is the DisputeGameFactory contract address.
DGFAddress string
// ProposalInterval is the delay between submitting L2 output proposals when the DGFAddress is set.
ProposalInterval time.Duration
// DisputeGameType is the type of dispute game to create when submitting an output proposal.
DisputeGameType uint8
}
func (c *CLIConfig) Check() error {
......@@ -60,6 +70,17 @@ func (c *CLIConfig) Check() error {
if err := c.TxMgrConfig.Check(); err != nil {
return err
}
if c.DGFAddress != "" && c.L2OOAddress != "" {
return errors.New("both the `DisputeGameFactory` and `L2OutputOracle` addresses were provided")
}
if c.DGFAddress != "" && c.ProposalInterval == 0 {
return errors.New("the `DisputeGameFactory` address was provided but the `ProposalInterval` was not set")
}
if c.ProposalInterval != 0 && c.DGFAddress == "" {
return errors.New("the `ProposalInterval` was provided but the `DisputeGameFactory` address was not set")
}
return nil
}
......@@ -78,5 +99,8 @@ func NewConfig(ctx *cli.Context) *CLIConfig {
LogConfig: oplog.ReadCLIConfig(ctx),
MetricsConfig: opmetrics.ReadCLIConfig(ctx),
PprofConfig: oppprof.ReadCLIConfig(ctx),
DGFAddress: ctx.String(flags.DisputeGameFactoryAddressFlag.Name),
ProposalInterval: ctx.Duration(flags.ProposalIntervalFlag.Name),
DisputeGameType: uint8(ctx.Uint(flags.DisputeGameTypeFlag.Name)),
}
}
......@@ -13,6 +13,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"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/log"
......@@ -23,8 +24,10 @@ import (
"github.com/ethereum-optimism/optimism/op-service/txmgr"
)
var supportedL2OutputVersion = eth.Bytes32{}
var ErrProposerNotRunning = errors.New("proposer is not running")
var (
supportedL2OutputVersion = eth.Bytes32{}
ErrProposerNotRunning = errors.New("proposer is not running")
)
type L1Client interface {
HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
......@@ -68,13 +71,27 @@ type L2OutputSubmitter struct {
l2ooContract *bindings.L2OutputOracleCaller
l2ooABI *abi.ABI
dgfContract *bindings.DisputeGameFactoryCaller
dgfABI *abi.ABI
}
// NewL2OutputSubmitter creates a new L2 Output Submitter
func NewL2OutputSubmitter(setup DriverSetup) (*L2OutputSubmitter, error) {
ctx, cancel := context.WithCancel(context.Background())
l2ooContract, err := bindings.NewL2OutputOracleCaller(setup.Cfg.L2OutputOracleAddr, setup.L1Client)
if setup.Cfg.L2OutputOracleAddr != nil {
return newL2OOSubmitter(ctx, cancel, setup)
} else if setup.Cfg.DisputeGameFactoryAddr != nil {
return newDGFSubmitter(ctx, cancel, setup)
} else {
cancel()
return nil, errors.New("neither the `L2OutputOracle` nor `DisputeGameFactory` addresses were provided")
}
}
func newL2OOSubmitter(ctx context.Context, cancel context.CancelFunc, setup DriverSetup) (*L2OutputSubmitter, error) {
l2ooContract, err := bindings.NewL2OutputOracleCaller(*setup.Cfg.L2OutputOracleAddr, setup.L1Client)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create L2OO at address %s: %w", setup.Cfg.L2OutputOracleAddr, err)
......@@ -106,6 +123,39 @@ func NewL2OutputSubmitter(setup DriverSetup) (*L2OutputSubmitter, error) {
}, nil
}
func newDGFSubmitter(ctx context.Context, cancel context.CancelFunc, setup DriverSetup) (*L2OutputSubmitter, error) {
dgfCaller, err := bindings.NewDisputeGameFactoryCaller(*setup.Cfg.DisputeGameFactoryAddr, setup.L1Client)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to create DGF at address %s: %w", setup.Cfg.DisputeGameFactoryAddr, err)
}
cCtx, cCancel := context.WithTimeout(ctx, setup.Cfg.NetworkTimeout)
defer cCancel()
version, err := dgfCaller.Version(&bind.CallOpts{Context: cCtx})
if err != nil {
cancel()
return nil, err
}
log.Info("Connected to DisputeGameFactory", "address", setup.Cfg.DisputeGameFactoryAddr, "version", version)
parsed, err := bindings.DisputeGameFactoryMetaData.GetAbi()
if err != nil {
cancel()
return nil, err
}
return &L2OutputSubmitter{
DriverSetup: setup,
done: make(chan struct{}),
ctx: ctx,
cancel: cancel,
dgfContract: dgfCaller,
dgfABI: parsed,
}, nil
}
func (l *L2OutputSubmitter) StartL2OutputSubmitting() error {
l.Log.Info("Starting Proposer")
......@@ -113,7 +163,7 @@ func (l *L2OutputSubmitter) StartL2OutputSubmitting() error {
defer l.mutex.Unlock()
if l.running {
return errors.New("Proposer is already running")
return errors.New("proposer is already running")
}
l.running = true
......@@ -154,6 +204,10 @@ func (l *L2OutputSubmitter) StopL2OutputSubmitting() error {
// FetchNextOutputInfo gets the block number of the next proposal.
// It returns: the next block number, if the proposal should be made, error
func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.OutputResponse, bool, error) {
if l.l2ooContract == nil {
return nil, false, fmt.Errorf("L2OutputOracle contract not set, cannot fetch next output info")
}
cCtx, cancel := context.WithTimeout(ctx, l.Cfg.NetworkTimeout)
defer cancel()
callOpts := &bind.CallOpts{
......@@ -166,17 +220,34 @@ func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.Outpu
return nil, false, err
}
// Fetch the current L2 heads
cCtx, cancel = context.WithTimeout(ctx, l.Cfg.NetworkTimeout)
currentBlockNumber, err := l.FetchCurrentBlockNumber(ctx)
if err != nil {
return nil, false, err
}
// Ensure that we do not submit a block in the future
if currentBlockNumber.Cmp(nextCheckpointBlock) < 0 {
l.Log.Debug("proposer submission interval has not elapsed", "currentBlockNumber", currentBlockNumber, "nextBlockNumber", nextCheckpointBlock)
return nil, false, nil
}
return l.fetchOutput(ctx, nextCheckpointBlock)
}
// FetchCurrentBlockNumber gets the current block number from the [L2OutputSubmitter]'s [RollupClient]. If the `AllowNonFinalized` configuration
// option is set, it will return the safe head block number, and if not, it will return the finalized head block number.
func (l *L2OutputSubmitter) FetchCurrentBlockNumber(ctx context.Context) (*big.Int, error) {
cCtx, cancel := context.WithTimeout(ctx, l.Cfg.NetworkTimeout)
defer cancel()
rollupClient, err := l.RollupProvider.RollupClient(cCtx)
if err != nil {
l.Log.Error("proposer unable to get rollup client", "err", err)
return nil, false, err
return nil, err
}
status, err := rollupClient.SyncStatus(cCtx)
if err != nil {
l.Log.Error("proposer unable to get sync status", "err", err)
return nil, false, err
return nil, err
}
// Use either the finalized or safe head depending on the config. Finalized head is default & safer.
......@@ -186,13 +257,7 @@ func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.Outpu
} else {
currentBlockNumber = new(big.Int).SetUint64(status.FinalizedL2.Number)
}
// Ensure that we do not submit a block in the future
if currentBlockNumber.Cmp(nextCheckpointBlock) < 0 {
l.Log.Debug("proposer submission interval has not elapsed", "currentBlockNumber", currentBlockNumber, "nextBlockNumber", nextCheckpointBlock)
return nil, false, nil
}
return l.fetchOutput(ctx, nextCheckpointBlock)
return currentBlockNumber, nil
}
func (l *L2OutputSubmitter) fetchOutput(ctx context.Context, block *big.Int) (*eth.OutputResponse, bool, error) {
......@@ -219,7 +284,7 @@ func (l *L2OutputSubmitter) fetchOutput(ctx context.Context, block *big.Int) (*e
}
// Always propose if it's part of the Finalized L2 chain. Or if allowed, if it's part of the safe L2 chain.
if !(output.BlockRef.Number <= output.Status.FinalizedL2.Number || (l.Cfg.AllowNonFinalized && output.BlockRef.Number <= output.Status.SafeL2.Number)) {
if output.BlockRef.Number > output.Status.FinalizedL2.Number && (!l.Cfg.AllowNonFinalized || output.BlockRef.Number > output.Status.SafeL2.Number) {
l.Log.Debug("not proposing yet, L2 block is not ready for proposal",
"l2_proposal", output.BlockRef,
"l2_safe", output.Status.SafeL2,
......@@ -245,6 +310,15 @@ func proposeL2OutputTxData(abi *abi.ABI, output *eth.OutputResponse) ([]byte, er
new(big.Int).SetUint64(output.Status.CurrentL1.Number))
}
func (l *L2OutputSubmitter) ProposeL2OutputDGFTxData(output *eth.OutputResponse) ([]byte, error) {
return proposeL2OutputDGFTxData(l.dgfABI, l.Cfg.DisputeGameType, output)
}
// proposeL2OutputDGFTxData creates the transaction data for the DisputeGameFactory's `create` function
func proposeL2OutputDGFTxData(abi *abi.ABI, gameType uint8, output *eth.OutputResponse) ([]byte, error) {
return abi.Pack("create", gameType, output.OutputRoot, math.U256Bytes(new(big.Int).SetUint64(output.BlockRef.Number)))
}
// We wait until l1head advances beyond blocknum. This is used to make sure proposal tx won't
// immediately fail when checking the l1 blockhash. Note that EstimateGas uses "latest" state to
// execute the transaction by default, meaning inside the call, the head block is considered
......@@ -266,7 +340,6 @@ func (l *L2OutputSubmitter) waitForL1Head(ctx context.Context, blockNum uint64)
if err != nil {
return err
}
break
case <-l.done:
return fmt.Errorf("L2OutputSubmitter is done()")
}
......@@ -280,18 +353,36 @@ func (l *L2OutputSubmitter) sendTransaction(ctx context.Context, output *eth.Out
if err != nil {
return err
}
data, err := l.ProposeL2OutputTxData(output)
if err != nil {
return err
}
receipt, err := l.Txmgr.Send(ctx, txmgr.TxCandidate{
TxData: data,
To: &l.Cfg.L2OutputOracleAddr,
GasLimit: 0,
})
if err != nil {
return err
var receipt *types.Receipt
if l.Cfg.DisputeGameFactoryAddr != nil {
data, err := l.ProposeL2OutputDGFTxData(output)
if err != nil {
return err
}
receipt, err = l.Txmgr.Send(ctx, txmgr.TxCandidate{
TxData: data,
To: l.Cfg.DisputeGameFactoryAddr,
GasLimit: 0,
})
if err != nil {
return err
}
} else {
data, err := l.ProposeL2OutputTxData(output)
if err != nil {
return err
}
receipt, err = l.Txmgr.Send(ctx, txmgr.TxCandidate{
TxData: data,
To: l.Cfg.L2OutputOracleAddr,
GasLimit: 0,
})
if err != nil {
return err
}
}
if receipt.Status == types.ReceiptStatusFailed {
l.Log.Error("proposer tx successfully published but reverted", "tx_hash", receipt.TxHash)
} else {
......@@ -306,36 +397,67 @@ func (l *L2OutputSubmitter) sendTransaction(ctx context.Context, output *eth.Out
// loop is responsible for creating & submitting the next outputs
func (l *L2OutputSubmitter) loop() {
defer l.wg.Done()
ctx := l.ctx
if l.dgfContract == nil {
l.loopL2OO(ctx)
} else {
l.loopDGF(ctx)
}
}
func (l *L2OutputSubmitter) loopL2OO(ctx context.Context) {
ticker := time.NewTicker(l.Cfg.PollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
output, shouldPropose, err := l.FetchNextOutputInfo(ctx)
if err != nil {
if err != nil || !shouldPropose {
break
}
if !shouldPropose {
l.proposeOutput(ctx, output)
case <-l.done:
return
}
}
}
func (l *L2OutputSubmitter) loopDGF(ctx context.Context) {
ticker := time.NewTicker(l.Cfg.ProposalInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
blockNumber, err := l.FetchCurrentBlockNumber(ctx)
if err != nil {
break
}
cCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
if err := l.sendTransaction(cCtx, output); err != nil {
l.Log.Error("Failed to send proposal transaction",
"err", err,
"l1blocknum", output.Status.CurrentL1.Number,
"l1blockhash", output.Status.CurrentL1.Hash,
"l1head", output.Status.HeadL1.Number)
cancel()
output, shouldPropose, err := l.fetchOutput(ctx, blockNumber)
if err != nil || !shouldPropose {
break
}
l.Metr.RecordL2BlocksProposed(output.BlockRef)
cancel()
l.proposeOutput(ctx, output)
case <-l.done:
return
}
}
}
func (l *L2OutputSubmitter) proposeOutput(ctx context.Context, output *eth.OutputResponse) {
cCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
if err := l.sendTransaction(cCtx, output); err != nil {
l.Log.Error("Failed to send proposal transaction",
"err", err,
"l1blocknum", output.Status.CurrentL1.Number,
"l1blockhash", output.Status.CurrentL1.Hash,
"l1head", output.Status.HeadL1.Number)
return
}
l.Metr.RecordL2BlocksProposed(output.BlockRef)
}
......@@ -15,7 +15,7 @@ import (
// Main is the entrypoint into the L2OutputSubmitter.
// This method returns a cliapp.LifecycleAction, to create an op-service CLI-lifecycle-managed L2Output-submitter
func Main(version string) cliapp.LifecycleAction {
return func(cliCtx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) {
return func(cliCtx *cli.Context, _ context.CancelCauseFunc) (cliapp.Lifecycle, error) {
if err := flags.CheckRequired(cliCtx); err != nil {
return nil, err
}
......
......@@ -26,16 +26,20 @@ import (
"github.com/ethereum/go-ethereum/log"
)
var (
ErrAlreadyStopped = errors.New("already stopped")
)
var ErrAlreadyStopped = errors.New("already stopped")
type ProposerConfig struct {
// How frequently to poll L2 for new finalized outputs
PollInterval time.Duration
NetworkTimeout time.Duration
L2OutputOracleAddr common.Address
// How frequently to post L2 outputs when the DisputeGameFactory is configured
ProposalInterval time.Duration
L2OutputOracleAddr *common.Address
DisputeGameFactoryAddr *common.Address
DisputeGameType uint8
// AllowNonFinalized enables the proposal of safe, but non-finalized L2 blocks.
// The L1 block-hash embedded in the proposal TX is checked and should ensure the proposal
// is never valid on an alternative L1 chain that would produce different L2 data.
......@@ -87,6 +91,9 @@ func (ps *ProposerService) initFromCLIConfig(ctx context.Context, version string
ps.NetworkTimeout = cfg.TxMgrConfig.NetworkTimeout
ps.AllowNonFinalized = cfg.AllowNonFinalized
ps.initL2ooAddress(cfg)
ps.initDGF(cfg)
if err := ps.initRPCClients(ctx, cfg); err != nil {
return err
}
......@@ -100,9 +107,6 @@ func (ps *ProposerService) initFromCLIConfig(ctx context.Context, version string
if err := ps.initPProf(cfg); err != nil {
return fmt.Errorf("failed to start pprof server: %w", err)
}
if err := ps.initL2ooAddress(cfg); err != nil {
return fmt.Errorf("failed to init L2ooAddress: %w", err)
}
if err := ps.initDriver(); err != nil {
return fmt.Errorf("failed to init Driver: %w", err)
}
......@@ -194,13 +198,24 @@ func (ps *ProposerService) initMetricsServer(cfg *CLIConfig) error {
return nil
}
func (ps *ProposerService) initL2ooAddress(cfg *CLIConfig) error {
func (ps *ProposerService) initL2ooAddress(cfg *CLIConfig) {
l2ooAddress, err := opservice.ParseAddress(cfg.L2OOAddress)
if err != nil {
return nil
// Return no error & set no L2OO related configuration fields.
return
}
ps.L2OutputOracleAddr = l2ooAddress
return nil
ps.L2OutputOracleAddr = &l2ooAddress
}
func (ps *ProposerService) initDGF(cfg *CLIConfig) {
dgfAddress, err := opservice.ParseAddress(cfg.DGFAddress)
if err != nil {
// Return no error & set no DGF related configuration fields.
return
}
ps.DisputeGameFactoryAddr = &dgfAddress
ps.ProposalInterval = cfg.ProposalInterval
ps.DisputeGameType = cfg.DisputeGameType
}
func (ps *ProposerService) initDriver() error {
......
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