Commit 05d367e5 authored by protolambda's avatar protolambda Committed by GitHub

op-proposer: cleanup proposer logic, update outputAtBlock API (#3805)

* op-proposer: cleanup proposer logic, update outputAtBlock API

* op-e2e,op-proposer: action test proposer

* fix flags
Co-authored-by: default avatarMatthew Slipper <me@matthewslipper.com>
parent 3f6c6701
package actions
import (
"crypto/ecdsa"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-proposer/drivers/l2output"
)
type ProposerCfg struct {
OutputOracleAddr common.Address
ProposerKey *ecdsa.PrivateKey
AllowNonFinalized bool
}
type L2Proposer struct {
log log.Logger
l1 *ethclient.Client
driver *l2output.Driver
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)
dr, err := l2output.NewDriver(l2output.Config{
Log: log,
Name: "proposer",
L1Client: l1,
RollupClient: rollupCl,
AllowNonFinalized: cfg.AllowNonFinalized,
L2OOAddr: cfg.OutputOracleAddr,
ChainID: chainID,
PrivKey: cfg.ProposerKey,
})
require.NoError(t, err)
return &L2Proposer{
log: log,
l1: l1,
driver: dr,
address: crypto.PubkeyToAddress(cfg.ProposerKey.PublicKey),
}
}
func (p *L2Proposer) CanPropose(t Testing) bool {
start, end, err := p.driver.GetBlockRange(t.Ctx())
require.NoError(t, err)
return start.Cmp(end) < 0
}
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())
}
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))
require.NoError(t, err)
err = p.driver.SendTransaction(t.Ctx(), tx)
require.NoError(t, err)
p.lastTx = tx.Hash()
}
func (p *L2Proposer) LastProposalTx() common.Hash {
return p.lastTx
}
package actions
import (
"math/big"
"testing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/testlog"
)
func TestProposer(gt *testing.T) {
t := NewDefaultTesting(gt)
dp := e2eutils.MakeDeployParams(t, defaultRollupTestParams)
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
miner, seqEngine, sequencer := setupSequencerTest(t, sd, log)
rollupSeqCl := sequencer.RollupClient()
batcher := NewL2Batcher(log, sd.RollupCfg, &BatcherCfg{
MinL1TxSize: 0,
MaxL1TxSize: 128_000,
BatcherKey: dp.Secrets.Batcher,
}, rollupSeqCl, miner.EthClient(), seqEngine.EthClient())
proposer := NewL2Proposer(t, log, &ProposerCfg{
OutputOracleAddr: sd.DeploymentsL1.L2OutputOracleProxy,
ProposerKey: dp.Secrets.Proposer,
AllowNonFinalized: false,
}, miner.EthClient(), sequencer.RollupClient())
// L1 block
miner.ActEmptyBlock(t)
// L2 block
sequencer.ActL1HeadSignal(t)
sequencer.ActL2PipelineFull(t)
sequencer.ActBuildToL1Head(t)
// submit and include in L1
batcher.ActSubmitAll(t)
miner.ActL1StartBlock(12)(t)
miner.ActL1IncludeTx(dp.Addresses.Batcher)(t)
miner.ActL1EndBlock(t)
// finalize the first and second L1 blocks, including the batch
miner.ActL1SafeNext(t)
miner.ActL1SafeNext(t)
miner.ActL1FinalizeNext(t)
miner.ActL1FinalizeNext(t)
// derive and see the L2 chain fully finalize
sequencer.ActL2PipelineFull(t)
sequencer.ActL1SafeSignal(t)
sequencer.ActL1FinalizedSignal(t)
require.Equal(t, sequencer.SyncStatus().UnsafeL2, sequencer.SyncStatus().FinalizedL2)
// make proposals until there is nothing left to propose
for proposer.CanPropose(t) {
// and propose it to L1
proposer.ActMakeProposalTx(t)
// include proposal on L1
miner.ActL1StartBlock(12)(t)
miner.ActL1IncludeTx(dp.Addresses.Proposer)(t)
miner.ActL1EndBlock(t)
// Check proposal was successful
receipt, err := miner.EthClient().TransactionReceipt(t.Ctx(), proposer.LastProposalTx())
require.NoError(t, err)
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "proposal failed")
}
// check that L1 stored the expected output root
outputOracleContract, err := bindings.NewL2OutputOracle(sd.DeploymentsL1.L2OutputOracleProxy, miner.EthClient())
require.NoError(t, err)
block := sequencer.SyncStatus().FinalizedL2
outputOnL1, err := outputOracleContract.GetL2Output(nil, new(big.Int).SetUint64(block.Number))
require.NoError(t, err)
require.Less(t, block.Time, outputOnL1.Timestamp.Uint64(), "output is registered with L1 timestamp of proposal tx, past L2 block")
outputComputed, err := sequencer.RollupClient().OutputAtBlock(t.Ctx(), block.Number)
require.NoError(t, err)
require.Equal(t, eth.Bytes32(outputOnL1.OutputRoot), outputComputed.OutputRoot, "output roots must match")
}
......@@ -26,7 +26,10 @@ import (
type L2Verifier struct {
log log.Logger
eng derive.Engine
eng interface {
derive.Engine
L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error)
}
// L2 rollup
derivation *derive.DerivationPipeline
......@@ -46,7 +49,8 @@ type L2Verifier struct {
type L2API interface {
derive.Engine
InfoByRpcNumber(ctx context.Context, num rpc.BlockNumber) (eth.BlockInfo, error)
L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error)
InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error)
// GetProof returns a proof of the account, it may return a nil result without error if the address was not found.
GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error)
}
......@@ -95,6 +99,11 @@ type l2VerifierBackend struct {
verifier *L2Verifier
}
func (s *l2VerifierBackend) BlockRefWithStatus(ctx context.Context, num uint64) (eth.L2BlockRef, *eth.SyncStatus, error) {
ref, err := s.verifier.eng.L2BlockRefByNumber(ctx, num)
return ref, s.verifier.SyncStatus(), err
}
func (s *l2VerifierBackend) SyncStatus(ctx context.Context) (*eth.SyncStatus, error) {
return s.verifier.SyncStatus(), nil
}
......
......@@ -11,6 +11,17 @@ import (
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
geth_eth "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
mocknet "github.com/libp2p/go-libp2p/p2p/net/mock"
"github.com/stretchr/testify/require"
bss "github.com/ethereum-optimism/optimism/op-batcher"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
......@@ -24,16 +35,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/testlog"
l2os "github.com/ethereum-optimism/optimism/op-proposer"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
geth_eth "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
mocknet "github.com/libp2p/go-libp2p/p2p/net/mock"
"github.com/stretchr/testify/require"
)
var (
......@@ -140,6 +141,7 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
"proposer": testlog.Logger(t, log.LvlCrit).New("role", "proposer"),
},
P2PTopology: nil, // no P2P connectivity by default
NonFinalizedProposals: false,
}
}
......@@ -181,6 +183,9 @@ type SystemConfig struct {
// A nil map disables P2P completely.
// Any node name not in the topology will not have p2p enabled.
P2PTopology map[string][]string
// If the proposer can make proposals for L2 blocks derived from L1 blocks which are not finalized on L1 yet.
NonFinalizedProposals bool
}
type System struct {
......@@ -479,13 +484,13 @@ func (cfg SystemConfig) Start() (*System, error) {
// L2Output Submitter
sys.L2OutputSubmitter, err = l2os.NewL2OutputSubmitter(l2os.Config{
L1EthRpc: sys.Nodes["l1"].WSEndpoint(),
L2EthRpc: sys.Nodes["sequencer"].WSEndpoint(),
RollupRpc: sys.RollupNodes["sequencer"].HTTPEndpoint(),
L2OOAddress: predeploys.DevL2OutputOracleAddr.String(),
PollInterval: 50 * time.Millisecond,
NumConfirmations: 1,
ResubmissionTimeout: 3 * time.Second,
SafeAbortNonceTooLowCount: 3,
AllowNonFinalized: cfg.NonFinalizedProposals,
LogConfig: oplog.CLIConfig{
Level: "info",
Format: "text",
......
......@@ -8,14 +8,6 @@ import (
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-node/withdrawals"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
......@@ -27,6 +19,15 @@ import (
"github.com/ethereum/go-ethereum/rpc"
"github.com/libp2p/go-libp2p-core/peer"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-node/withdrawals"
)
// Init testing to enable test flags
......@@ -49,6 +50,7 @@ func TestL2OutputSubmitter(t *testing.T) {
}
cfg := DefaultSystemConfig(t)
cfg.NonFinalizedProposals = true // speed up the time till we see output proposals
sys, err := cfg.Start()
require.Nil(t, err, "Error starting up system")
......@@ -99,11 +101,9 @@ func TestL2OutputSubmitter(t *testing.T) {
// finalized.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
l2Output, err := rollupClient.OutputAtBlock(ctx, l2ooBlockNumber)
l2Output, err := rollupClient.OutputAtBlock(ctx, l2ooBlockNumber.Uint64())
require.Nil(t, err)
require.Len(t, l2Output, 2)
require.Equal(t, l2Output[1][:], committedL2Output.OutputRoot[:])
require.Equal(t, l2Output.OutputRoot[:], committedL2Output.OutputRoot[:])
break
}
......
package eth
import (
"github.com/ethereum/go-ethereum/common"
)
type OutputResponse struct {
Version Bytes32 `json:"version"`
OutputRoot Bytes32 `json:"outputRoot"`
BlockRef L2BlockRef `json:"blockRef"`
WithdrawalStorageRoot common.Hash `json:"withdrawalStorageRoot"`
StateRoot common.Hash `json:"stateRoot"`
Status *SyncStatus `json:"syncStatus"`
}
......@@ -4,24 +4,26 @@ import (
"context"
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/version"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
)
type l2EthClient interface {
InfoByRpcNumber(ctx context.Context, num rpc.BlockNumber) (eth.BlockInfo, error)
InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error)
// GetProof returns a proof of the account, it may return a nil result without error if the address was not found.
GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error)
}
type driverClient interface {
SyncStatus(ctx context.Context) (*eth.SyncStatus, error)
BlockRefWithStatus(ctx context.Context, num uint64) (eth.L2BlockRef, *eth.SyncStatus, error)
ResetDerivationPipeline(context.Context) error
}
......@@ -66,38 +68,47 @@ func NewNodeAPI(config *rollup.Config, l2Client l2EthClient, dr driverClient, lo
}
}
func (n *nodeAPI) OutputAtBlock(ctx context.Context, number rpc.BlockNumber) ([]eth.Bytes32, error) {
func (n *nodeAPI) OutputAtBlock(ctx context.Context, number hexutil.Uint64) (*eth.OutputResponse, error) {
recordDur := n.m.RecordRPCServerRequest("optimism_outputAtBlock")
defer recordDur()
// TODO: rpc.BlockNumber doesn't support the "safe" tag. Need a new type
head, err := n.client.InfoByRpcNumber(ctx, number)
ref, status, err := n.dr.BlockRefWithStatus(ctx, uint64(number))
if err != nil {
n.log.Error("failed to get block", "err", err)
return nil, err
return nil, fmt.Errorf("failed to get L2 block ref with sync status: %w", err)
}
head, err := n.client.InfoByHash(ctx, ref.Hash)
if err != nil {
return nil, fmt.Errorf("failed to get L2 block by hash %s: %w", ref, err)
}
if head == nil {
return nil, ethereum.NotFound
}
proof, err := n.client.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, toBlockNumArg(number))
proof, err := n.client.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, ref.Hash.String())
if err != nil {
n.log.Error("failed to get contract proof", "err", err)
return nil, err
return nil, fmt.Errorf("failed to get contract proof at block %s: %w", ref, err)
}
if proof == nil {
return nil, ethereum.NotFound
return nil, fmt.Errorf("proof %w", ethereum.NotFound)
}
// make sure that the proof (including storage hash) that we retrieved is correct by verifying it against the state-root
if err := proof.Verify(head.Root()); err != nil {
n.log.Error("invalid withdrawal root detected in block", "stateRoot", head.Root(), "blocknum", number, "msg", err)
return nil, fmt.Errorf("invalid withdrawal root hash")
return nil, fmt.Errorf("invalid withdrawal root hash, state root was %s: %w", head.Root(), err)
}
var l2OutputRootVersion eth.Bytes32 // it's zero for now
l2OutputRoot := rollup.ComputeL2OutputRoot(l2OutputRootVersion, head.Hash(), head.Root(), proof.StorageHash)
return []eth.Bytes32{l2OutputRootVersion, l2OutputRoot}, nil
return &eth.OutputResponse{
Version: l2OutputRootVersion,
OutputRoot: l2OutputRoot,
BlockRef: ref,
WithdrawalStorageRoot: proof.StorageHash,
StateRoot: head.Root(),
Status: status,
}, nil
}
func (n *nodeAPI) SyncStatus(ctx context.Context) (*eth.SyncStatus, error) {
......@@ -117,9 +128,3 @@ func (n *nodeAPI) Version(ctx context.Context) (string, error) {
defer recordDur()
return version.Version + "-" + version.Meta, nil
}
func toBlockNumArg(number rpc.BlockNumber) string {
// never returns an error
out, _ := number.MarshalText()
return string(out)
}
......@@ -6,9 +6,15 @@ import (
"math/rand"
"testing"
rpcclient "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
rpcclient "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/eth"
......@@ -17,9 +23,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum-optimism/optimism/op-node/version"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
)
func TestOutputAtBlock(t *testing.T) {
......@@ -92,24 +95,40 @@ func TestOutputAtBlock(t *testing.T) {
InfoBaseFee: header.BaseFee,
InfoReceiptRoot: header.ReceiptHash,
}
l2Client.ExpectInfoByRpcNumber(rpc.LatestBlockNumber, info, nil)
l2Client.ExpectGetProof(predeploys.L2ToL1MessagePasserAddr, "latest", &result, nil)
ref := eth.L2BlockRef{
Hash: header.Hash(),
Number: header.Number.Uint64(),
ParentHash: header.ParentHash,
Time: header.Time,
L1Origin: eth.BlockID{},
SequenceNumber: 0,
}
l2Client.ExpectInfoByHash(common.HexToHash("0x8512bee03061475e4b069171f7b406097184f16b22c3f5c97c0abfc49591c524"), info, nil)
l2Client.ExpectGetProof(predeploys.L2ToL1MessagePasserAddr, "0x8512bee03061475e4b069171f7b406097184f16b22c3f5c97c0abfc49591c524", &result, nil)
drClient := &mockDriverClient{}
status := randomSyncStatus(rand.New(rand.NewSource(123)))
drClient.ExpectBlockRefWithStatus(0xdcdc89, ref, status, nil)
server, err := newRPCServer(context.Background(), rpcCfg, rollupCfg, l2Client, drClient, log, "0.0", metrics.NewMetrics(""))
assert.NoError(t, err)
assert.NoError(t, server.Start())
require.NoError(t, err)
require.NoError(t, server.Start())
defer server.Stop()
client, err := rpcclient.DialRPCClientWithBackoff(context.Background(), log, "http://"+server.Addr().String())
assert.NoError(t, err)
require.NoError(t, err)
var out []eth.Bytes32
err = client.CallContext(context.Background(), &out, "optimism_outputAtBlock", "latest")
assert.NoError(t, err)
assert.Len(t, out, 2)
var out *eth.OutputResponse
err = client.CallContext(context.Background(), &out, "optimism_outputAtBlock", "0xdcdc89")
require.NoError(t, err)
require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", out.Version.String())
require.Equal(t, "0xc861dbdc5bf1d8bbbc0bca7cd876ab6a70748c50b2054a46e8f30e99002170ab", out.OutputRoot.String())
require.Equal(t, "0xb46d4bcb0e471e1b8506031a1f34ebc6f200253cbaba56246dd2320e8e2c8f13", out.StateRoot.String())
require.Equal(t, "0xc1917a80cb25ccc50d0d1921525a44fb619b4601194ca726ae32312f08a799f8", out.WithdrawalStorageRoot.String())
require.Equal(t, *status, *out.Status)
l2Client.Mock.AssertExpectations(t)
drClient.Mock.AssertExpectations(t)
}
func TestVersion(t *testing.T) {
......@@ -137,19 +156,26 @@ func TestVersion(t *testing.T) {
assert.Equal(t, version.Version+"-"+version.Meta, out)
}
func TestSyncStatus(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
l2Client := &testutils.MockL2Client{}
drClient := &mockDriverClient{}
rng := rand.New(rand.NewSource(1234))
status := eth.SyncStatus{
func randomSyncStatus(rng *rand.Rand) *eth.SyncStatus {
return &eth.SyncStatus{
CurrentL1: testutils.RandomBlockRef(rng),
CurrentL1Finalized: testutils.RandomBlockRef(rng),
HeadL1: testutils.RandomBlockRef(rng),
SafeL1: testutils.RandomBlockRef(rng),
FinalizedL1: testutils.RandomBlockRef(rng),
UnsafeL2: testutils.RandomL2BlockRef(rng),
SafeL2: testutils.RandomL2BlockRef(rng),
FinalizedL2: testutils.RandomL2BlockRef(rng),
}
drClient.On("SyncStatus").Return(&status)
}
func TestSyncStatus(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
l2Client := &testutils.MockL2Client{}
drClient := &mockDriverClient{}
rng := rand.New(rand.NewSource(1234))
status := randomSyncStatus(rng)
drClient.On("SyncStatus").Return(status)
rpcCfg := &RPCConfig{
ListenAddr: "localhost",
......@@ -169,13 +195,22 @@ func TestSyncStatus(t *testing.T) {
var out *eth.SyncStatus
err = client.CallContext(context.Background(), &out, "optimism_syncStatus")
assert.NoError(t, err)
assert.Equal(t, &status, out)
assert.Equal(t, status, out)
}
type mockDriverClient struct {
mock.Mock
}
func (c *mockDriverClient) ExpectBlockRefWithStatus(num uint64, ref eth.L2BlockRef, status *eth.SyncStatus, err error) {
c.Mock.On("BlockRefWithStatus", num).Return(ref, status, &err)
}
func (c *mockDriverClient) BlockRefWithStatus(ctx context.Context, num uint64) (eth.L2BlockRef, *eth.SyncStatus, error) {
m := c.Mock.MethodCalled("BlockRefWithStatus", num)
return m[0].(eth.L2BlockRef), m[1].(*eth.SyncStatus), *m[2].(*error)
}
func (c *mockDriverClient) SyncStatus(ctx context.Context) (*eth.SyncStatus, error) {
return c.Mock.MethodCalled("SyncStatus").Get(0).(*eth.SyncStatus), nil
}
......
......@@ -5,15 +5,15 @@ import (
"fmt"
"io"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/sources/caching"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/sources/caching"
)
type EthClientConfig struct {
......@@ -193,11 +193,6 @@ func (s *EthClient) InfoByLabel(ctx context.Context, label eth.BlockLabel) (eth.
return s.headerCall(ctx, "eth_getBlockByNumber", string(label))
}
func (s *EthClient) InfoByRpcNumber(ctx context.Context, num rpc.BlockNumber) (eth.BlockInfo, error) {
// can't hit the cache when querying the head due to reorgs / changes.
return s.headerCall(ctx, "eth_getBlockByNumber", num)
}
func (s *EthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error) {
if header, ok := s.headersCache.Get(hash); ok {
if txs, ok := s.transactionsCache.Get(hash); ok {
......
......@@ -2,12 +2,12 @@ package sources
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/common/hexutil"
)
type RollupClient struct {
......@@ -18,9 +18,9 @@ func NewRollupClient(rpc client.RPC) *RollupClient {
return &RollupClient{rpc}
}
func (r *RollupClient) OutputAtBlock(ctx context.Context, blockNum *big.Int) ([]eth.Bytes32, error) {
var output []eth.Bytes32
err := r.rpc.CallContext(ctx, &output, "optimism_outputAtBlock", hexutil.EncodeBig(blockNum))
func (r *RollupClient) OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error) {
var output *eth.OutputResponse
err := r.rpc.CallContext(ctx, &output, "optimism_outputAtBlock", hexutil.Uint64(blockNum))
return output, err
}
......
......@@ -5,10 +5,10 @@ import (
"github.com/stretchr/testify/mock"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-node/eth"
)
type MockEthClient struct {
......@@ -42,15 +42,6 @@ func (m *MockEthClient) ExpectInfoByLabel(label eth.BlockLabel, info eth.BlockIn
m.Mock.On("InfoByLabel", label).Once().Return(&info, &err)
}
func (m *MockEthClient) InfoByRpcNumber(ctx context.Context, num rpc.BlockNumber) (eth.BlockInfo, error) {
out := m.Mock.MethodCalled("InfoByRpcNumber", num)
return *out[0].(*eth.BlockInfo), *out[1].(*error)
}
func (m *MockEthClient) ExpectInfoByRpcNumber(num rpc.BlockNumber, info eth.BlockInfo, err error) {
m.Mock.On("InfoByRpcNumber", num).Once().Return(&info, &err)
}
func (m *MockEthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error) {
out := m.Mock.MethodCalled("InfoAndTxsByHash", hash)
return out[0].(eth.BlockInfo), out[1].(types.Transactions), *out[2].(*error)
......
......@@ -18,9 +18,6 @@ type Config struct {
// L1EthRpc is the HTTP provider URL for L1.
L1EthRpc string
// L2EthRpc is the HTTP provider URL for L2.
L2EthRpc string
// RollupRpc is the HTTP provider URL for the rollup node.
RollupRpc string
......@@ -60,6 +57,10 @@ type Config struct {
/* Optional Params */
// AllowNonFinalized can be set to true to propose outputs
// for L2 blocks derived from non-finalized L1 data.
AllowNonFinalized bool
LogConfig oplog.CLIConfig
MetricsConfig opmetrics.CLIConfig
......@@ -88,7 +89,6 @@ func NewConfig(ctx *cli.Context) Config {
return Config{
/* Required Flags */
L1EthRpc: ctx.GlobalString(flags.L1EthRpcFlag.Name),
L2EthRpc: ctx.GlobalString(flags.L2EthRpcFlag.Name),
RollupRpc: ctx.GlobalString(flags.RollupRpcFlag.Name),
L2OOAddress: ctx.GlobalString(flags.L2OOAddressFlag.Name),
PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name),
......@@ -98,6 +98,7 @@ 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),
......
......@@ -9,8 +9,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
......@@ -18,6 +16,9 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"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)
......@@ -26,11 +27,25 @@ var supportedL2OutputVersion = eth.Bytes32{}
type Config struct {
Log log.Logger
Name string
// L1Client is used to submit transactions to
L1Client *ethclient.Client
L2Client *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
// ChainID is the L1 chain ID used for proposal transaction signing
ChainID *big.Int
// Privkey used for proposal transaction signing
PrivKey *ecdsa.PrivateKey
}
......@@ -43,9 +58,7 @@ type Driver struct {
}
func NewDriver(cfg Config) (*Driver, error) {
l2ooContract, err := bindings.NewL2OutputOracle(
cfg.L2OOAddr, cfg.L1Client,
)
l2ooContract, err := bindings.NewL2OutputOracle(cfg.L2OOAddr, cfg.L1Client)
if err != nil {
return nil, err
}
......@@ -62,7 +75,7 @@ func NewDriver(cfg Config) (*Driver, error) {
)
walletAddr := crypto.PubkeyToAddress(cfg.PrivKey.PublicKey)
log.Info("Configured driver", "wallet", walletAddr, "l2-output-contract", cfg.L2OOAddr)
cfg.Log.Info("Configured driver", "wallet", walletAddr, "l2-output-contract", cfg.L2OOAddr)
return &Driver{
cfg: cfg,
......@@ -86,9 +99,7 @@ func (d *Driver) 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.
func (d *Driver) GetBlockRange(
ctx context.Context) (*big.Int, *big.Int, error) {
func (d *Driver) GetBlockRange(ctx context.Context) (*big.Int, *big.Int, error) {
name := d.cfg.Name
callOpts := &bind.CallOpts{
......@@ -115,12 +126,12 @@ func (d *Driver) GetBlockRange(
d.l.Error(name+" unable to get sync status", "err", err)
return nil, nil, err
}
latestHeader, err := d.cfg.L2Client.HeaderByNumber(ctx, new(big.Int).SetUint64(status.SafeL2.Number))
if err != nil {
d.l.Error(name+" unable to retrieve latest header", "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)
}
currentBlockNumber := big.NewInt(latestHeader.Number.Int64())
// If we do not have the new L2 Block number
if currentBlockNumber.Cmp(nextBlockNumber) < 0 {
......@@ -144,46 +155,36 @@ func (d *Driver) GetBlockRange(
// 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) {
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)
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)
// 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()
l2OutputRoot, err := d.outputRootAtBlock(ctx, nextCheckpointBlock)
output, err := d.cfg.RollupClient.OutputAtBlock(ctx, nextCheckpointBlock)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to fetch output at block %d: %w", nextCheckpointBlock, err)
}
numElements := new(big.Int).Sub(start, end).Uint64()
d.l.Info(name+" checkpoint constructed", "start", start, "end", end,
"nonce", nonce, "blocks_committed", numElements, "checkpoint_block", nextCheckpointBlock)
l1Header, err := d.cfg.L1Client.HeaderByNumber(ctx, nil)
if err != nil {
return nil, fmt.Errorf("error resolving checkpoint block: %w", err)
if output.Version != supportedL2OutputVersion {
return nil, fmt.Errorf("unsupported l2 output version: %s", output.Version)
}
l2Header, err := d.cfg.L2Client.HeaderByNumber(ctx, nextCheckpointBlock)
if err != nil {
return nil, fmt.Errorf("error resolving checkpoint block: %w", err)
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)
}
if l2Header.Number.Cmp(nextCheckpointBlock) != 0 {
return nil, fmt.Errorf("invalid blockNumber: next blockNumber is %v, blockNumber of block is %v", nextCheckpointBlock, l2Header.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, err := bind.NewKeyedTransactorWithChainID(
d.cfg.PrivKey, d.cfg.ChainID,
)
opts, err := bind.NewKeyedTransactorWithChainID(d.cfg.PrivKey, d.cfg.ChainID)
if err != nil {
return nil, err
}
......@@ -191,18 +192,41 @@ func (d *Driver) CraftTx(
opts.Nonce = nonce
opts.NoSend = true
return d.l2ooContract.ProposeL2Output(opts, l2OutputRoot, nextCheckpointBlock, l1Header.Hash(), l1Header.Number)
// 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: Thie method SHOULD NOT publish the resulting transaction.
func (d *Driver) UpdateGasPrice(
ctx context.Context,
tx *types.Transaction,
) (*types.Transaction, error) {
// NOTE: This method SHOULD NOT publish the resulting transaction.
func (d *Driver) UpdateGasPrice(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
opts, err := bind.NewKeyedTransactorWithChainID(
d.cfg.PrivKey, d.cfg.ChainID,
)
......@@ -216,26 +240,8 @@ func (d *Driver) UpdateGasPrice(
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 {
// 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)
}
func (d *Driver) outputRootAtBlock(ctx context.Context, blockNum *big.Int) (eth.Bytes32, error) {
output, err := d.cfg.RollupClient.OutputAtBlock(ctx, blockNum)
if err != nil {
return eth.Bytes32{}, err
}
if len(output) != 2 {
return eth.Bytes32{}, fmt.Errorf("invalid outputAtBlock response")
}
if version := output[0]; version != supportedL2OutputVersion {
return eth.Bytes32{}, fmt.Errorf("unsupported l2 output version")
}
return output[1], nil
}
......@@ -21,12 +21,6 @@ var (
Required: true,
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "L1_ETH_RPC"),
}
L2EthRpcFlag = cli.StringFlag{
Name: "l2-eth-rpc",
Usage: "HTTP provider URL for L2",
Required: true,
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "L2_ETH_RPC"),
}
RollupRpcFlag = cli.StringFlag{
Name: "rollup-rpc",
Usage: "HTTP provider URL for the rollup node",
......@@ -68,6 +62,9 @@ var (
Required: true,
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "RESUBMISSION_TIMEOUT"),
}
/* Optional flags */
MnemonicFlag = cli.StringFlag{
Name: "mnemonic",
Usage: "The mnemonic used to derive the wallets for either the " +
......@@ -85,11 +82,15 @@ var (
Usage: "The private key to use with the l2output wallet. Must not be used with mnemonic.",
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "PRIVATE_KEY"),
}
AllowNonFinalizedFlag = cli.BoolFlag{
Name: "allow-non-finalized",
Usage: "Allow the proposer to submit proposals for L2 blocks derived from non-finalized L1 blocks.",
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "ALLOW_NON_FINALIZED"),
}
)
var requiredFlags = []cli.Flag{
L1EthRpcFlag,
L2EthRpcFlag,
RollupRpcFlag,
L2OOAddressFlag,
PollIntervalFlag,
......@@ -102,6 +103,7 @@ var optionalFlags = []cli.Flag{
MnemonicFlag,
L2OutputHDPathFlag,
PrivateKeyFlag,
AllowNonFinalizedFlag,
}
func init() {
......
......@@ -12,14 +12,6 @@ import (
"syscall"
"time"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-proposer/drivers/l2output"
"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"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
......@@ -28,6 +20,15 @@ import (
"github.com/ethereum/go-ethereum/rpc"
hdwallet "github.com/miguelmota/go-ethereum-hdwallet"
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-proposer/drivers/l2output"
"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"
)
const (
......@@ -172,11 +173,6 @@ func NewL2OutputSubmitter(
return nil, err
}
l2Client, err := dialEthClientWithTimeout(ctx, cfg.L2EthRpc)
if err != nil {
return nil, err
}
rollupClient, err := dialRollupClientWithTimeout(ctx, cfg.RollupRpc)
if err != nil {
return nil, err
......@@ -200,8 +196,8 @@ func NewL2OutputSubmitter(
Log: l,
Name: "L2Output Submitter",
L1Client: l1Client,
L2Client: l2Client,
RollupClient: rollupClient,
AllowNonFinalized: cfg.AllowNonFinalized,
L2OOAddr: l2ooAddress,
ChainID: chainID,
PrivKey: l2OutputPrivKey,
......
......@@ -89,7 +89,6 @@ services:
- "7302:7300"
environment:
OP_PROPOSER_L1_ETH_RPC: http://l1:8545
OP_PROPOSER_L2_ETH_RPC: http://l2:8545
OP_PROPOSER_ROLLUP_RPC: http://op-node:8545
OP_PROPOSER_POLL_INTERVAL: 1s
OP_PROPOSER_NUM_CONFIRMATIONS: 1
......@@ -101,6 +100,7 @@ services:
OP_PROPOSER_L2OO_ADDRESS: "${L2OO_ADDRESS}"
OP_PROPOSER_PPROF_ENABLED: "true"
OP_PROPOSER_METRICS_ENABLED: "true"
OP_PROPOSER_ALLOW_NON_FINALIZED: "true"
op-batcher:
depends_on:
......
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