Commit 705c7ef8 authored by Joshua Gutow's avatar Joshua Gutow

op-proposer: Cleanup

This removes the driver / service design from the proposer & puts it all
in the L2 Output Submitter. This significantly reduces some of the
boilerplate. This also removes some extra config options.
parent 98b6a400
......@@ -4,6 +4,7 @@ import (
"context"
"crypto/ecdsa"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
......@@ -14,6 +15,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-proposer/proposer"
"github.com/ethereum-optimism/optimism/op-proposer/txmgr"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
)
......@@ -26,28 +28,41 @@ type ProposerCfg struct {
type L2Proposer struct {
log log.Logger
l1 *ethclient.Client
driver *proposer.Driver
driver *proposer.L2OutputSubmitter
address common.Address
lastTx common.Hash
}
func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Client, rollupCl *sources.RollupClient) *L2Proposer {
chainID, err := l1.ChainID(t.Ctx())
require.NoError(t, err)
signer := opcrypto.PrivateKeySignerFn(cfg.ProposerKey, chainID)
dr, err := proposer.NewDriver(proposer.DriverConfig{
Log: log,
Name: "proposer",
signer := func(chainID *big.Int) proposer.SignerFn {
s := opcrypto.PrivateKeySignerFn(cfg.ProposerKey, chainID)
return func(_ context.Context, addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
return s(addr, tx)
}
}
from := crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey)
proposerCfg := proposer.Config{
L2OutputOracleAddr: cfg.OutputOracleAddr,
PollInterval: time.Second,
TxManagerConfig: txmgr.Config{
Log: log,
Name: "action-proposer",
ResubmissionTimeout: 5 * time.Second,
ReceiptQueryInterval: time.Second,
NumConfirmations: 1,
SafeAbortNonceTooLowCount: 4,
},
L1Client: l1,
RollupClient: rollupCl,
AllowNonFinalized: cfg.AllowNonFinalized,
L2OOAddr: cfg.OutputOracleAddr,
From: crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey),
SignerFn: func(_ context.Context, addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
return signer(addr, tx)
},
})
From: from,
SignerFnFactory: signer,
}
dr, err := proposer.NewL2OutputSubmitterWithSigner(proposerCfg, log)
require.NoError(t, err)
return &L2Proposer{
log: log,
l1: l1,
......@@ -57,25 +72,24 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
}
func (p *L2Proposer) CanPropose(t Testing) bool {
start, end, err := p.driver.GetBlockRange(t.Ctx())
_, shouldPropose, err := p.driver.FetchNextOutputInfo(t.Ctx())
require.NoError(t, err)
return start.Cmp(end) < 0
return shouldPropose
}
func (p *L2Proposer) ActMakeProposalTx(t Testing) {
start, end, err := p.driver.GetBlockRange(t.Ctx())
require.NoError(t, err)
if start.Cmp(end) == 0 {
t.InvalidAction("nothing to propose, block range starts and ends at %s", start.String())
output, shouldPropose, err := p.driver.FetchNextOutputInfo(t.Ctx())
if !shouldPropose {
return
}
nonce, err := p.l1.PendingNonceAt(t.Ctx(), p.address)
require.NoError(t, err)
tx, err := p.driver.CraftTx(t.Ctx(), start, end, new(big.Int).SetUint64(nonce))
tx, err := p.driver.CreateProposalTx(t.Ctx(), output)
require.NoError(t, err)
err = p.driver.SendTransaction(t.Ctx(), tx)
require.NoError(t, err)
p.lastTx = tx.Hash()
}
......
......@@ -342,7 +342,7 @@ func TestMigration(t *testing.T) {
batcher.Stop()
})
proposer, err := l2os.NewL2OutputSubmitter(l2os.Config{
proposer, err := l2os.NewL2OutputSubmitter(l2os.CLIConfig{
L1EthRpc: forkedL1URL,
RollupRpc: rollupNode.HTTPEndpoint(),
L2OOAddress: l2OS.Address.String(),
......@@ -356,7 +356,7 @@ func TestMigration(t *testing.T) {
Format: "text",
},
PrivateKey: hexPriv(secrets.Proposer),
}, "", lgr.New("module", "proposer"))
}, lgr.New("module", "proposer"))
require.NoError(t, err)
t.Cleanup(func() {
proposer.Stop()
......
......@@ -498,7 +498,7 @@ func (cfg SystemConfig) Start() (*System, error) {
}
// L2Output Submitter
sys.L2OutputSubmitter, err = l2os.NewL2OutputSubmitter(l2os.Config{
sys.L2OutputSubmitter, err = l2os.NewL2OutputSubmitter(l2os.CLIConfig{
L1EthRpc: sys.Nodes["l1"].WSEndpoint(),
RollupRpc: sys.RollupNodes["sequencer"].HTTPEndpoint(),
L2OOAddress: predeploys.DevL2OutputOracleAddr.String(),
......@@ -512,7 +512,7 @@ func (cfg SystemConfig) Start() (*System, error) {
Format: "text",
},
PrivateKey: hexPriv(cfg.Secrets.Proposer),
}, "", sys.cfg.Loggers["proposer"])
}, sys.cfg.Loggers["proposer"])
if err != nil {
return nil, fmt.Errorf("unable to setup l2 output submitter: %w", err)
}
......
package mock
import (
"context"
"math/big"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
// L1ClientConfig houses the internal methods that are executed by the mock
// L1Client. Any members left as nil will panic on execution.
type L1ClientConfig struct {
// BlockNumber returns the most recent block number.
BlockNumber func(context.Context) (uint64, error)
// HeaderByNumber returns a block header from the current canonical chain.
// If number is nil, the latest known header is returned.
HeaderByNumber func(context.Context, *big.Int) (*types.Header, error)
// NonceAt returns the account nonce of the given account. The block number
// can be nil, in which case the nonce is taken from the latest known block.
NonceAt func(context.Context, common.Address, *big.Int) (uint64, error)
// SendTransaction injects a signed transaction into the pending pool for
// execution.
//
// If the transaction was a contract creation use the TransactionReceipt
// method to get the contract address after the transaction has been mined.
SendTransaction func(context.Context, *types.Transaction) error
// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559
// to allow a timely execution of a transaction.
SuggestGasTipCap func(context.Context) (*big.Int, error)
// TransactionReceipt returns the receipt of a transaction by transaction
// hash. Note that the receipt is not available for pending transactions.
TransactionReceipt func(context.Context, common.Hash) (*types.Receipt, error)
}
// L1Client represents a mock L1Client.
type L1Client struct {
cfg L1ClientConfig
mu sync.RWMutex
}
// NewL1Client returns a new L1Client using the mocked methods in the
// L1ClientConfig.
func NewL1Client(cfg L1ClientConfig) *L1Client {
return &L1Client{
cfg: cfg,
}
}
// BlockNumber returns the most recent block number.
func (c *L1Client) BlockNumber(ctx context.Context) (uint64, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.BlockNumber(ctx)
}
// HeaderByNumber returns a block header from the current canonical chain. If
// number is nil, the latest known header is returned.
func (c *L1Client) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (*types.Header, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.HeaderByNumber(ctx, blockNumber)
}
// NonceAt executes the mock NonceAt method.
func (c *L1Client) NonceAt(ctx context.Context, addr common.Address, blockNumber *big.Int) (uint64, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.NonceAt(ctx, addr, blockNumber)
}
// SendTransaction executes the mock SendTransaction method.
func (c *L1Client) SendTransaction(ctx context.Context, tx *types.Transaction) error {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.SendTransaction(ctx, tx)
}
// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559 to
// allow a timely execution of a transaction.
func (c *L1Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.SuggestGasTipCap(ctx)
}
// TransactionReceipt executes the mock TransactionReceipt method.
func (c *L1Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.cfg.TransactionReceipt(ctx, txHash)
}
// SetBlockNumberFunc overwrites the mock BlockNumber method.
func (c *L1Client) SetBlockNumberFunc(
f func(context.Context) (uint64, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.BlockNumber = f
}
// SetHeaderByNumberFunc overwrites the mock HeaderByNumber method.
func (c *L1Client) SetHeaderByNumberFunc(
f func(ctx context.Context, blockNumber *big.Int) (*types.Header, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.HeaderByNumber = f
}
// SetNonceAtFunc overwrites the mock NonceAt method.
func (c *L1Client) SetNonceAtFunc(
f func(context.Context, common.Address, *big.Int) (uint64, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.NonceAt = f
}
// SetSendTransactionFunc overwrites the mock SendTransaction method.
func (c *L1Client) SetSendTransactionFunc(
f func(context.Context, *types.Transaction) error) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.SendTransaction = f
}
// SetSuggestGasTipCapFunc overwrites themock SuggestGasTipCap method.
func (c *L1Client) SetSuggestGasTipCapFunc(
f func(context.Context) (*big.Int, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.SuggestGasTipCap = f
}
// SetTransactionReceiptFunc overwrites the mock TransactionReceipt method.
func (c *L1Client) SetTransactionReceiptFunc(
f func(context.Context, common.Hash) (*types.Receipt, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.cfg.TransactionReceipt = f
}
......@@ -3,16 +3,36 @@ package proposer
import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-proposer/flags"
"github.com/ethereum-optimism/optimism/op-proposer/txmgr"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
)
// Config contains the well typed fields that are used to initialize the output submitter.
// It is intended for programmatic use.
type Config struct {
L2OutputOracleAddr common.Address
PollInterval time.Duration
TxManagerConfig txmgr.Config
L1Client *ethclient.Client
RollupClient *sources.RollupClient
AllowNonFinalized bool
From common.Address
SignerFnFactory SignerFactory
}
// CLIConfig is a well typed config that is parsed from the CLI params.
// This also contains config options for auxiliary services.
// It is transformed into a `Config` before the L2 output submitter is started.
type CLIConfig struct {
/* Required Params */
// L1EthRpc is the HTTP provider URL for L1.
......@@ -68,7 +88,7 @@ type Config struct {
PprofConfig oppprof.CLIConfig
}
func (c Config) Check() error {
func (c CLIConfig) Check() error {
if err := c.RPCConfig.Check(); err != nil {
return err
}
......@@ -85,9 +105,9 @@ func (c Config) Check() error {
}
// NewConfig parses the Config from the provided flags or environment variables.
func NewConfig(ctx *cli.Context) Config {
return Config{
/* Required Flags */
func NewConfig(ctx *cli.Context) CLIConfig {
return CLIConfig{
// Required Flags
L1EthRpc: ctx.GlobalString(flags.L1EthRpcFlag.Name),
RollupRpc: ctx.GlobalString(flags.RollupRpcFlag.Name),
L2OOAddress: ctx.GlobalString(flags.L2OOAddressFlag.Name),
......@@ -98,10 +118,11 @@ func NewConfig(ctx *cli.Context) Config {
Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name),
L2OutputHDPath: ctx.GlobalString(flags.L2OutputHDPathFlag.Name),
PrivateKey: ctx.GlobalString(flags.PrivateKeyFlag.Name),
AllowNonFinalized: ctx.GlobalBool(flags.AllowNonFinalizedFlag.Name),
RPCConfig: oprpc.ReadCLIConfig(ctx),
LogConfig: oplog.ReadCLIConfig(ctx),
MetricsConfig: opmetrics.ReadCLIConfig(ctx),
PprofConfig: oppprof.ReadCLIConfig(ctx),
// Optional Flags
AllowNonFinalized: ctx.GlobalBool(flags.AllowNonFinalizedFlag.Name),
RPCConfig: oprpc.ReadCLIConfig(ctx),
LogConfig: oplog.ReadCLIConfig(ctx),
MetricsConfig: opmetrics.ReadCLIConfig(ctx),
PprofConfig: oppprof.ReadCLIConfig(ctx),
}
}
package proposer
import (
"context"
"fmt"
"math/big"
"strings"
"github.com/ethereum-optimism/optimism/op-node/sources"
"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/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/eth"
)
var bigOne = big.NewInt(1)
var supportedL2OutputVersion = eth.Bytes32{}
type SignerFn func(context.Context, common.Address, *types.Transaction) (*types.Transaction, error)
type DriverConfig struct {
Log log.Logger
Name string
// L1Client is used to submit transactions to
L1Client *ethclient.Client
// RollupClient is used to retrieve output roots from
RollupClient *sources.RollupClient
// 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.
// This option is not necessary when higher proposal latency is acceptable and L1 is healthy.
AllowNonFinalized bool
// L2OOAddr is the L1 contract address of the L2 Output Oracle.
L2OOAddr common.Address
// From is the address to send transactions from
From common.Address
// SignerFn is the function used to sign transactions
SignerFn SignerFn
}
type Driver struct {
cfg DriverConfig
l2ooContract *bindings.L2OutputOracle
rawL2ooContract *bind.BoundContract
walletAddr common.Address
l log.Logger
}
func NewDriver(cfg DriverConfig) (*Driver, error) {
l2ooContract, err := bindings.NewL2OutputOracle(cfg.L2OOAddr, cfg.L1Client)
if err != nil {
return nil, err
}
parsed, err := abi.JSON(strings.NewReader(
bindings.L2OutputOracleMetaData.ABI,
))
if err != nil {
return nil, err
}
rawL2ooContract := bind.NewBoundContract(
cfg.L2OOAddr, parsed, cfg.L1Client, cfg.L1Client, cfg.L1Client,
)
cfg.Log.Info("Configured driver", "wallet", cfg.From, "l2-output-contract", cfg.L2OOAddr)
return &Driver{
cfg: cfg,
l2ooContract: l2ooContract,
rawL2ooContract: rawL2ooContract,
walletAddr: cfg.From,
l: cfg.Log,
}, nil
}
// Name is an identifier used to prefix logs for a particular service.
func (d *Driver) Name() string {
return d.cfg.Name
}
// WalletAddr is the wallet address used to pay for transaction fees.
func (d *Driver) WalletAddr() common.Address {
return d.walletAddr
}
// GetBlockRange returns the start and end L2 block heights that need to be
// processed. Note that the end value is *exclusive*, therefore if the returned
// values are identical nothing needs to be processed.
func (d *Driver) GetBlockRange(ctx context.Context) (*big.Int, *big.Int, error) {
name := d.cfg.Name
callOpts := &bind.CallOpts{
Pending: false,
Context: ctx,
}
// Determine the last committed L2 Block Number
start, err := d.l2ooContract.LatestBlockNumber(callOpts)
if err != nil {
d.l.Error(name+" unable to get latest block number", "err", err)
return nil, nil, err
}
start.Add(start, bigOne)
// Next determine the L2 block that we need to commit
nextBlockNumber, err := d.l2ooContract.NextBlockNumber(callOpts)
if err != nil {
d.l.Error(name+" unable to get next block number", "err", err)
return nil, nil, err
}
status, err := d.cfg.RollupClient.SyncStatus(ctx)
if err != nil {
d.l.Error(name+" unable to get sync status", "err", err)
return nil, nil, err
}
var currentBlockNumber *big.Int
if d.cfg.AllowNonFinalized {
currentBlockNumber = new(big.Int).SetUint64(status.SafeL2.Number)
} else {
currentBlockNumber = new(big.Int).SetUint64(status.FinalizedL2.Number)
}
// If we do not have the new L2 Block number
if currentBlockNumber.Cmp(nextBlockNumber) < 0 {
d.l.Info(name+" submission interval has not elapsed",
"currentBlockNumber", currentBlockNumber, "nextBlockNumber", nextBlockNumber)
return start, start, nil
}
d.l.Info(name+" submission interval has elapsed",
"currentBlockNumber", currentBlockNumber, "nextBlockNumber", nextBlockNumber)
// Otherwise the submission interval has elapsed. Transform the next
// expected timestamp into its L2 block number, and add one since end is
// exclusive.
end := new(big.Int).Add(nextBlockNumber, bigOne)
return start, end, nil
}
// CraftTx transforms the L2 blocks between start and end into a transaction
// using the given nonce.
//
// NOTE: This method SHOULD NOT publish the resulting transaction.
func (d *Driver) CraftTx(ctx context.Context, start, end, nonce *big.Int) (*types.Transaction, error) {
name := d.cfg.Name
d.l.Info(name+" crafting checkpoint tx", "start", start, "end", end, "nonce", nonce)
// Fetch the final block in the range, as this is the only L2 output we need to submit.
nextCheckpointBlock := new(big.Int).Sub(end, bigOne).Uint64()
output, err := d.cfg.RollupClient.OutputAtBlock(ctx, nextCheckpointBlock)
if err != nil {
return nil, fmt.Errorf("failed to fetch output at block %d: %w", nextCheckpointBlock, err)
}
if output.Version != supportedL2OutputVersion {
return nil, fmt.Errorf("unsupported l2 output version: %s", output.Version)
}
if output.BlockRef.Number != nextCheckpointBlock { // sanity check, e.g. in case of bad RPC caching
return nil, fmt.Errorf("invalid blockNumber: next blockNumber is %v, blockNumber of block is %v", nextCheckpointBlock, output.BlockRef.Number)
}
// 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 || (d.cfg.AllowNonFinalized && output.BlockRef.Number <= output.Status.SafeL2.Number)) {
d.l.Debug("not proposing yet, L2 block is not ready for proposal",
"l2_proposal", output.BlockRef,
"l2_safe", output.Status.SafeL2,
"l2_finalized", output.Status.FinalizedL2,
"allow_non_finalized", d.cfg.AllowNonFinalized)
return nil, fmt.Errorf("output for L2 block %s is still unsafe", output.BlockRef)
}
opts := &bind.TransactOpts{
From: d.cfg.From,
Signer: func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
return d.cfg.SignerFn(ctx, addr, tx)
},
Context: ctx,
Nonce: nonce,
NoSend: true,
}
// Note: the CurrentL1 is up to (and incl.) what the safe chain and finalized chain have been derived from,
// and should be a quite recent L1 block (depends on L1 conf distance applied to rollup node).
tx, err := d.l2ooContract.ProposeL2Output(
opts,
output.OutputRoot,
new(big.Int).SetUint64(output.BlockRef.Number),
output.Status.CurrentL1.Hash,
new(big.Int).SetUint64(output.Status.CurrentL1.Number))
if err != nil {
return nil, err
}
numElements := new(big.Int).Sub(start, end).Uint64()
d.l.Info(name+" proposal constructed",
"start", start, "end", end,
"nonce", nonce, "blocks_committed", numElements,
"tx_hash", tx.Hash(),
"output_version", output.Version,
"output_root", output.OutputRoot,
"output_block", output.BlockRef,
"output_withdrawals_root", output.WithdrawalStorageRoot,
"output_state_root", output.StateRoot,
"current_l1", output.Status.CurrentL1,
"safe_l2", output.Status.SafeL2,
"finalized_l2", output.Status.FinalizedL2,
)
return tx, nil
}
// UpdateGasPrice signs an otherwise identical txn to the one provided but with
// updated gas prices sampled from the existing network conditions.
//
// NOTE: This method SHOULD NOT publish the resulting transaction.
func (d *Driver) UpdateGasPrice(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
opts := &bind.TransactOpts{
From: d.cfg.From,
Signer: func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
return d.cfg.SignerFn(ctx, addr, tx)
},
Context: ctx,
Nonce: new(big.Int).SetUint64(tx.Nonce()),
NoSend: true,
}
return d.rawL2ooContract.RawTransact(opts, tx.Data())
}
// SendTransaction injects a signed transaction into the pending pool for execution.
func (d *Driver) SendTransaction(ctx context.Context, tx *types.Transaction) error {
d.l.Info(d.cfg.Name+" sending transaction", "tx", tx.Hash())
return d.cfg.L1Client.SendTransaction(ctx, tx)
}
package proposer
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
// L1Client is an abstraction over an L1 Ethereum client functionality required
// by the batch submitter.
type L1Client interface {
// HeaderByNumber returns a block header from the current canonical chain.
// If number is nil, the latest known header is returned.
HeaderByNumber(context.Context, *big.Int) (*types.Header, error)
// NonceAt returns the account nonce of the given account. The block number
// can be nil, in which case the nonce is taken from the latest known block.
NonceAt(context.Context, common.Address, *big.Int) (uint64, error)
// SendTransaction injects a signed transaction into the pending pool for
// execution.
//
// If the transaction was a contract creation use the TransactionReceipt
// method to get the contract address after the transaction has been mined.
SendTransaction(context.Context, *types.Transaction) error
// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559
// to allow a timely execution of a transaction.
SuggestGasTipCap(context.Context) (*big.Int, error)
// TransactionReceipt returns the receipt of a transaction by transaction
// hash. Note that the receipt is not available for pending transactions.
TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
}
This diff is collapsed.
package proposer
import (
"context"
"math/big"
"sync"
"time"
"github.com/ethereum-optimism/optimism/op-proposer/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
// DriverInterface is an interface for creating and submitting transactions for a
// specific contract.
type DriverInterface interface {
// Name is an identifier used to prefix logs for a particular service.
Name() string
// WalletAddr is the wallet address used to pay for transaction fees.
WalletAddr() common.Address
// GetBlockRange returns the start and end L2 block heights that need to be
// processed. Note that the end value is *exclusive*, therefore if the
// returned values are identical nothing needs to be processed.
GetBlockRange(ctx context.Context) (*big.Int, *big.Int, error)
// CraftTx transforms the L2 blocks between start and end into a transaction
// using the given nonce.
//
// NOTE: This method SHOULD NOT publish the resulting transaction.
CraftTx(
ctx context.Context,
start, end, nonce *big.Int,
) (*types.Transaction, error)
// UpdateGasPrice signs an otherwise identical txn to the one provided but
// with updated gas prices sampled from the existing network conditions.
//
// NOTE: Thie method SHOULD NOT publish the resulting transaction.
UpdateGasPrice(
ctx context.Context,
tx *types.Transaction,
) (*types.Transaction, error)
// SendTransaction injects a signed transaction into the pending pool for
// execution.
SendTransaction(ctx context.Context, tx *types.Transaction) error
}
type ServiceConfig struct {
Log log.Logger
Context context.Context
Driver DriverInterface
PollInterval time.Duration
L1Client *ethclient.Client
TxManagerConfig txmgr.Config
}
type Service struct {
cfg ServiceConfig
txMgr txmgr.TxManager
l log.Logger
ctx context.Context
cancel func()
wg sync.WaitGroup
}
func NewService(cfg ServiceConfig) *Service {
txMgr := txmgr.NewSimpleTxManager(
cfg.Driver.Name(), cfg.TxManagerConfig, cfg.L1Client,
)
ctx, cancel := context.WithCancel(cfg.Context)
return &Service{
cfg: cfg,
txMgr: txMgr,
l: cfg.Log,
ctx: ctx,
cancel: cancel,
}
}
func (s *Service) Start() error {
s.wg.Add(1)
go s.eventLoop()
return nil
}
func (s *Service) Stop() error {
s.cancel()
s.wg.Wait()
return nil
}
func (s *Service) eventLoop() {
defer s.wg.Done()
name := s.cfg.Driver.Name()
ticker := time.NewTicker(s.cfg.PollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Determine the range of L2 blocks that the submitter has not
// processed, and needs to take action on.
s.l.Info(name + " fetching current block range")
start, end, err := s.cfg.Driver.GetBlockRange(s.ctx)
if err != nil {
s.l.Error(name+" unable to get block range", "err", err)
continue
}
// No new updates.
if start.Cmp(end) == 0 {
s.l.Info(name+" no updates", "start", start, "end", end)
continue
}
s.l.Info(name+" block range", "start", start, "end", end)
// Query for the submitter's current nonce.
nonce64, err := s.cfg.L1Client.NonceAt(
s.ctx, s.cfg.Driver.WalletAddr(), nil,
)
if err != nil {
s.l.Error(name+" unable to get current nonce",
"err", err)
continue
}
nonce := new(big.Int).SetUint64(nonce64)
tx, err := s.cfg.Driver.CraftTx(
s.ctx, start, end, nonce,
)
if err != nil {
s.l.Error(name+" unable to craft tx",
"err", err)
continue
}
// Construct the a closure that will update the txn with the current
// gas prices.
updateGasPrice := func(ctx context.Context) (*types.Transaction, error) {
s.l.Info(name+" updating batch tx gas price", "start", start,
"end", end, "nonce", nonce)
return s.cfg.Driver.UpdateGasPrice(ctx, tx)
}
// Wait until one of our submitted transactions confirms. If no
// receipt is received it's likely our gas price was too low.
receipt, err := s.txMgr.Send(
s.ctx, updateGasPrice, s.cfg.Driver.SendTransaction,
)
if err != nil {
s.l.Error(name+" unable to publish tx", "err", err)
continue
}
// The transaction was successfully submitted.
s.l.Info(name+" tx successfully published",
"tx_hash", receipt.TxHash)
case <-s.ctx.Done():
s.l.Info(name + " service shutting down")
return
}
}
}
package proposer
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
)
// dialEthClientWithTimeout attempts to dial the L1 provider using the provided
// URL. If the dial doesn't complete within defaultDialTimeout seconds, this
// method will return an error.
func dialEthClientWithTimeout(ctx context.Context, url string) (*ethclient.Client, error) {
ctxt, cancel := context.WithTimeout(ctx, defaultDialTimeout)
defer cancel()
return ethclient.DialContext(ctxt, url)
}
// dialRollupClientWithTimeout attempts to dial the RPC provider using the provided
// URL. If the dial doesn't complete within defaultDialTimeout seconds, this
// method will return an error.
func dialRollupClientWithTimeout(ctx context.Context, url string) (*sources.RollupClient, error) {
ctxt, cancel := context.WithTimeout(ctx, defaultDialTimeout)
defer cancel()
rpcCl, err := rpc.DialContext(ctxt, url)
if err != nil {
return nil, err
}
return sources.NewRollupClient(client.NewBaseRPCClient(rpcCl)), nil
}
// parseAddress parses an ETH address from a hex string. This method will fail if
// the address is not a valid hexadecimal address.
func parseAddress(address string) (common.Address, error) {
if common.IsHexAddress(address) {
return common.HexToAddress(address), nil
}
return common.Address{}, fmt.Errorf("invalid address: %v", address)
}
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