Commit 8ade648e authored by Joshua Gutow's avatar Joshua Gutow

op-proposer: Use new API

This also adds a test for the proposer tx data creation.
parent 971387d1
...@@ -6,11 +6,13 @@ import ( ...@@ -6,11 +6,13 @@ import (
"math/big" "math/big"
"time" "time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/sources" "github.com/ethereum-optimism/optimism/op-node/sources"
...@@ -31,6 +33,9 @@ type L2Proposer struct { ...@@ -31,6 +33,9 @@ type L2Proposer struct {
l1 *ethclient.Client l1 *ethclient.Client
driver *proposer.L2OutputSubmitter driver *proposer.L2OutputSubmitter
address common.Address address common.Address
privKey *ecdsa.PrivateKey
signer opcrypto.SignerFn
contractAddr common.Address
lastTx common.Hash lastTx common.Hash
} }
...@@ -70,9 +75,52 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl ...@@ -70,9 +75,52 @@ func NewL2Proposer(t Testing, log log.Logger, cfg *ProposerCfg, l1 *ethclient.Cl
l1: l1, l1: l1,
driver: dr, driver: dr,
address: crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey), address: crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey),
privKey: cfg.ProposerKey,
signer: proposerCfg.TxManagerConfig.Signer,
contractAddr: cfg.OutputOracleAddr,
} }
} }
// sendTx reimplements creating & sending transactions because we need to do the final send as async in
// the action tests while we do it synchronously in the real system.
func (p *L2Proposer) sendTx(t Testing, data []byte) {
gasTipCap := big.NewInt(2 * params.GWei)
pendingHeader, err := p.l1.HeaderByNumber(t.Ctx(), big.NewInt(-1))
require.NoError(t, err, "need l1 pending header for gas price estimation")
gasFeeCap := new(big.Int).Add(gasTipCap, new(big.Int).Mul(pendingHeader.BaseFee, big.NewInt(2)))
chainID, err := p.l1.ChainID(t.Ctx())
require.NoError(t, err)
nonce, err := p.l1.NonceAt(t.Ctx(), p.address, nil)
require.NoError(t, err)
gasLimit, err := p.l1.EstimateGas(t.Ctx(), ethereum.CallMsg{
From: p.address,
To: &p.contractAddr,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Data: data,
})
require.NoError(t, err)
rawTx := &types.DynamicFeeTx{
Nonce: nonce,
To: &p.contractAddr,
Data: data,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Gas: gasLimit,
ChainID: chainID,
}
tx, err := types.SignNewTx(p.privKey, types.LatestSignerForChainID(chainID), rawTx)
require.NoError(t, err, "need to sign tx")
err = p.l1.SendTransaction(t.Ctx(), tx)
require.NoError(t, err, "need to send tx")
p.lastTx = tx.Hash()
}
func (p *L2Proposer) CanPropose(t Testing) bool { func (p *L2Proposer) CanPropose(t Testing) bool {
_, shouldPropose, err := p.driver.FetchNextOutputInfo(t.Ctx()) _, shouldPropose, err := p.driver.FetchNextOutputInfo(t.Ctx())
require.NoError(t, err) require.NoError(t, err)
...@@ -86,15 +134,12 @@ func (p *L2Proposer) ActMakeProposalTx(t Testing) { ...@@ -86,15 +134,12 @@ func (p *L2Proposer) ActMakeProposalTx(t Testing) {
} }
require.NoError(t, err) require.NoError(t, err)
tx, err := p.driver.CreateProposalTx(t.Ctx(), output) txData, err := p.driver.ProposeL2OutputTxData(output)
require.NoError(t, err) require.NoError(t, err)
// Note: Use L1 instead of the output submitter's transaction manager because // Note: Use L1 instead of the output submitter's transaction manager because
// this is non-blocking while the txmgr is blocking & deadlocks the tests // this is non-blocking while the txmgr is blocking & deadlocks the tests
err = p.l1.SendTransaction(t.Ctx(), tx) p.sendTx(t, txData)
require.NoError(t, err)
p.lastTx = tx.Hash()
} }
func (p *L2Proposer) LastProposalTx() common.Hash { func (p *L2Proposer) LastProposalTx() common.Hash {
......
...@@ -245,3 +245,23 @@ func RandomBlockPrependTxs(rng *rand.Rand, txCount int, ptxs ...*types.Transacti ...@@ -245,3 +245,23 @@ func RandomBlockPrependTxs(rng *rand.Rand, txCount int, ptxs ...*types.Transacti
} }
return block, receipts return block, receipts
} }
func RandomOutputResponse(rng *rand.Rand) *eth.OutputResponse {
return &eth.OutputResponse{
Version: eth.Bytes32(RandomHash(rng)),
OutputRoot: eth.Bytes32(RandomHash(rng)),
BlockRef: RandomL2BlockRef(rng),
WithdrawalStorageRoot: RandomHash(rng),
StateRoot: RandomHash(rng),
Status: &eth.SyncStatus{
CurrentL1: RandomBlockRef(rng),
CurrentL1Finalized: RandomBlockRef(rng),
HeadL1: RandomBlockRef(rng),
SafeL1: RandomBlockRef(rng),
FinalizedL1: RandomBlockRef(rng),
UnsafeL2: RandomL2BlockRef(rng),
SafeL2: RandomL2BlockRef(rng),
FinalizedL2: RandomL2BlockRef(rng),
},
}
}
package proposer
import (
"math/big"
"math/rand"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/testutils"
"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/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()
from := crypto.PubkeyToAddress(privateKey.PublicKey)
if err != nil {
return common.Address{}, nil, nil, nil, err
}
opts, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1337))
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,
big.NewInt(10),
big.NewInt(2),
big.NewInt(0),
big.NewInt(0),
from,
common.Address{0xdd},
big.NewInt(100))
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()
require.NoError(t, err)
rng := rand.New(rand.NewSource(1234))
abi, err := bindings.L2OutputOracleMetaData.GetAbi()
require.NoError(t, err)
output := testutils.RandomOutputResponse(rng)
txData, err := proposeL2OutputTxData(abi, 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(
opts,
output.OutputRoot,
new(big.Int).SetUint64(output.BlockRef.Number),
output.Status.CurrentL1.Hash,
new(big.Int).SetUint64(output.Status.CurrentL1.Number))
require.NoError(t, err)
require.Equal(t, txData, tx.Data())
}
...@@ -8,7 +8,6 @@ import ( ...@@ -8,7 +8,6 @@ import (
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"os/signal" "os/signal"
"strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
...@@ -16,7 +15,6 @@ import ( ...@@ -16,7 +15,6 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli" "github.com/urfave/cli"
...@@ -124,13 +122,14 @@ type L2OutputSubmitter struct { ...@@ -124,13 +122,14 @@ type L2OutputSubmitter struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
// L1Client is used to submit transactions to // l1Client is retained to make it easier to start the metrics balance check
l1Client *ethclient.Client l1Client *ethclient.Client
// RollupClient is used to retrieve output roots from // RollupClient is used to retrieve output roots from
rollupClient *sources.RollupClient rollupClient *sources.RollupClient
l2ooContract *bindings.L2OutputOracle l2ooContract *bindings.L2OutputOracleCaller
rawL2ooContract *bind.BoundContract l2ooContractAddr common.Address
l2ooABI *abi.ABI
// AllowNonFinalized enables the proposal of safe, but non-finalized L2 blocks. // 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 // The L1 block-hash embedded in the proposal TX is checked and should ensure the proposal
...@@ -139,8 +138,6 @@ type L2OutputSubmitter struct { ...@@ -139,8 +138,6 @@ type L2OutputSubmitter struct {
allowNonFinalized bool allowNonFinalized bool
// From is the address to send transactions from // From is the address to send transactions from
from common.Address from common.Address
// SignerFn is the function used to sign transactions
signerFn opcrypto.SignerFn
// How frequently to poll L2 for new finalized outputs // How frequently to poll L2 for new finalized outputs
pollInterval time.Duration pollInterval time.Duration
} }
...@@ -211,25 +208,26 @@ func NewL2OutputSubmitter(cfg Config, l log.Logger, m metrics.Metricer) (*L2Outp ...@@ -211,25 +208,26 @@ func NewL2OutputSubmitter(cfg Config, l log.Logger, m metrics.Metricer) (*L2Outp
signer := cfg.SignerFnFactory(chainID) signer := cfg.SignerFnFactory(chainID)
cfg.TxManagerConfig.Signer = signer cfg.TxManagerConfig.Signer = signer
l2ooContract, err := bindings.NewL2OutputOracle(cfg.L2OutputOracleAddr, cfg.L1Client) l2ooContract, err := bindings.NewL2OutputOracleCaller(cfg.L2OutputOracleAddr, cfg.L1Client)
if err != nil { if err != nil {
cancel() cancel()
return nil, err return nil, err
} }
version, err := l2ooContract.Version(&bind.CallOpts{}) cCtx, cCancel = context.WithTimeout(ctx, defaultDialTimeout)
defer cCancel()
version, err := l2ooContract.Version(&bind.CallOpts{Context: cCtx})
if err != nil { if err != nil {
cancel() cancel()
return nil, err return nil, err
} }
log.Info("Connected to L2OutputOracle", "address", cfg.L2OutputOracleAddr, "version", version) log.Info("Connected to L2OutputOracle", "address", cfg.L2OutputOracleAddr, "version", version)
parsed, err := abi.JSON(strings.NewReader(bindings.L2OutputOracleMetaData.ABI)) parsed, err := bindings.L2OutputOracleMetaData.GetAbi()
if err != nil { if err != nil {
cancel() cancel()
return nil, err return nil, err
} }
rawL2ooContract := bind.NewBoundContract(cfg.L2OutputOracleAddr, parsed, cfg.L1Client, cfg.L1Client, cfg.L1Client)
return &L2OutputSubmitter{ return &L2OutputSubmitter{
txMgr: txmgr.NewSimpleTxManager("proposer", l, cfg.TxManagerConfig, cfg.L1Client), txMgr: txmgr.NewSimpleTxManager("proposer", l, cfg.TxManagerConfig, cfg.L1Client),
...@@ -243,11 +241,11 @@ func NewL2OutputSubmitter(cfg Config, l log.Logger, m metrics.Metricer) (*L2Outp ...@@ -243,11 +241,11 @@ func NewL2OutputSubmitter(cfg Config, l log.Logger, m metrics.Metricer) (*L2Outp
rollupClient: cfg.RollupClient, rollupClient: cfg.RollupClient,
l2ooContract: l2ooContract, l2ooContract: l2ooContract,
rawL2ooContract: rawL2ooContract, l2ooContractAddr: cfg.L2OutputOracleAddr,
l2ooABI: parsed,
allowNonFinalized: cfg.AllowNonFinalized, allowNonFinalized: cfg.AllowNonFinalized,
from: cfg.From, from: cfg.From,
signerFn: signer,
pollInterval: cfg.PollInterval, pollInterval: cfg.PollInterval,
}, nil }, nil
} }
...@@ -264,29 +262,14 @@ func (l *L2OutputSubmitter) Stop() { ...@@ -264,29 +262,14 @@ func (l *L2OutputSubmitter) Stop() {
l.wg.Wait() l.wg.Wait()
} }
// 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 (l *L2OutputSubmitter) UpdateGasPrice(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
opts := &bind.TransactOpts{
From: l.from,
Signer: func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
return l.signerFn(ctx, addr, tx)
},
Context: ctx,
Nonce: new(big.Int).SetUint64(tx.Nonce()),
NoSend: true,
}
return l.rawL2ooContract.RawTransact(opts, tx.Data())
}
// FetchNextOutputInfo gets the block number of the next proposal. // FetchNextOutputInfo gets the block number of the next proposal.
// It returns: the next block number, if the proposal should be made, error // It returns: the next block number, if the proposal should be made, error
func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.OutputResponse, bool, error) { func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.OutputResponse, bool, error) {
cCtx, cancel := context.WithTimeout(ctx, defaultDialTimeout)
defer cancel()
callOpts := &bind.CallOpts{ callOpts := &bind.CallOpts{
From: l.from, From: l.from,
Context: ctx, Context: cCtx,
} }
nextCheckpointBlock, err := l.l2ooContract.NextBlockNumber(callOpts) nextCheckpointBlock, err := l.l2ooContract.NextBlockNumber(callOpts)
if err != nil { if err != nil {
...@@ -294,7 +277,9 @@ func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.Outpu ...@@ -294,7 +277,9 @@ func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.Outpu
return nil, false, err return nil, false, err
} }
// Fetch the current L2 heads // Fetch the current L2 heads
status, err := l.rollupClient.SyncStatus(ctx) cCtx, cancel = context.WithTimeout(ctx, defaultDialTimeout)
defer cancel()
status, err := l.rollupClient.SyncStatus(cCtx)
if err != nil { if err != nil {
l.log.Error("proposer unable to get sync status", "err", err) l.log.Error("proposer unable to get sync status", "err", err)
return nil, false, err return nil, false, err
...@@ -312,17 +297,23 @@ func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.Outpu ...@@ -312,17 +297,23 @@ func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.Outpu
return nil, false, nil return nil, false, nil
} }
output, err := l.rollupClient.OutputAtBlock(ctx, nextCheckpointBlock.Uint64()) return l.fetchOuput(ctx, nextCheckpointBlock)
}
func (l *L2OutputSubmitter) fetchOuput(ctx context.Context, block *big.Int) (*eth.OutputResponse, bool, error) {
ctx, cancel := context.WithTimeout(ctx, defaultDialTimeout)
defer cancel()
output, err := l.rollupClient.OutputAtBlock(ctx, block.Uint64())
if err != nil { if err != nil {
l.log.Error("failed to fetch output at block %d: %w", nextCheckpointBlock, err) l.log.Error("failed to fetch output at block %d: %w", block, err)
return nil, false, err return nil, false, err
} }
if output.Version != supportedL2OutputVersion { if output.Version != supportedL2OutputVersion {
l.log.Error("unsupported l2 output version: %s", output.Version) l.log.Error("unsupported l2 output version: %s", output.Version)
return nil, false, errors.New("unsupported l2 output version") return nil, false, errors.New("unsupported l2 output version")
} }
if output.BlockRef.Number != nextCheckpointBlock.Uint64() { // sanity check, e.g. in case of bad RPC caching if output.BlockRef.Number != block.Uint64() { // sanity check, e.g. in case of bad RPC caching
l.log.Error("invalid blockNumber: next blockNumber is %v, blockNumber of block is %v", nextCheckpointBlock, output.BlockRef.Number) l.log.Error("invalid blockNumber: next blockNumber is %v, blockNumber of block is %v", block, output.BlockRef.Number)
return nil, false, errors.New("invalid blockNumber") return nil, false, errors.New("invalid blockNumber")
} }
...@@ -338,54 +329,36 @@ func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.Outpu ...@@ -338,54 +329,36 @@ func (l *L2OutputSubmitter) FetchNextOutputInfo(ctx context.Context) (*eth.Outpu
return output, true, nil return output, true, nil
} }
// CreateProposalTx transforms an output response into a signed output transaction. // ProposeL2OutputTxData creates the transaction data for the ProposeL2Output function
// It does not send the transaction to the transaction pool. func (l *L2OutputSubmitter) ProposeL2OutputTxData(output *eth.OutputResponse) ([]byte, error) {
func (l *L2OutputSubmitter) CreateProposalTx(ctx context.Context, output *eth.OutputResponse) (*types.Transaction, error) { return proposeL2OutputTxData(l.l2ooABI, output)
nonce, err := l.l1Client.NonceAt(ctx, l.from, nil) }
if err != nil {
l.log.Error("Failed to get nonce", "err", err, "from", l.from)
return nil, err
}
opts := &bind.TransactOpts{
From: l.from,
Signer: func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) {
return l.signerFn(ctx, addr, tx)
},
Context: ctx,
Nonce: new(big.Int).SetUint64(nonce),
NoSend: true,
}
tx, err := l.l2ooContract.ProposeL2Output( // proposeL2OutputTxData creates the transaction data for the ProposeL2Output function
opts, func proposeL2OutputTxData(abi *abi.ABI, output *eth.OutputResponse) ([]byte, error) {
return abi.Pack(
"proposeL2Output",
output.OutputRoot, output.OutputRoot,
new(big.Int).SetUint64(output.BlockRef.Number), new(big.Int).SetUint64(output.BlockRef.Number),
output.Status.CurrentL1.Hash, output.Status.CurrentL1.Hash,
new(big.Int).SetUint64(output.Status.CurrentL1.Number)) new(big.Int).SetUint64(output.Status.CurrentL1.Number))
if err != nil {
l.log.Error("failed to create the ProposeL2Output transaction", "err", err)
return nil, err
}
return tx, nil
} }
// SendTransaction sends a transaction through the transaction manager which handles automatic // sendTransaction creates & sends transactions through the underlying transaction manager.
// price bumping. func (l *L2OutputSubmitter) sendTransaction(ctx context.Context, output *eth.OutputResponse) error {
// It also hardcodes a timeout of 100s. data, err := l.ProposeL2OutputTxData(output)
func (l *L2OutputSubmitter) SendTransaction(ctx context.Context, tx *types.Transaction) error { if err != nil {
// Wait until one of our submitted transactions confirms. If no return err
// receipt is received it's likely our gas price was too low. }
cCtx, cancel := context.WithTimeout(ctx, 100*time.Second) receipt, err := l.txMgr.Send(ctx, txmgr.TxCandidate{
defer cancel() TxData: data,
l.log.Info("Sending transaction", "tx_hash", tx.Hash()) To: l.l2ooContractAddr,
receipt, err := l.txMgr.Send(cCtx, tx) GasLimit: 0,
From: l.from,
})
if err != nil { if err != nil {
l.log.Error("proposer unable to publish tx", "err", err)
return err return err
} }
// The transaction was successfully submitted
l.log.Info("proposer tx successfully published", "tx_hash", receipt.TxHash) l.log.Info("proposer tx successfully published", "tx_hash", receipt.TxHash)
return nil return nil
} }
...@@ -401,9 +374,7 @@ func (l *L2OutputSubmitter) loop() { ...@@ -401,9 +374,7 @@ func (l *L2OutputSubmitter) loop() {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
cCtx, cancel := context.WithTimeout(ctx, 30*time.Second) output, shouldPropose, err := l.FetchNextOutputInfo(ctx)
output, shouldPropose, err := l.FetchNextOutputInfo(cCtx)
cancel()
if err != nil { if err != nil {
break break
} }
...@@ -411,15 +382,8 @@ func (l *L2OutputSubmitter) loop() { ...@@ -411,15 +382,8 @@ func (l *L2OutputSubmitter) loop() {
break break
} }
cCtx, cancel = context.WithTimeout(ctx, 30*time.Second) cCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
tx, err := l.CreateProposalTx(cCtx, output) if err := l.sendTransaction(cCtx, output); err != nil {
cancel()
if err != nil {
l.log.Error("Failed to create proposal transaction", "err", err)
break
}
cCtx, cancel = context.WithTimeout(ctx, 10*time.Minute)
if err := l.SendTransaction(cCtx, tx); err != nil {
l.log.Error("Failed to send proposal transaction", "err", err) l.log.Error("Failed to send proposal transaction", "err", err)
cancel() cancel()
break break
......
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