From 6b988c3f0f4b75369d2e414fbad72efaa167e839 Mon Sep 17 00:00:00 2001 From: Joshua Gutow Date: Thu, 17 Mar 2022 12:57:14 -0700 Subject: [PATCH 1/2] ref impl: Flatten source interfaces This makes the following changes: 1. Flatten the L1 & L2 source into one object that fulfills several interfaces (primarily the BlockReference) 2. Place the L1Range function in the L1 Source (removed from sync package) 3. Provide FindSyncStart with the L2 block to use --- opnode/l1/source.go | 91 +++++++++++++++++---- opnode/l2/source.go | 39 +++++++-- opnode/node/node.go | 3 +- opnode/rollup/derive/payload_attributes.go | 8 +- opnode/rollup/driver/driver.go | 64 ++++++++------- opnode/rollup/driver/fake_chain.go | 25 +++++- opnode/rollup/driver/state.go | 38 ++++----- opnode/rollup/driver/state_test.go | 2 +- opnode/rollup/driver/step.go | 22 +---- opnode/rollup/sync/reference.go | 92 --------------------- opnode/rollup/sync/start.go | 94 +++++----------------- opnode/rollup/sync/start_test.go | 30 ++++++- 12 files changed, 237 insertions(+), 271 deletions(-) delete mode 100644 opnode/rollup/sync/reference.go diff --git a/opnode/l1/source.go b/opnode/l1/source.go index 2f6b1eba..80c0ad0b 100644 --- a/opnode/l1/source.go +++ b/opnode/l1/source.go @@ -2,6 +2,7 @@ package l1 import ( "context" + "errors" "fmt" "math/big" @@ -13,6 +14,11 @@ import ( "github.com/ethereum/go-ethereum/ethclient" ) +var WrongChainErr = errors.New("wrong chain") +var TooDeepReorgErr = errors.New("reorg is too deep") +var MaxReorgDepth = 500 +var MaxBlocksInL1Range = uint64(100) + type Source struct { client *ethclient.Client downloader *Downloader @@ -25,20 +31,6 @@ func NewSource(client *ethclient.Client) Source { } } -func (s Source) BlockLinkByNumber(ctx context.Context, num uint64) (self eth.BlockID, parent eth.BlockID, err error) { - header, err := s.client.HeaderByNumber(ctx, big.NewInt(int64(num))) - if err != nil { - // w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain. - return eth.BlockID{}, eth.BlockID{}, fmt.Errorf("failed to determine block-hash of height %d, could not get header: %w", num, err) - } - parentNum := num - if parentNum > 0 { - parentNum -= 1 - } - return eth.BlockID{Hash: header.Hash(), Number: num}, eth.BlockID{Hash: header.ParentHash, Number: parentNum}, nil - -} - func (s Source) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) { return s.client.SubscribeNewHead(ctx, ch) } @@ -74,6 +66,7 @@ func (s Source) Close() { func (s Source) FetchL1Info(ctx context.Context, id eth.BlockID) (derive.L1Info, error) { return s.client.BlockByHash(ctx, id.Hash) } + func (s Source) FetchReceipts(ctx context.Context, id eth.BlockID) ([]*types.Receipt, error) { _, receipts, err := s.Fetch(ctx, id) return receipts, err @@ -91,3 +84,73 @@ func (s Source) FetchTransactions(ctx context.Context, window []eth.BlockID) ([] return txns, nil } +func (s Source) L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) { + return s.l1BlockRefByNumber(ctx, nil) +} + +func (s Source) L1BlockRefByNumber(ctx context.Context, l1Num uint64) (eth.L1BlockRef, error) { + return s.l1BlockRefByNumber(ctx, new(big.Int).SetUint64(l1Num)) +} + +// l1BlockRefByNumber wraps l1.HeaderByNumber to return an eth.L1BlockRef +// This is internal because the exposed L1BlockRefByNumber takes uint64 instead of big.Ints +func (s Source) l1BlockRefByNumber(ctx context.Context, number *big.Int) (eth.L1BlockRef, error) { + header, err := s.client.HeaderByNumber(ctx, number) + if err != nil { + // w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain. + return eth.L1BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", number, err) + } + l1Num := header.Number.Uint64() + parentNum := l1Num + if parentNum > 0 { + parentNum -= 1 + } + return eth.L1BlockRef{ + Self: eth.BlockID{Hash: header.Hash(), Number: l1Num}, + Parent: eth.BlockID{Hash: header.ParentHash, Number: parentNum}, + }, nil +} + +// L1Range returns a range of L1 block beginning just after `begin`. +func (s Source) L1Range(ctx context.Context, begin eth.BlockID) ([]eth.BlockID, error) { + // Ensure that we start on the expected chain. + if canonicalBegin, err := s.L1BlockRefByNumber(ctx, begin.Number); err != nil { + return nil, fmt.Errorf("failed to fetch L1 block %v %v: %w", begin.Number, begin.Hash, err) + } else { + if canonicalBegin.Self != begin { + return nil, fmt.Errorf("Re-org at begin block. Expected: %v. Actual: %v", begin, canonicalBegin.Self) + } + } + + l1head, err := s.L1HeadBlockRef(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch head L1 block: %w", err) + } + maxBlocks := MaxBlocksInL1Range + // Cap maxBlocks if there are less than maxBlocks between `begin` and the head of the chain. + if l1head.Self.Number-begin.Number <= maxBlocks { + maxBlocks = l1head.Self.Number - begin.Number + } + + if maxBlocks == 0 { + return nil, nil + } + + prevHash := begin.Hash + var res []eth.BlockID + // TODO: Walk backwards to be able to use block by hash + for i := begin.Number + 1; i < begin.Number+maxBlocks+1; i++ { + n, err := s.L1BlockRefByNumber(ctx, i) + if err != nil { + return nil, fmt.Errorf("failed to fetch L1 block %v: %w", i, err) + } + // TODO(Joshua): Look into why this fails around the genesis block + if n.Parent.Number != 0 && n.Parent.Hash != prevHash { + return nil, errors.New("re-organization occurred while attempting to get l1 range") + } + prevHash = n.Self.Hash + res = append(res, n.Self) + } + + return res, nil +} diff --git a/opnode/l2/source.go b/opnode/l2/source.go index 74d820a1..c7b8f14c 100644 --- a/opnode/l2/source.go +++ b/opnode/l2/source.go @@ -6,6 +6,9 @@ import ( "math/big" "time" + "github.com/ethereum-optimism/optimistic-specs/opnode/eth" + "github.com/ethereum-optimism/optimistic-specs/opnode/rollup" + "github.com/ethereum-optimism/optimistic-specs/opnode/rollup/derive" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" @@ -14,16 +17,18 @@ import ( ) type Source struct { - rpc *rpc.Client // raw RPC client. Used for the consensus namespace - client *ethclient.Client // go-ethereum's wrapper around the rpc client for the eth namespace - log log.Logger + rpc *rpc.Client // raw RPC client. Used for the consensus namespace + client *ethclient.Client // go-ethereum's wrapper around the rpc client for the eth namespace + genesis *rollup.Genesis + log log.Logger } -func NewSource(l2Node *rpc.Client, log log.Logger) (*Source, error) { +func NewSource(ll2Node *rpc.Client, genesis *rollup.Genesis, log log.Logger) (*Source, error) { return &Source{ - rpc: l2Node, - client: ethclient.NewClient(l2Node), - log: log, + rpc: ll2Node, + client: ethclient.NewClient(ll2Node), + genesis: genesis, + log: log, }, nil } @@ -123,3 +128,23 @@ func (s *Source) GetPayload(ctx context.Context, payloadId PayloadID) (*Executio e.Debug("Received payload") return &result, nil } + +// L2BlockRefByNumber returns the canonical block and parent ids. +func (s *Source) L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error) { + block, err := s.client.BlockByNumber(ctx, l2Num) + if err != nil { + // w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain. + return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", l2Num, err) + } + return derive.BlockReferences(block, s.genesis) +} + +// L2BlockRefByHash returns the block & parent ids based on the supplied hash. The returned BlockRef may not be in the canonical chain +func (s *Source) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) { + block, err := s.client.BlockByHash(ctx, l2Hash) + if err != nil { + // w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain. + return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", l2Hash, err) + } + return derive.BlockReferences(block, s.genesis) +} diff --git a/opnode/node/node.go b/opnode/node/node.go index 4c3a3723..675f2dc8 100644 --- a/opnode/node/node.go +++ b/opnode/node/node.go @@ -61,6 +61,7 @@ func New(ctx context.Context, cfg *Config, log log.Logger) (*OpNode, error) { // l1Node.SetHeader() l1Source := l1.NewSource(ethclient.NewClient(l1Node)) var l2Engines []*driver.Driver + genesis := cfg.Rollup.Genesis for i, addr := range cfg.L2EngineAddrs { l2Node, err := dialRPCClientWithBackoff(ctx, log, addr) @@ -69,7 +70,7 @@ func New(ctx context.Context, cfg *Config, log log.Logger) (*OpNode, error) { } // TODO: we may need to authenticate the connection with L2 // backend.SetHeader() - client, err := l2.NewSource(l2Node, log.New("engine_client", i)) + client, err := l2.NewSource(l2Node, &genesis, log.New("engine_client", i)) if err != nil { return nil, err } diff --git a/opnode/rollup/derive/payload_attributes.go b/opnode/rollup/derive/payload_attributes.go index 82273bae..a2f6bf67 100644 --- a/opnode/rollup/derive/payload_attributes.go +++ b/opnode/rollup/derive/payload_attributes.go @@ -8,8 +8,8 @@ import ( "github.com/ethereum-optimism/optimistic-specs/opnode/rollup" - "github.com/ethereum-optimism/optimistic-specs/opnode/l2" "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/crypto" "github.com/ethereum/go-ethereum/trie" @@ -270,7 +270,7 @@ func SortedAndPreparedBatches(batches []*BatchData, epoch, blockTime, minL2Time, return out } -func L1InfoDepositBytes(l1Info L1Info) (l2.Data, error) { +func L1InfoDepositBytes(l1Info L1Info) (hexutil.Bytes, error) { l1Tx := types.NewTx(L1InfoDeposit(l1Info)) opaqueL1Tx, err := l1Tx.MarshalBinary() if err != nil { @@ -279,12 +279,12 @@ func L1InfoDepositBytes(l1Info L1Info) (l2.Data, error) { return opaqueL1Tx, nil } -func DeriveDeposits(epoch uint64, receipts []*types.Receipt) ([]l2.Data, error) { +func DeriveDeposits(epoch uint64, receipts []*types.Receipt) ([]hexutil.Bytes, error) { userDeposits, err := UserDeposits(epoch, receipts) if err != nil { return nil, fmt.Errorf("failed to derive user deposits: %v", err) } - encodedTxs := make([]l2.Data, 0, len(userDeposits)) + encodedTxs := make([]hexutil.Bytes, 0, len(userDeposits)) for i, tx := range userDeposits { opaqueTx, err := types.NewTx(tx).MarshalBinary() if err != nil { diff --git a/opnode/rollup/driver/driver.go b/opnode/rollup/driver/driver.go index 474d31e3..41d1b8bb 100644 --- a/opnode/rollup/driver/driver.go +++ b/opnode/rollup/driver/driver.go @@ -2,14 +2,15 @@ package driver import ( "context" + "math/big" "github.com/ethereum-optimism/optimistic-specs/opnode/eth" "github.com/ethereum-optimism/optimistic-specs/opnode/l1" "github.com/ethereum-optimism/optimistic-specs/opnode/l2" "github.com/ethereum-optimism/optimistic-specs/opnode/rollup" "github.com/ethereum-optimism/optimistic-specs/opnode/rollup/derive" - "github.com/ethereum-optimism/optimistic-specs/opnode/rollup/sync" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" ) @@ -21,15 +22,44 @@ type BatchSubmitter interface { Submit(config *rollup.Config, batches []*derive.BatchData) (common.Hash, error) } +type Downloader interface { + // FetchL1Info fetches the L1 header information corresponding to a L1 block ID + FetchL1Info(ctx context.Context, id eth.BlockID) (derive.L1Info, error) + // FetchReceipts of a L1 block + FetchReceipts(ctx context.Context, id eth.BlockID) ([]*types.Receipt, error) + // FetchTransactions from the given window of L1 blocks + FetchTransactions(ctx context.Context, window []eth.BlockID) ([]*types.Transaction, error) +} + +type BlockPreparer interface { + GetPayload(ctx context.Context, payloadId l2.PayloadID) (*l2.ExecutionPayload, error) + ForkchoiceUpdate(ctx context.Context, state *l2.ForkchoiceState, attr *l2.PayloadAttributes) (*l2.ForkchoiceUpdatedResult, error) + ExecutePayload(ctx context.Context, payload *l2.ExecutionPayload) error + BlockByHash(context.Context, common.Hash) (*types.Block, error) +} + +type L1Chain interface { + L1BlockRefByNumber(ctx context.Context, l1Num uint64) (eth.L1BlockRef, error) + L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) + L1Range(ctx context.Context, base eth.BlockID) ([]eth.BlockID, error) +} + +// TODO: Extend L2 Interface to get safe/unsafe blocks (specifically for Unsafe L2 head) +type L2Chain interface { + L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error) + L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) +} + +type outputInterface interface { + step(ctx context.Context, l2Head eth.BlockID, l2Finalized eth.BlockID, unsafeL2Head eth.BlockID, l1Input []eth.BlockID) (eth.BlockID, error) + newBlock(ctx context.Context, l2Finalized eth.BlockID, l2Parent eth.BlockID, l2Safe eth.BlockID, l1Origin eth.BlockID, includeDeposits bool) (eth.BlockID, *derive.BatchData, error) +} + func NewDriver(cfg rollup.Config, l2 *l2.Source, l1 *l1.Source, log log.Logger, submitter BatchSubmitter, sequencer bool) *Driver { if sequencer && submitter == nil { log.Error("Bad configuration") // TODO: return error } - input := &inputImpl{ - chainSource: sync.NewChainSource(l1, l2, &cfg.Genesis), - genesis: &cfg.Genesis, - } output := &outputImpl{ Config: cfg, dl: l1, @@ -37,7 +67,7 @@ func NewDriver(cfg rollup.Config, l2 *l2.Source, l1 *l1.Source, log log.Logger, log: log, } return &Driver{ - s: NewState(log, cfg, input, output, submitter, sequencer), + s: NewState(log, cfg, l1, l2, output, submitter, sequencer), } } @@ -47,25 +77,3 @@ func (d *Driver) Start(ctx context.Context, l1Heads <-chan eth.L1BlockRef) error func (d *Driver) Close() error { return d.s.Close() } - -type inputImpl struct { - chainSource sync.ChainSource - genesis *rollup.Genesis -} - -func (i *inputImpl) L1Head(ctx context.Context) (eth.L1BlockRef, error) { - return i.chainSource.L1HeadBlockRef(ctx) -} - -func (i *inputImpl) L2Head(ctx context.Context) (eth.L2BlockRef, error) { - return i.chainSource.L2BlockRefByNumber(ctx, nil) - -} - -func (i *inputImpl) L1ChainWindow(ctx context.Context, base eth.BlockID) ([]eth.BlockID, error) { - return sync.FindL1Range(ctx, i.chainSource, base) -} - -func (i *inputImpl) SafeL2Head(ctx context.Context) (eth.L2BlockRef, error) { - return sync.FindSafeL2Head(ctx, i.chainSource, i.genesis) -} diff --git a/opnode/rollup/driver/fake_chain.go b/opnode/rollup/driver/fake_chain.go index 5b547539..f9598515 100644 --- a/opnode/rollup/driver/fake_chain.go +++ b/opnode/rollup/driver/fake_chain.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum-optimism/optimistic-specs/opnode/eth" "github.com/ethereum-optimism/optimistic-specs/opnode/rollup" - "github.com/ethereum-optimism/optimistic-specs/opnode/rollup/sync" ) func fakeGenesis(l1 rune, l2 rune, l2offset int) rollup.Genesis { @@ -89,6 +88,27 @@ type fakeChainSource struct { log log.Logger } +func (m *fakeChainSource) L1Range(ctx context.Context, base eth.BlockID) ([]eth.BlockID, error) { + var out []eth.BlockID + found := false + for i, b := range m.l1s[m.l1reorg] { + if found { + out = append(out, b.Self) + } + if b.Self == base { + found = true + } + if i == m.l1head { + if found { + return out, nil + } else { + return nil, ethereum.NotFound + } + } + } + return nil, ethereum.NotFound +} + func (m *fakeChainSource) L1BlockRefByNumber(ctx context.Context, l1Num uint64) (eth.L1BlockRef, error) { m.log.Trace("L1BlockRefByNumber", "l1Num", l1Num, "l1Head", m.l1head, "reorg", m.l1reorg) if l1Num > uint64(m.l1head) { @@ -131,7 +151,8 @@ func (m *fakeChainSource) L2BlockRefByHash(ctx context.Context, l2Hash common.Ha return eth.L2BlockRef{}, ethereum.NotFound } -var _ sync.ChainSource = (*fakeChainSource)(nil) +var _ L1Chain = (*fakeChainSource)(nil) +var _ L2Chain = (*fakeChainSource)(nil) func (m *fakeChainSource) reorgL1() { m.log.Trace("Reorg L1", "new_reorg", m.l1reorg+1, "old_reorg", m.l1reorg) diff --git a/opnode/rollup/driver/state.go b/opnode/rollup/driver/state.go index f8627721..74cd5806 100644 --- a/opnode/rollup/driver/state.go +++ b/opnode/rollup/driver/state.go @@ -7,24 +7,10 @@ import ( "github.com/ethereum-optimism/optimistic-specs/opnode/eth" "github.com/ethereum-optimism/optimistic-specs/opnode/rollup" "github.com/ethereum-optimism/optimistic-specs/opnode/rollup/derive" + "github.com/ethereum-optimism/optimistic-specs/opnode/rollup/sync" "github.com/ethereum/go-ethereum/log" ) -// TODO: Extend L2 Inteface to get safe/unsafe blocks (specifically for Unsafe L2 head) - -type inputInterface interface { - L1Head(ctx context.Context) (eth.L1BlockRef, error) - L2Head(ctx context.Context) (eth.L2BlockRef, error) - L1ChainWindow(ctx context.Context, base eth.BlockID) ([]eth.BlockID, error) - // SafeL2Head is the L2 Head found via the sync algorithm - SafeL2Head(ctx context.Context) (eth.L2BlockRef, error) -} - -type outputInterface interface { - step(ctx context.Context, l2Head eth.BlockID, l2Finalized eth.BlockID, unsafeL2Head eth.BlockID, l1Input []eth.BlockID) (eth.BlockID, error) - newBlock(ctx context.Context, l2Finalized eth.BlockID, l2Parent eth.BlockID, l2Safe eth.BlockID, l1Origin eth.BlockID, includeDeposits bool) (eth.BlockID, *derive.BatchData, error) -} - type state struct { // Chain State l1Head eth.BlockID // Latest recorded head of the L1 Chain @@ -41,7 +27,8 @@ type state struct { // Connections (in/out) l1Heads <-chan eth.L1BlockRef - input inputInterface + l1 L1Chain + l2 L2Chain output outputInterface bss BatchSubmitter @@ -49,12 +36,13 @@ type state struct { done chan struct{} } -func NewState(log log.Logger, config rollup.Config, input inputInterface, output outputInterface, submitter BatchSubmitter, sequencer bool) *state { +func NewState(log log.Logger, config rollup.Config, l1 L1Chain, l2 L2Chain, output outputInterface, submitter BatchSubmitter, sequencer bool) *state { return &state{ Config: config, done: make(chan struct{}), log: log, - input: input, + l1: l1, + l2: l2, output: output, bss: submitter, sequencer: sequencer, @@ -62,11 +50,11 @@ func NewState(log log.Logger, config rollup.Config, input inputInterface, output } func (s *state) Start(ctx context.Context, l1Heads <-chan eth.L1BlockRef) error { - l1Head, err := s.input.L1Head(ctx) + l1Head, err := s.l1.L1HeadBlockRef(ctx) if err != nil { return err } - l2Head, err := s.input.L2Head(ctx) + l2Head, err := s.l2.L2BlockRefByNumber(ctx, nil) if err != nil { return err } @@ -101,7 +89,7 @@ func (s *state) l1WindowEnd() eth.BlockID { // It starts just after `s.l1WindowEnd()`. func (s *state) extendL1Window(ctx context.Context) error { s.log.Trace("Extending the cached window from L1", "cached_size", len(s.l1Window), "window_end", s.l1WindowEnd()) - nexts, err := s.input.L1ChainWindow(ctx, s.l1WindowEnd()) + nexts, err := s.l1.L1Range(ctx, s.l1WindowEnd()) if err != nil { return err } @@ -195,7 +183,13 @@ func (s *state) loop() { } } else { s.log.Warn("L1 Head signal indicates an L1 re-org", "old_l1_head", s.l1Head, "new_l1_head_parent", newL1Head.Parent, "new_l1_head", newL1Head.Self) - nextL2Head, err := s.input.SafeL2Head(ctx) + // TODO(Joshua): Fix having to make this call when being careful about the exact state + l2Head, err := s.l2.L2BlockRefByNumber(context.Background(), nil) + if err != nil { + s.log.Error("Could not get fetch L2 head when trying to handle a re-org", "err", err) + continue + } + nextL2Head, err := sync.FindSafeL2Head(ctx, l2Head.Self, s.l1, s.l2, &s.Config.Genesis) if err != nil { s.log.Error("Could not get new safe L2 head when trying to handle a re-org", "err", err) continue diff --git a/opnode/rollup/driver/state_test.go b/opnode/rollup/driver/state_test.go index 80caa84f..112df039 100644 --- a/opnode/rollup/driver/state_test.go +++ b/opnode/rollup/driver/state_test.go @@ -140,7 +140,7 @@ func (tc *stateTestCase) Run(t *testing.T) { return r.l2Head, r.err } config := rollup.Config{SeqWindowSize: uint64(tc.seqWindow), Genesis: tc.genesis, BlockTime: 2} - state := NewState(log, config, &inputImpl{chainSource: chainSource, genesis: &tc.genesis}, outputHandlerFn(outputHandler), nil, false) + state := NewState(log, config, chainSource, chainSource, outputHandlerFn(outputHandler), nil, false) defer func() { assert.NoError(t, state.Close(), "Error closing state") }() diff --git a/opnode/rollup/driver/step.go b/opnode/rollup/driver/step.go index acfda99d..554c3a5b 100644 --- a/opnode/rollup/driver/step.go +++ b/opnode/rollup/driver/step.go @@ -11,32 +11,14 @@ import ( "github.com/ethereum-optimism/optimistic-specs/opnode/rollup" "github.com/ethereum-optimism/optimistic-specs/opnode/rollup/derive" - "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" ) -type Downloader interface { - // FetchL1Info fetches the L1 header information corresponding to a L1 block ID - FetchL1Info(ctx context.Context, id eth.BlockID) (derive.L1Info, error) - // FetchReceipts of a L1 block - FetchReceipts(ctx context.Context, id eth.BlockID) ([]*types.Receipt, error) - // FetchTransactions from the given window of L1 blocks - FetchTransactions(ctx context.Context, window []eth.BlockID) ([]*types.Transaction, error) -} - -// L2 is block preparer + BlockByHash -type L2Client interface { - GetPayload(ctx context.Context, payloadId l2.PayloadID) (*l2.ExecutionPayload, error) - ForkchoiceUpdate(ctx context.Context, state *l2.ForkchoiceState, attr *l2.PayloadAttributes) (*l2.ForkchoiceUpdatedResult, error) - ExecutePayload(ctx context.Context, payload *l2.ExecutionPayload) error - BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) -} - type outputImpl struct { dl Downloader - l2 L2Client + l2 BlockPreparer log log.Logger Config rollup.Config } @@ -194,7 +176,7 @@ func (d *outputImpl) step(ctx context.Context, l2Head eth.BlockID, l2Finalized e return last, fmt.Errorf("failed to extend L2 chain at block %d/%d of epoch %d: %w", i, len(batches), epoch, err) } last = payload.ID() - fc.HeadBlockHash = last.Hash + fc.HeadBlockHash = last.Hash // should be safe block, but geth is broken } return last, nil diff --git a/opnode/rollup/sync/reference.go b/opnode/rollup/sync/reference.go deleted file mode 100644 index 305258d7..00000000 --- a/opnode/rollup/sync/reference.go +++ /dev/null @@ -1,92 +0,0 @@ -package sync - -import ( - "context" - "fmt" - "math/big" - - "github.com/ethereum-optimism/optimistic-specs/opnode/eth" - "github.com/ethereum-optimism/optimistic-specs/opnode/rollup" - "github.com/ethereum-optimism/optimistic-specs/opnode/rollup/derive" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" -) - -// L1Client is the subset of methods that ChainSource needs to determine the L1 block graph -type L1Client interface { - HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) -} - -// L2Client is the subset of methods that ChainSource needs to determine the L2 block graph -type L2Client interface { - BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) - BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) -} - -// ChainSource provides access to the L1 and L2 block graph -type ChainSource interface { - L1BlockRefByNumber(ctx context.Context, l1Num uint64) (eth.L1BlockRef, error) - L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) - L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error) - L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) -} - -func NewChainSource(l1 L1Client, l2 L2Client, genesis *rollup.Genesis) *chainSourceImpl { - return &chainSourceImpl{l1: l1, l2: l2, genesis: genesis} -} - -type chainSourceImpl struct { - l1 L1Client - l2 L2Client - genesis *rollup.Genesis -} - -// L1BlockRefByNumber returns the canonical block and parent ids. -func (src chainSourceImpl) L1BlockRefByNumber(ctx context.Context, l1Num uint64) (eth.L1BlockRef, error) { - return src.l1BlockRefByNumber(ctx, new(big.Int).SetUint64(l1Num)) -} - -// L1BlockRefByNumber returns the canonical head block and parent ids. -func (src chainSourceImpl) L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) { - return src.l1BlockRefByNumber(ctx, nil) -} - -// l1BlockRefByNumber wraps l1.HeaderByNumber to return an eth.L1BlockRef -// This is internal because the exposed L1BlockRefByNumber takes uint64 instead of big.Ints -func (src chainSourceImpl) l1BlockRefByNumber(ctx context.Context, number *big.Int) (eth.L1BlockRef, error) { - header, err := src.l1.HeaderByNumber(ctx, number) - if err != nil { - // w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain. - return eth.L1BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", number, err) - } - l1Num := header.Number.Uint64() - parentNum := l1Num - if parentNum > 0 { - parentNum -= 1 - } - return eth.L1BlockRef{ - Self: eth.BlockID{Hash: header.Hash(), Number: l1Num}, - Parent: eth.BlockID{Hash: header.ParentHash, Number: parentNum}, - }, nil -} - -// L2BlockRefByNumber returns the canonical block and parent ids. -func (src chainSourceImpl) L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error) { - block, err := src.l2.BlockByNumber(ctx, l2Num) - if err != nil { - // w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain. - return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", l2Num, err) - } - return derive.BlockReferences(block, src.genesis) -} - -// L2BlockRefByHash returns the block & parent ids based on the supplied hash. The returned BlockRef may not be in the canonical chain -func (src chainSourceImpl) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) { - block, err := src.l2.BlockByHash(ctx, l2Hash) - if err != nil { - // w%: wrap the error, we still need to detect if a canonical block is not found, a.k.a. end of chain. - return eth.L2BlockRef{}, fmt.Errorf("failed to determine block-hash of height %v, could not get header: %w", l2Hash, err) - } - return derive.BlockReferences(block, src.genesis) -} diff --git a/opnode/rollup/sync/start.go b/opnode/rollup/sync/start.go index 7efcb3cd..386a810c 100644 --- a/opnode/rollup/sync/start.go +++ b/opnode/rollup/sync/start.go @@ -35,56 +35,41 @@ import ( "context" "errors" "fmt" + "math/big" "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum-optimism/optimistic-specs/opnode/eth" "github.com/ethereum-optimism/optimistic-specs/opnode/rollup" ) +type L1Chain interface { + L1BlockRefByNumber(ctx context.Context, l1Num uint64) (eth.L1BlockRef, error) + L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) +} + +type L2Chain interface { + L2BlockRefByNumber(ctx context.Context, l2Num *big.Int) (eth.L2BlockRef, error) + L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) +} + var WrongChainErr = errors.New("wrong chain") var TooDeepReorgErr = errors.New("reorg is too deep") var MaxReorgDepth = 500 -var MaxBlocksInL1Range = uint64(100) - -// FindSyncStart finds the L2 head and the chain of L1 blocks after the L1 base block. -// Note: The ChainSource should memoize calls as the L1 and L2 chains will be walked multiple times. -// The L2 Head is the highest possible l2block such that it is valid (see above rules). -// It also returns a portion of the L1 chain starting just after l2block.l1parent.number. -// - The returned L1 blocks were canonical when the function was called. -// - The returned L1 block are contiguous and ordered from low to high. -// - The first block (if len > 0) has height l2block.l1parent.number + 1. -// - The length of the array may be any value, including 0. -// If err is not nil, the above return values are not well defined. An error will be returned in the following cases: -// - Wrapped ethereum.NotFound if it could not find a block in L1 or L2. This error may be temporary. -// - Wrapped WrongChainErr if the l1_rollup_genesis block is not reachable from the L2 chain. -func FindSyncStart(ctx context.Context, source ChainSource, genesis *rollup.Genesis) ([]eth.BlockID, eth.BlockID, error) { - l2Head, err := FindSafeL2Head(ctx, source, genesis) - if err != nil { - return nil, eth.BlockID{}, err - } - l1blocks, err := FindL1Range(ctx, source, l2Head.L1Origin) - if err != nil { - return nil, eth.BlockID{}, fmt.Errorf("failed to fetch l1 range: %w", err) - } - return l1blocks, l2Head.Self, nil - -} - -// FindSafeL2Head takes the current L2 Head and then finds the topmost L2 head that is valid -// In the case that there are no re-orgs, this is just the L2 head. Otherwise it has to walk back -// until it finds the first L2 block that is based on a canonical L1 block. -func FindSafeL2Head(ctx context.Context, source ChainSource, genesis *rollup.Genesis) (eth.L2BlockRef, error) { +// FindSafeL2Head takes the supplied L2 start block and walks the L2 chain until it finds the first L2 block reachable from the supplied +// block that is also canonical. +func FindSafeL2Head(ctx context.Context, start eth.BlockID, l1 L1Chain, l2 L2Chain, genesis *rollup.Genesis) (eth.L2BlockRef, error) { // Starting point - l2Head, err := source.L2BlockRefByNumber(ctx, nil) + l2Head, err := l2.L2BlockRefByHash(ctx, start.Hash) if err != nil { return eth.L2BlockRef{}, fmt.Errorf("failed to fetch L2 head: %w", err) } reorgDepth := 0 // Walk L2 chain from L2 head to first L2 block which has a L1 Parent that is canonical. May walk to L2 genesis for n := l2Head; ; { - l1header, err := source.L1BlockRefByNumber(ctx, n.L1Origin.Number) + l1header, err := l1.L1BlockRefByNumber(ctx, n.L1Origin.Number) if err != nil { // Generic error, bail out. if !errors.Is(err, ethereum.NotFound) { @@ -105,7 +90,7 @@ func FindSafeL2Head(ctx context.Context, source ChainSource, genesis *rollup.Gen } // Pull L2 parent for next iteration - n, err = source.L2BlockRefByHash(ctx, n.Parent.Hash) + n, err = l2.L2BlockRefByHash(ctx, n.Parent.Hash) if err != nil { return eth.L2BlockRef{}, fmt.Errorf("failed to fetch L2 block by hash %v: %w", n.Parent.Hash, err) } @@ -115,46 +100,3 @@ func FindSafeL2Head(ctx context.Context, source ChainSource, genesis *rollup.Gen } } } - -// FindL1Range returns a range of L1 block beginning just after `begin`. -func FindL1Range(ctx context.Context, source ChainSource, begin eth.BlockID) ([]eth.BlockID, error) { - // Ensure that we start on the expected chain. - if canonicalBegin, err := source.L1BlockRefByNumber(ctx, begin.Number); err != nil { - return nil, fmt.Errorf("failed to fetch L1 block %v %v: %w", begin.Number, begin.Hash, err) - } else { - if canonicalBegin.Self != begin { - return nil, fmt.Errorf("Re-org at begin block. Expected: %v. Actual: %v", begin, canonicalBegin.Self) - } - } - - l1head, err := source.L1HeadBlockRef(ctx) - if err != nil { - return nil, fmt.Errorf("failed to fetch head L1 block: %w", err) - } - maxBlocks := MaxBlocksInL1Range - // Cap maxBlocks if there are less than maxBlocks between `begin` and the head of the chain. - if l1head.Self.Number-begin.Number <= maxBlocks { - maxBlocks = l1head.Self.Number - begin.Number - } - - if maxBlocks == 0 { - return nil, nil - } - - prevHash := begin.Hash - var res []eth.BlockID - for i := begin.Number + 1; i < begin.Number+maxBlocks+1; i++ { - n, err := source.L1BlockRefByNumber(ctx, i) - if err != nil { - return nil, fmt.Errorf("failed to fetch L1 block %v: %w", i, err) - } - // TODO(Joshua): Look into why this fails around the genesis block - if n.Parent.Number != 0 && n.Parent.Hash != prevHash { - return nil, errors.New("re-organization occurred while attempting to get l1 range") - } - prevHash = n.Self.Hash - res = append(res, n.Self) - } - - return res, nil -} diff --git a/opnode/rollup/sync/start_test.go b/opnode/rollup/sync/start_test.go index 78c0ceb9..c6b71ce8 100644 --- a/opnode/rollup/sync/start_test.go +++ b/opnode/rollup/sync/start_test.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type fakeChainSource struct { @@ -17,6 +18,23 @@ type fakeChainSource struct { L2 []eth.L2BlockRef } +func (m *fakeChainSource) L1Range(ctx context.Context, base eth.BlockID) ([]eth.BlockID, error) { + var out []eth.BlockID + found := false + for _, b := range m.L1 { + if found { + out = append(out, b.Self) + } + if b.Self == base { + found = true + } + } + if found { + return out, nil + } + return nil, ethereum.NotFound +} + func (m *fakeChainSource) L1BlockRefByNumber(ctx context.Context, l1Num uint64) (eth.L1BlockRef, error) { if l1Num >= uint64(len(m.L1)) { return eth.L1BlockRef{}, ethereum.NotFound @@ -52,7 +70,8 @@ func (m *fakeChainSource) L2BlockRefByHash(ctx context.Context, l2Hash common.Ha return eth.L2BlockRef{}, ethereum.NotFound } -var _ ChainSource = (*fakeChainSource)(nil) +var _ L1Chain = (*fakeChainSource)(nil) +var _ L2Chain = (*fakeChainSource)(nil) func fakeID(id rune, num uint64) eth.BlockID { var h common.Hash @@ -129,14 +148,17 @@ func (c *syncStartTestCase) Run(t *testing.T) { L1: fakeID(c.GenesisL1, c.OffsetL2), L2: fakeID(c.GenesisL2, 0), } - - nextRefL1s, refL2, err := FindSyncStart(context.Background(), msr, genesis) + head, err := msr.L2BlockRefByNumber(context.Background(), nil) + require.Nil(t, err) + refL2, err := FindSafeL2Head(context.Background(), head.Self, msr, msr, genesis) if c.ExpectedErr != nil { assert.Error(t, err, "Expecting an error in this test case") assert.ErrorIs(t, err, c.ExpectedErr) } else { - expectedRefL2 := refToRune(refL2) + nextRefL1s, err := msr.L1Range(context.Background(), refL2.L1Origin) + require.Nil(t, err) + expectedRefL2 := refToRune(refL2.Self) var expectedRefsL1 []rune for _, ref := range nextRefL1s { expectedRefsL1 = append(expectedRefsL1, refToRune(ref)) From 22ccfed9b18fa6638bd4ddcdf8e8c76da6135899 Mon Sep 17 00:00:00 2001 From: Joshua Gutow Date: Mon, 21 Mar 2022 16:58:05 -0700 Subject: [PATCH 2/2] ref impl: Address PR Comments - var -> const (+ removing som extra copies) - Rename BlockPreparer interface to Engine - Remove out of date comment and update safe block --- opnode/l1/source.go | 5 +---- opnode/rollup/driver/driver.go | 2 +- opnode/rollup/driver/step.go | 6 ++++-- opnode/rollup/sync/start.go | 3 ++- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/opnode/l1/source.go b/opnode/l1/source.go index 80c0ad0b..03c31018 100644 --- a/opnode/l1/source.go +++ b/opnode/l1/source.go @@ -14,10 +14,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" ) -var WrongChainErr = errors.New("wrong chain") -var TooDeepReorgErr = errors.New("reorg is too deep") -var MaxReorgDepth = 500 -var MaxBlocksInL1Range = uint64(100) +const MaxBlocksInL1Range = uint64(100) type Source struct { client *ethclient.Client diff --git a/opnode/rollup/driver/driver.go b/opnode/rollup/driver/driver.go index 41d1b8bb..d515ed8e 100644 --- a/opnode/rollup/driver/driver.go +++ b/opnode/rollup/driver/driver.go @@ -31,7 +31,7 @@ type Downloader interface { FetchTransactions(ctx context.Context, window []eth.BlockID) ([]*types.Transaction, error) } -type BlockPreparer interface { +type Engine interface { GetPayload(ctx context.Context, payloadId l2.PayloadID) (*l2.ExecutionPayload, error) ForkchoiceUpdate(ctx context.Context, state *l2.ForkchoiceState, attr *l2.PayloadAttributes) (*l2.ForkchoiceUpdatedResult, error) ExecutePayload(ctx context.Context, payload *l2.ExecutionPayload) error diff --git a/opnode/rollup/driver/step.go b/opnode/rollup/driver/step.go index 554c3a5b..1afca3e3 100644 --- a/opnode/rollup/driver/step.go +++ b/opnode/rollup/driver/step.go @@ -18,7 +18,7 @@ import ( type outputImpl struct { dl Downloader - l2 BlockPreparer + l2 Engine log log.Logger Config rollup.Config } @@ -176,7 +176,9 @@ func (d *outputImpl) step(ctx context.Context, l2Head eth.BlockID, l2Finalized e return last, fmt.Errorf("failed to extend L2 chain at block %d/%d of epoch %d: %w", i, len(batches), epoch, err) } last = payload.ID() - fc.HeadBlockHash = last.Hash // should be safe block, but geth is broken + // TODO(Joshua): Update this to handle verifiers + sequencers + fc.HeadBlockHash = last.Hash + fc.SafeBlockHash = last.Hash } return last, nil diff --git a/opnode/rollup/sync/start.go b/opnode/rollup/sync/start.go index 386a810c..1c245cf9 100644 --- a/opnode/rollup/sync/start.go +++ b/opnode/rollup/sync/start.go @@ -56,7 +56,8 @@ type L2Chain interface { var WrongChainErr = errors.New("wrong chain") var TooDeepReorgErr = errors.New("reorg is too deep") -var MaxReorgDepth = 500 + +const MaxReorgDepth = 500 // FindSafeL2Head takes the supplied L2 start block and walks the L2 chain until it finds the first L2 block reachable from the supplied // block that is also canonical.