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)),
}
}
This diff is collapsed.
......@@ -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