diff --git a/op-e2e/actions/l2_proposer.go b/op-e2e/actions/l2_proposer.go new file mode 100644 index 0000000000000..dc91c0cf655a4 --- /dev/null +++ b/op-e2e/actions/l2_proposer.go @@ -0,0 +1,78 @@ +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 +} diff --git a/op-e2e/actions/l2_proposer_test.go b/op-e2e/actions/l2_proposer_test.go new file mode 100644 index 0000000000000..ea8698cb8b6e1 --- /dev/null +++ b/op-e2e/actions/l2_proposer_test.go @@ -0,0 +1,82 @@ +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") +} diff --git a/op-e2e/actions/l2_verifier.go b/op-e2e/actions/l2_verifier.go index de741bf74d404..c7eab809c348e 100644 --- a/op-e2e/actions/l2_verifier.go +++ b/op-e2e/actions/l2_verifier.go @@ -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 } diff --git a/op-e2e/setup.go b/op-e2e/setup.go index 3e2aab5d14a69..b982d4927b528 100644 --- a/op-e2e/setup.go +++ b/op-e2e/setup.go @@ -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 ( @@ -139,7 +140,8 @@ func DefaultSystemConfig(t *testing.T) SystemConfig { "batcher": testlog.Logger(t, log.LvlInfo).New("role", "batcher"), "proposer": testlog.Logger(t, log.LvlCrit).New("role", "proposer"), }, - P2PTopology: nil, // no P2P connectivity by default + 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", diff --git a/op-e2e/system_test.go b/op-e2e/system_test.go index 38b99b57ca572..acbbe288a4015 100644 --- a/op-e2e/system_test.go +++ b/op-e2e/system_test.go @@ -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 } diff --git a/op-node/eth/output.go b/op-node/eth/output.go new file mode 100644 index 0000000000000..9594775d48415 --- /dev/null +++ b/op-node/eth/output.go @@ -0,0 +1,14 @@ +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"` +} diff --git a/op-node/node/api.go b/op-node/node/api.go index e5a4b14c5ec45..b8dd7de6e329f 100644 --- a/op-node/node/api.go +++ b/op-node/node/api.go @@ -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 ð.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) -} diff --git a/op-node/node/server_test.go b/op-node/node/server_test.go index 83b9206551875..a1f42a972c906 100644 --- a/op-node/node/server_test.go +++ b/op-node/node/server_test.go @@ -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 randomSyncStatus(rng *rand.Rand) *eth.SyncStatus { + return ð.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), + } +} + 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{ - CurrentL1: testutils.RandomBlockRef(rng), - HeadL1: testutils.RandomBlockRef(rng), - UnsafeL2: testutils.RandomL2BlockRef(rng), - SafeL2: testutils.RandomL2BlockRef(rng), - FinalizedL2: testutils.RandomL2BlockRef(rng), - } - drClient.On("SyncStatus").Return(&status) + 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 } diff --git a/op-node/sources/eth_client.go b/op-node/sources/eth_client.go index 26b8fb235bae2..f70d8c5fb0f66 100644 --- a/op-node/sources/eth_client.go +++ b/op-node/sources/eth_client.go @@ -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 { diff --git a/op-node/sources/rollupclient.go b/op-node/sources/rollupclient.go index 577c69dcadc61..0747974e54dee 100644 --- a/op-node/sources/rollupclient.go +++ b/op-node/sources/rollupclient.go @@ -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 } diff --git a/op-node/testutils/mock_eth_client.go b/op-node/testutils/mock_eth_client.go index 2c377c4871336..9cd39b013ec0e 100644 --- a/op-node/testutils/mock_eth_client.go +++ b/op-node/testutils/mock_eth_client.go @@ -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) diff --git a/op-proposer/config.go b/op-proposer/config.go index 95b25ec99621d..f407b8ee5af58 100644 --- a/op-proposer/config.go +++ b/op-proposer/config.go @@ -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), diff --git a/op-proposer/drivers/l2output/driver.go b/op-proposer/drivers/l2output/driver.go index bdca9592e66fc..7282397a9468a 100644 --- a/op-proposer/drivers/l2output/driver.go +++ b/op-proposer/drivers/l2output/driver.go @@ -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,20 +16,37 @@ 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) var supportedL2OutputVersion = eth.Bytes32{} type Config struct { - Log log.Logger - Name string - L1Client *ethclient.Client - L2Client *ethclient.Client + 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 - L2OOAddr common.Address - ChainID *big.Int - PrivKey *ecdsa.PrivateKey + + // 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 } type Driver struct { @@ -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 -} diff --git a/op-proposer/flags/flags.go b/op-proposer/flags/flags.go index 4550c14b42a8d..09bccc4ef7a77 100644 --- a/op-proposer/flags/flags.go +++ b/op-proposer/flags/flags.go @@ -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() { diff --git a/op-proposer/l2_output_submitter.go b/op-proposer/l2_output_submitter.go index a5655a3a1b007..4ecbe71ca2a8f 100644 --- a/op-proposer/l2_output_submitter.go +++ b/op-proposer/l2_output_submitter.go @@ -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 @@ -197,14 +193,14 @@ func NewL2OutputSubmitter( } l2OutputDriver, err := l2output.NewDriver(l2output.Config{ - Log: l, - Name: "L2Output Submitter", - L1Client: l1Client, - L2Client: l2Client, - RollupClient: rollupClient, - L2OOAddr: l2ooAddress, - ChainID: chainID, - PrivKey: l2OutputPrivKey, + Log: l, + Name: "L2Output Submitter", + L1Client: l1Client, + RollupClient: rollupClient, + AllowNonFinalized: cfg.AllowNonFinalized, + L2OOAddr: l2ooAddress, + ChainID: chainID, + PrivKey: l2OutputPrivKey, }) if err != nil { return nil, err diff --git a/ops-bedrock/docker-compose.yml b/ops-bedrock/docker-compose.yml index 80335f9340a46..967bdc5a7f365 100644 --- a/ops-bedrock/docker-compose.yml +++ b/ops-bedrock/docker-compose.yml @@ -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: