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 ( ...@@ -26,7 +26,10 @@ import (
type L2Verifier struct { type L2Verifier struct {
log log.Logger log log.Logger
eng derive.Engine eng interface {
derive.Engine
L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error)
}
// L2 rollup // L2 rollup
derivation *derive.DerivationPipeline derivation *derive.DerivationPipeline
...@@ -46,7 +49,8 @@ type L2Verifier struct { ...@@ -46,7 +49,8 @@ type L2Verifier struct {
type L2API interface { type L2API interface {
derive.Engine 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 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) GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error)
} }
...@@ -95,6 +99,11 @@ type l2VerifierBackend struct { ...@@ -95,6 +99,11 @@ type l2VerifierBackend struct {
verifier *L2Verifier 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) { func (s *l2VerifierBackend) SyncStatus(ctx context.Context) (*eth.SyncStatus, error) {
return s.verifier.SyncStatus(), nil return s.verifier.SyncStatus(), nil
} }
......
...@@ -11,6 +11,17 @@ import ( ...@@ -11,6 +11,17 @@ import (
"testing" "testing"
"time" "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" bss "github.com/ethereum-optimism/optimism/op-batcher"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
...@@ -24,16 +35,6 @@ import ( ...@@ -24,16 +35,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
l2os "github.com/ethereum-optimism/optimism/op-proposer" l2os "github.com/ethereum-optimism/optimism/op-proposer"
oplog "github.com/ethereum-optimism/optimism/op-service/log" 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 ( var (
...@@ -139,7 +140,8 @@ func DefaultSystemConfig(t *testing.T) SystemConfig { ...@@ -139,7 +140,8 @@ func DefaultSystemConfig(t *testing.T) SystemConfig {
"batcher": testlog.Logger(t, log.LvlInfo).New("role", "batcher"), "batcher": testlog.Logger(t, log.LvlInfo).New("role", "batcher"),
"proposer": testlog.Logger(t, log.LvlCrit).New("role", "proposer"), "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 { ...@@ -181,6 +183,9 @@ type SystemConfig struct {
// A nil map disables P2P completely. // A nil map disables P2P completely.
// Any node name not in the topology will not have p2p enabled. // Any node name not in the topology will not have p2p enabled.
P2PTopology map[string][]string 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 { type System struct {
...@@ -479,13 +484,13 @@ func (cfg SystemConfig) Start() (*System, error) { ...@@ -479,13 +484,13 @@ func (cfg SystemConfig) Start() (*System, error) {
// L2Output Submitter // L2Output Submitter
sys.L2OutputSubmitter, err = l2os.NewL2OutputSubmitter(l2os.Config{ sys.L2OutputSubmitter, err = l2os.NewL2OutputSubmitter(l2os.Config{
L1EthRpc: sys.Nodes["l1"].WSEndpoint(), L1EthRpc: sys.Nodes["l1"].WSEndpoint(),
L2EthRpc: sys.Nodes["sequencer"].WSEndpoint(),
RollupRpc: sys.RollupNodes["sequencer"].HTTPEndpoint(), RollupRpc: sys.RollupNodes["sequencer"].HTTPEndpoint(),
L2OOAddress: predeploys.DevL2OutputOracleAddr.String(), L2OOAddress: predeploys.DevL2OutputOracleAddr.String(),
PollInterval: 50 * time.Millisecond, PollInterval: 50 * time.Millisecond,
NumConfirmations: 1, NumConfirmations: 1,
ResubmissionTimeout: 3 * time.Second, ResubmissionTimeout: 3 * time.Second,
SafeAbortNonceTooLowCount: 3, SafeAbortNonceTooLowCount: 3,
AllowNonFinalized: cfg.NonFinalizedProposals,
LogConfig: oplog.CLIConfig{ LogConfig: oplog.CLIConfig{
Level: "info", Level: "info",
Format: "text", Format: "text",
......
...@@ -8,14 +8,6 @@ import ( ...@@ -8,14 +8,6 @@ import (
"testing" "testing"
"time" "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"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
...@@ -27,6 +19,15 @@ import ( ...@@ -27,6 +19,15 @@ import (
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/libp2p/go-libp2p-core/peer" "github.com/libp2p/go-libp2p-core/peer"
"github.com/stretchr/testify/require" "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 // Init testing to enable test flags
...@@ -49,6 +50,7 @@ func TestL2OutputSubmitter(t *testing.T) { ...@@ -49,6 +50,7 @@ func TestL2OutputSubmitter(t *testing.T) {
} }
cfg := DefaultSystemConfig(t) cfg := DefaultSystemConfig(t)
cfg.NonFinalizedProposals = true // speed up the time till we see output proposals
sys, err := cfg.Start() sys, err := cfg.Start()
require.Nil(t, err, "Error starting up system") require.Nil(t, err, "Error starting up system")
...@@ -99,11 +101,9 @@ func TestL2OutputSubmitter(t *testing.T) { ...@@ -99,11 +101,9 @@ func TestL2OutputSubmitter(t *testing.T) {
// finalized. // finalized.
ctx, cancel := context.WithTimeout(context.Background(), time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() defer cancel()
l2Output, err := rollupClient.OutputAtBlock(ctx, l2ooBlockNumber) l2Output, err := rollupClient.OutputAtBlock(ctx, l2ooBlockNumber.Uint64())
require.Nil(t, err) require.Nil(t, err)
require.Len(t, l2Output, 2) require.Equal(t, l2Output.OutputRoot[:], committedL2Output.OutputRoot[:])
require.Equal(t, l2Output[1][:], committedL2Output.OutputRoot[:])
break 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 ( ...@@ -4,24 +4,26 @@ import (
"context" "context"
"fmt" "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-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/version" "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 { 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 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) GetProof(ctx context.Context, address common.Address, blockTag string) (*eth.AccountResult, error)
} }
type driverClient interface { type driverClient interface {
SyncStatus(ctx context.Context) (*eth.SyncStatus, error) SyncStatus(ctx context.Context) (*eth.SyncStatus, error)
BlockRefWithStatus(ctx context.Context, num uint64) (eth.L2BlockRef, *eth.SyncStatus, error)
ResetDerivationPipeline(context.Context) error ResetDerivationPipeline(context.Context) error
} }
...@@ -66,38 +68,47 @@ func NewNodeAPI(config *rollup.Config, l2Client l2EthClient, dr driverClient, lo ...@@ -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") recordDur := n.m.RecordRPCServerRequest("optimism_outputAtBlock")
defer recordDur() 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 { if err != nil {
n.log.Error("failed to get block", "err", err) return nil, fmt.Errorf("failed to get L2 block ref with sync status: %w", err)
return nil, 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 { if head == nil {
return nil, ethereum.NotFound 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 { if err != nil {
n.log.Error("failed to get contract proof", "err", err) return nil, fmt.Errorf("failed to get contract proof at block %s: %w", ref, err)
return nil, err
} }
if proof == nil { 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 // 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 { if err := proof.Verify(head.Root()); err != nil {
n.log.Error("invalid withdrawal root detected in block", "stateRoot", head.Root(), "blocknum", number, "msg", err) 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 var l2OutputRootVersion eth.Bytes32 // it's zero for now
l2OutputRoot := rollup.ComputeL2OutputRoot(l2OutputRootVersion, head.Hash(), head.Root(), proof.StorageHash) 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) { func (n *nodeAPI) SyncStatus(ctx context.Context) (*eth.SyncStatus, error) {
...@@ -117,9 +128,3 @@ func (n *nodeAPI) Version(ctx context.Context) (string, error) { ...@@ -117,9 +128,3 @@ func (n *nodeAPI) Version(ctx context.Context) (string, error) {
defer recordDur() defer recordDur()
return version.Version + "-" + version.Meta, nil 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 ( ...@@ -6,9 +6,15 @@ import (
"math/rand" "math/rand"
"testing" "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/assert"
"github.com/stretchr/testify/mock" "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-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
...@@ -17,9 +23,6 @@ import ( ...@@ -17,9 +23,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-node/testutils" "github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum-optimism/optimism/op-node/version" "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) { func TestOutputAtBlock(t *testing.T) {
...@@ -92,24 +95,40 @@ func TestOutputAtBlock(t *testing.T) { ...@@ -92,24 +95,40 @@ func TestOutputAtBlock(t *testing.T) {
InfoBaseFee: header.BaseFee, InfoBaseFee: header.BaseFee,
InfoReceiptRoot: header.ReceiptHash, InfoReceiptRoot: header.ReceiptHash,
} }
l2Client.ExpectInfoByRpcNumber(rpc.LatestBlockNumber, info, nil) ref := eth.L2BlockRef{
l2Client.ExpectGetProof(predeploys.L2ToL1MessagePasserAddr, "latest", &result, nil) 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{} 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("")) server, err := newRPCServer(context.Background(), rpcCfg, rollupCfg, l2Client, drClient, log, "0.0", metrics.NewMetrics(""))
assert.NoError(t, err) require.NoError(t, err)
assert.NoError(t, server.Start()) require.NoError(t, server.Start())
defer server.Stop() defer server.Stop()
client, err := rpcclient.DialRPCClientWithBackoff(context.Background(), log, "http://"+server.Addr().String()) client, err := rpcclient.DialRPCClientWithBackoff(context.Background(), log, "http://"+server.Addr().String())
assert.NoError(t, err) require.NoError(t, err)
var out []eth.Bytes32 var out *eth.OutputResponse
err = client.CallContext(context.Background(), &out, "optimism_outputAtBlock", "latest") err = client.CallContext(context.Background(), &out, "optimism_outputAtBlock", "0xdcdc89")
assert.NoError(t, err) require.NoError(t, err)
assert.Len(t, out, 2)
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) l2Client.Mock.AssertExpectations(t)
drClient.Mock.AssertExpectations(t)
} }
func TestVersion(t *testing.T) { func TestVersion(t *testing.T) {
...@@ -137,19 +156,26 @@ func TestVersion(t *testing.T) { ...@@ -137,19 +156,26 @@ func TestVersion(t *testing.T) {
assert.Equal(t, version.Version+"-"+version.Meta, out) assert.Equal(t, version.Version+"-"+version.Meta, out)
} }
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),
}
}
func TestSyncStatus(t *testing.T) { func TestSyncStatus(t *testing.T) {
log := testlog.Logger(t, log.LvlError) log := testlog.Logger(t, log.LvlError)
l2Client := &testutils.MockL2Client{} l2Client := &testutils.MockL2Client{}
drClient := &mockDriverClient{} drClient := &mockDriverClient{}
rng := rand.New(rand.NewSource(1234)) rng := rand.New(rand.NewSource(1234))
status := eth.SyncStatus{ status := randomSyncStatus(rng)
CurrentL1: testutils.RandomBlockRef(rng), drClient.On("SyncStatus").Return(status)
HeadL1: testutils.RandomBlockRef(rng),
UnsafeL2: testutils.RandomL2BlockRef(rng),
SafeL2: testutils.RandomL2BlockRef(rng),
FinalizedL2: testutils.RandomL2BlockRef(rng),
}
drClient.On("SyncStatus").Return(&status)
rpcCfg := &RPCConfig{ rpcCfg := &RPCConfig{
ListenAddr: "localhost", ListenAddr: "localhost",
...@@ -169,13 +195,22 @@ func TestSyncStatus(t *testing.T) { ...@@ -169,13 +195,22 @@ func TestSyncStatus(t *testing.T) {
var out *eth.SyncStatus var out *eth.SyncStatus
err = client.CallContext(context.Background(), &out, "optimism_syncStatus") err = client.CallContext(context.Background(), &out, "optimism_syncStatus")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, &status, out) assert.Equal(t, status, out)
} }
type mockDriverClient struct { type mockDriverClient struct {
mock.Mock 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) { func (c *mockDriverClient) SyncStatus(ctx context.Context) (*eth.SyncStatus, error) {
return c.Mock.MethodCalled("SyncStatus").Get(0).(*eth.SyncStatus), nil return c.Mock.MethodCalled("SyncStatus").Get(0).(*eth.SyncStatus), nil
} }
......
...@@ -5,15 +5,15 @@ import ( ...@@ -5,15 +5,15 @@ import (
"fmt" "fmt"
"io" "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"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "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 { type EthClientConfig struct {
...@@ -193,11 +193,6 @@ func (s *EthClient) InfoByLabel(ctx context.Context, label eth.BlockLabel) (eth. ...@@ -193,11 +193,6 @@ func (s *EthClient) InfoByLabel(ctx context.Context, label eth.BlockLabel) (eth.
return s.headerCall(ctx, "eth_getBlockByNumber", string(label)) 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) { func (s *EthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error) {
if header, ok := s.headersCache.Get(hash); ok { if header, ok := s.headersCache.Get(hash); ok {
if txs, ok := s.transactionsCache.Get(hash); ok { if txs, ok := s.transactionsCache.Get(hash); ok {
......
...@@ -2,12 +2,12 @@ package sources ...@@ -2,12 +2,12 @@ package sources
import ( import (
"context" "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/client"
"github.com/ethereum-optimism/optimism/op-node/eth" "github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/common/hexutil"
) )
type RollupClient struct { type RollupClient struct {
...@@ -18,9 +18,9 @@ func NewRollupClient(rpc client.RPC) *RollupClient { ...@@ -18,9 +18,9 @@ func NewRollupClient(rpc client.RPC) *RollupClient {
return &RollupClient{rpc} return &RollupClient{rpc}
} }
func (r *RollupClient) OutputAtBlock(ctx context.Context, blockNum *big.Int) ([]eth.Bytes32, error) { func (r *RollupClient) OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error) {
var output []eth.Bytes32 var output *eth.OutputResponse
err := r.rpc.CallContext(ctx, &output, "optimism_outputAtBlock", hexutil.EncodeBig(blockNum)) err := r.rpc.CallContext(ctx, &output, "optimism_outputAtBlock", hexutil.Uint64(blockNum))
return output, err return output, err
} }
......
...@@ -5,10 +5,10 @@ import ( ...@@ -5,10 +5,10 @@ import (
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/ethereum-optimism/optimism/op-node/eth"
"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/rpc"
"github.com/ethereum-optimism/optimism/op-node/eth"
) )
type MockEthClient struct { type MockEthClient struct {
...@@ -42,15 +42,6 @@ func (m *MockEthClient) ExpectInfoByLabel(label eth.BlockLabel, info eth.BlockIn ...@@ -42,15 +42,6 @@ func (m *MockEthClient) ExpectInfoByLabel(label eth.BlockLabel, info eth.BlockIn
m.Mock.On("InfoByLabel", label).Once().Return(&info, &err) 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) { func (m *MockEthClient) InfoAndTxsByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, types.Transactions, error) {
out := m.Mock.MethodCalled("InfoAndTxsByHash", hash) out := m.Mock.MethodCalled("InfoAndTxsByHash", hash)
return out[0].(eth.BlockInfo), out[1].(types.Transactions), *out[2].(*error) return out[0].(eth.BlockInfo), out[1].(types.Transactions), *out[2].(*error)
......
...@@ -18,9 +18,6 @@ type Config struct { ...@@ -18,9 +18,6 @@ type Config struct {
// L1EthRpc is the HTTP provider URL for L1. // L1EthRpc is the HTTP provider URL for L1.
L1EthRpc string L1EthRpc string
// L2EthRpc is the HTTP provider URL for L2.
L2EthRpc string
// RollupRpc is the HTTP provider URL for the rollup node. // RollupRpc is the HTTP provider URL for the rollup node.
RollupRpc string RollupRpc string
...@@ -60,6 +57,10 @@ type Config struct { ...@@ -60,6 +57,10 @@ type Config struct {
/* Optional Params */ /* 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 LogConfig oplog.CLIConfig
MetricsConfig opmetrics.CLIConfig MetricsConfig opmetrics.CLIConfig
...@@ -88,7 +89,6 @@ func NewConfig(ctx *cli.Context) Config { ...@@ -88,7 +89,6 @@ func NewConfig(ctx *cli.Context) Config {
return Config{ return Config{
/* Required Flags */ /* Required Flags */
L1EthRpc: ctx.GlobalString(flags.L1EthRpcFlag.Name), L1EthRpc: ctx.GlobalString(flags.L1EthRpcFlag.Name),
L2EthRpc: ctx.GlobalString(flags.L2EthRpcFlag.Name),
RollupRpc: ctx.GlobalString(flags.RollupRpcFlag.Name), RollupRpc: ctx.GlobalString(flags.RollupRpcFlag.Name),
L2OOAddress: ctx.GlobalString(flags.L2OOAddressFlag.Name), L2OOAddress: ctx.GlobalString(flags.L2OOAddressFlag.Name),
PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name), PollInterval: ctx.GlobalDuration(flags.PollIntervalFlag.Name),
...@@ -98,6 +98,7 @@ func NewConfig(ctx *cli.Context) Config { ...@@ -98,6 +98,7 @@ func NewConfig(ctx *cli.Context) Config {
Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name), Mnemonic: ctx.GlobalString(flags.MnemonicFlag.Name),
L2OutputHDPath: ctx.GlobalString(flags.L2OutputHDPathFlag.Name), L2OutputHDPath: ctx.GlobalString(flags.L2OutputHDPathFlag.Name),
PrivateKey: ctx.GlobalString(flags.PrivateKeyFlag.Name), PrivateKey: ctx.GlobalString(flags.PrivateKeyFlag.Name),
AllowNonFinalized: ctx.GlobalBool(flags.AllowNonFinalizedFlag.Name),
RPCConfig: oprpc.ReadCLIConfig(ctx), RPCConfig: oprpc.ReadCLIConfig(ctx),
LogConfig: oplog.ReadCLIConfig(ctx), LogConfig: oplog.ReadCLIConfig(ctx),
MetricsConfig: opmetrics.ReadCLIConfig(ctx), MetricsConfig: opmetrics.ReadCLIConfig(ctx),
......
...@@ -9,8 +9,6 @@ import ( ...@@ -9,8 +9,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/sources" "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"
"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"
...@@ -18,20 +16,37 @@ import ( ...@@ -18,20 +16,37 @@ import (
"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-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/eth"
) )
var bigOne = big.NewInt(1) var bigOne = big.NewInt(1)
var supportedL2OutputVersion = eth.Bytes32{} var supportedL2OutputVersion = eth.Bytes32{}
type Config struct { type Config struct {
Log log.Logger Log log.Logger
Name string Name string
L1Client *ethclient.Client
L2Client *ethclient.Client // L1Client is used to submit transactions to
L1Client *ethclient.Client
// RollupClient is used to retrieve output roots from
RollupClient *sources.RollupClient RollupClient *sources.RollupClient
L2OOAddr common.Address
ChainID *big.Int // AllowNonFinalized enables the proposal of safe, but non-finalized L2 blocks.
PrivKey *ecdsa.PrivateKey // 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 { type Driver struct {
...@@ -43,9 +58,7 @@ type Driver struct { ...@@ -43,9 +58,7 @@ type Driver struct {
} }
func NewDriver(cfg Config) (*Driver, error) { func NewDriver(cfg Config) (*Driver, error) {
l2ooContract, err := bindings.NewL2OutputOracle( l2ooContract, err := bindings.NewL2OutputOracle(cfg.L2OOAddr, cfg.L1Client)
cfg.L2OOAddr, cfg.L1Client,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -62,7 +75,7 @@ func NewDriver(cfg Config) (*Driver, error) { ...@@ -62,7 +75,7 @@ func NewDriver(cfg Config) (*Driver, error) {
) )
walletAddr := crypto.PubkeyToAddress(cfg.PrivKey.PublicKey) 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{ return &Driver{
cfg: cfg, cfg: cfg,
...@@ -86,9 +99,7 @@ func (d *Driver) WalletAddr() common.Address { ...@@ -86,9 +99,7 @@ func (d *Driver) WalletAddr() common.Address {
// GetBlockRange returns the start and end L2 block heights that need to be // 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 // processed. Note that the end value is *exclusive*, therefore if the returned
// values are identical nothing needs to be processed. // values are identical nothing needs to be processed.
func (d *Driver) GetBlockRange( func (d *Driver) GetBlockRange(ctx context.Context) (*big.Int, *big.Int, error) {
ctx context.Context) (*big.Int, *big.Int, error) {
name := d.cfg.Name name := d.cfg.Name
callOpts := &bind.CallOpts{ callOpts := &bind.CallOpts{
...@@ -115,12 +126,12 @@ func (d *Driver) GetBlockRange( ...@@ -115,12 +126,12 @@ func (d *Driver) GetBlockRange(
d.l.Error(name+" unable to get sync status", "err", err) d.l.Error(name+" unable to get sync status", "err", err)
return nil, nil, err return nil, nil, err
} }
latestHeader, err := d.cfg.L2Client.HeaderByNumber(ctx, new(big.Int).SetUint64(status.SafeL2.Number)) var currentBlockNumber *big.Int
if err != nil { if d.cfg.AllowNonFinalized {
d.l.Error(name+" unable to retrieve latest header", "err", err) currentBlockNumber = new(big.Int).SetUint64(status.SafeL2.Number)
return nil, nil, err } 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 we do not have the new L2 Block number
if currentBlockNumber.Cmp(nextBlockNumber) < 0 { if currentBlockNumber.Cmp(nextBlockNumber) < 0 {
...@@ -144,46 +155,36 @@ func (d *Driver) GetBlockRange( ...@@ -144,46 +155,36 @@ func (d *Driver) GetBlockRange(
// using the given nonce. // using the given nonce.
// //
// NOTE: This method SHOULD NOT publish the resulting transaction. // NOTE: This method SHOULD NOT publish the resulting transaction.
func (d *Driver) CraftTx( func (d *Driver) CraftTx(ctx context.Context, start, end, nonce *big.Int) (*types.Transaction, error) {
ctx context.Context,
start, end, nonce *big.Int,
) (*types.Transaction, error) {
name := d.cfg.Name name := d.cfg.Name
d.l.Info(name+" crafting checkpoint tx", "start", start, "end", end, d.l.Info(name+" crafting checkpoint tx", "start", start, "end", end, "nonce", nonce)
"nonce", nonce)
// Fetch the final block in the range, as this is the only L2 output we need // Fetch the final block in the range, as this is the only L2 output we need to submit.
// to submit. nextCheckpointBlock := new(big.Int).Sub(end, bigOne).Uint64()
nextCheckpointBlock := new(big.Int).Sub(end, bigOne)
l2OutputRoot, err := d.outputRootAtBlock(ctx, nextCheckpointBlock) output, err := d.cfg.RollupClient.OutputAtBlock(ctx, nextCheckpointBlock)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to fetch output at block %d: %w", nextCheckpointBlock, err)
} }
if output.Version != supportedL2OutputVersion {
numElements := new(big.Int).Sub(start, end).Uint64() return nil, fmt.Errorf("unsupported l2 output version: %s", output.Version)
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.BlockRef.Number != nextCheckpointBlock { // sanity check, e.g. in case of bad RPC caching
l2Header, err := d.cfg.L2Client.HeaderByNumber(ctx, nextCheckpointBlock) return nil, fmt.Errorf("invalid blockNumber: next blockNumber is %v, blockNumber of block is %v", nextCheckpointBlock, output.BlockRef.Number)
if err != nil {
return nil, fmt.Errorf("error resolving checkpoint block: %w", err)
} }
if l2Header.Number.Cmp(nextCheckpointBlock) != 0 { // Always propose if it's part of the Finalized L2 chain. Or if allowed, if it's part of the safe L2 chain.
return nil, fmt.Errorf("invalid blockNumber: next blockNumber is %v, blockNumber of block is %v", nextCheckpointBlock, l2Header.Number) 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( opts, err := bind.NewKeyedTransactorWithChainID(d.cfg.PrivKey, d.cfg.ChainID)
d.cfg.PrivKey, d.cfg.ChainID,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -191,18 +192,41 @@ func (d *Driver) CraftTx( ...@@ -191,18 +192,41 @@ func (d *Driver) CraftTx(
opts.Nonce = nonce opts.Nonce = nonce
opts.NoSend = true 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 // UpdateGasPrice signs an otherwise identical txn to the one provided but with
// updated gas prices sampled from the existing network conditions. // updated gas prices sampled from the existing network conditions.
// //
// NOTE: Thie method SHOULD NOT publish the resulting transaction. // NOTE: This method SHOULD NOT publish the resulting transaction.
func (d *Driver) UpdateGasPrice( func (d *Driver) UpdateGasPrice(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
ctx context.Context,
tx *types.Transaction,
) (*types.Transaction, error) {
opts, err := bind.NewKeyedTransactorWithChainID( opts, err := bind.NewKeyedTransactorWithChainID(
d.cfg.PrivKey, d.cfg.ChainID, d.cfg.PrivKey, d.cfg.ChainID,
) )
...@@ -216,26 +240,8 @@ func (d *Driver) UpdateGasPrice( ...@@ -216,26 +240,8 @@ func (d *Driver) UpdateGasPrice(
return d.rawL2ooContract.RawTransact(opts, tx.Data()) return d.rawL2ooContract.RawTransact(opts, tx.Data())
} }
// SendTransaction injects a signed transaction into the pending pool for // SendTransaction injects a signed transaction into the pending pool for execution.
// execution. func (d *Driver) SendTransaction(ctx context.Context, tx *types.Transaction) error {
func (d *Driver) SendTransaction( d.l.Info(d.cfg.Name+" sending transaction", "tx", tx.Hash())
ctx context.Context,
tx *types.Transaction,
) error {
return d.cfg.L1Client.SendTransaction(ctx, tx) 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 ( ...@@ -21,12 +21,6 @@ var (
Required: true, Required: true,
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "L1_ETH_RPC"), 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{ RollupRpcFlag = cli.StringFlag{
Name: "rollup-rpc", Name: "rollup-rpc",
Usage: "HTTP provider URL for the rollup node", Usage: "HTTP provider URL for the rollup node",
...@@ -68,6 +62,9 @@ var ( ...@@ -68,6 +62,9 @@ var (
Required: true, Required: true,
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "RESUBMISSION_TIMEOUT"), EnvVar: opservice.PrefixEnvVar(envVarPrefix, "RESUBMISSION_TIMEOUT"),
} }
/* Optional flags */
MnemonicFlag = cli.StringFlag{ MnemonicFlag = cli.StringFlag{
Name: "mnemonic", Name: "mnemonic",
Usage: "The mnemonic used to derive the wallets for either the " + Usage: "The mnemonic used to derive the wallets for either the " +
...@@ -85,11 +82,15 @@ var ( ...@@ -85,11 +82,15 @@ var (
Usage: "The private key to use with the l2output wallet. Must not be used with mnemonic.", Usage: "The private key to use with the l2output wallet. Must not be used with mnemonic.",
EnvVar: opservice.PrefixEnvVar(envVarPrefix, "PRIVATE_KEY"), 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{ var requiredFlags = []cli.Flag{
L1EthRpcFlag, L1EthRpcFlag,
L2EthRpcFlag,
RollupRpcFlag, RollupRpcFlag,
L2OOAddressFlag, L2OOAddressFlag,
PollIntervalFlag, PollIntervalFlag,
...@@ -102,6 +103,7 @@ var optionalFlags = []cli.Flag{ ...@@ -102,6 +103,7 @@ var optionalFlags = []cli.Flag{
MnemonicFlag, MnemonicFlag,
L2OutputHDPathFlag, L2OutputHDPathFlag,
PrivateKeyFlag, PrivateKeyFlag,
AllowNonFinalizedFlag,
} }
func init() { func init() {
......
...@@ -12,14 +12,6 @@ import ( ...@@ -12,14 +12,6 @@ import (
"syscall" "syscall"
"time" "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/accounts"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
...@@ -28,6 +20,15 @@ import ( ...@@ -28,6 +20,15 @@ import (
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
hdwallet "github.com/miguelmota/go-ethereum-hdwallet" hdwallet "github.com/miguelmota/go-ethereum-hdwallet"
"github.com/urfave/cli" "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 ( const (
...@@ -172,11 +173,6 @@ func NewL2OutputSubmitter( ...@@ -172,11 +173,6 @@ func NewL2OutputSubmitter(
return nil, err return nil, err
} }
l2Client, err := dialEthClientWithTimeout(ctx, cfg.L2EthRpc)
if err != nil {
return nil, err
}
rollupClient, err := dialRollupClientWithTimeout(ctx, cfg.RollupRpc) rollupClient, err := dialRollupClientWithTimeout(ctx, cfg.RollupRpc)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -197,14 +193,14 @@ func NewL2OutputSubmitter( ...@@ -197,14 +193,14 @@ func NewL2OutputSubmitter(
} }
l2OutputDriver, err := l2output.NewDriver(l2output.Config{ l2OutputDriver, err := l2output.NewDriver(l2output.Config{
Log: l, Log: l,
Name: "L2Output Submitter", Name: "L2Output Submitter",
L1Client: l1Client, L1Client: l1Client,
L2Client: l2Client, RollupClient: rollupClient,
RollupClient: rollupClient, AllowNonFinalized: cfg.AllowNonFinalized,
L2OOAddr: l2ooAddress, L2OOAddr: l2ooAddress,
ChainID: chainID, ChainID: chainID,
PrivKey: l2OutputPrivKey, PrivKey: l2OutputPrivKey,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
......
...@@ -89,7 +89,6 @@ services: ...@@ -89,7 +89,6 @@ services:
- "7302:7300" - "7302:7300"
environment: environment:
OP_PROPOSER_L1_ETH_RPC: http://l1:8545 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_ROLLUP_RPC: http://op-node:8545
OP_PROPOSER_POLL_INTERVAL: 1s OP_PROPOSER_POLL_INTERVAL: 1s
OP_PROPOSER_NUM_CONFIRMATIONS: 1 OP_PROPOSER_NUM_CONFIRMATIONS: 1
...@@ -101,6 +100,7 @@ services: ...@@ -101,6 +100,7 @@ services:
OP_PROPOSER_L2OO_ADDRESS: "${L2OO_ADDRESS}" OP_PROPOSER_L2OO_ADDRESS: "${L2OO_ADDRESS}"
OP_PROPOSER_PPROF_ENABLED: "true" OP_PROPOSER_PPROF_ENABLED: "true"
OP_PROPOSER_METRICS_ENABLED: "true" OP_PROPOSER_METRICS_ENABLED: "true"
OP_PROPOSER_ALLOW_NON_FINALIZED: "true"
op-batcher: op-batcher:
depends_on: 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