Commit aa707a47 authored by Joshua Gutow's avatar Joshua Gutow Committed by GitHub

Op-Node: Fix divergence due to bad reset logic (#3044)

* Op-Node: Restore sync package

* Op-Node: Fix reset/reorg logic

This now uses the old sync package b/c the pipeline reset logic was
not actually working.

* OP Node: Add time invariant check to reset step

* Don't skip L1 Origin in reset step

* sync: Rollback to block with sequence number 0
Co-authored-by: default avatarmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent 7e6eb9b2
...@@ -89,29 +89,14 @@ func (bq *BatchQueue) Step(ctx context.Context, outer Progress) error { ...@@ -89,29 +89,14 @@ func (bq *BatchQueue) Step(ctx context.Context, outer Progress) error {
} }
func (bq *BatchQueue) ResetStep(ctx context.Context, l1Fetcher L1Fetcher) error { func (bq *BatchQueue) ResetStep(ctx context.Context, l1Fetcher L1Fetcher) error {
// Reset such that the highestL1InclusionBlock is the same as the l2SafeHeadOrigin - the sequence window size // Copy over the Origin the from the next stage
// It is set in the engine queue (two stages away) such that the L2 Safe Head origin is the progress
bq.progress = bq.next.Progress()
bq.batchesByTimestamp = make(map[uint64][]*BatchWithL1InclusionBlock) bq.batchesByTimestamp = make(map[uint64][]*BatchWithL1InclusionBlock)
// Include the new origin as an origin to build off of.
bq.l1Blocks = bq.l1Blocks[:0] bq.l1Blocks = bq.l1Blocks[:0]
bq.l1Blocks = append(bq.l1Blocks, bq.progress.Origin)
startNumber := bq.next.Progress().Origin.Number
if startNumber < bq.config.SeqWindowSize {
startNumber = 0
} else {
startNumber -= bq.config.SeqWindowSize
}
// clip to genesis
if startNumber < bq.config.Genesis.L1.Number {
startNumber = bq.config.Genesis.L1.Number
}
l1BlockStart, err := l1Fetcher.L1BlockRefByNumber(ctx, startNumber)
if err != nil {
return err
}
bq.log.Info("found reset origin for batch queue", "origin", l1BlockStart)
bq.l1Blocks = append(bq.l1Blocks, l1BlockStart)
bq.progress.Origin = l1BlockStart
bq.progress.Closed = false
return io.EOF return io.EOF
} }
......
...@@ -2,15 +2,13 @@ package derive ...@@ -2,15 +2,13 @@ package derive
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"time" "time"
"github.com/ethereum/go-ethereum"
"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/rollup/sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -37,8 +35,6 @@ type EngineQueue struct { ...@@ -37,8 +35,6 @@ type EngineQueue struct {
safeHead eth.L2BlockRef safeHead eth.L2BlockRef
unsafeHead eth.L2BlockRef unsafeHead eth.L2BlockRef
resetting bool
toFinalize eth.BlockID toFinalize eth.BlockID
progress Progress progress Progress
...@@ -262,64 +258,32 @@ func (eq *EngineQueue) forceNextSafeAttributes(ctx context.Context) error { ...@@ -262,64 +258,32 @@ func (eq *EngineQueue) forceNextSafeAttributes(ctx context.Context) error {
// ResetStep Walks the L2 chain backwards until it finds an L2 block whose L1 origin is canonical. // ResetStep Walks the L2 chain backwards until it finds an L2 block whose L1 origin is canonical.
// The unsafe head is set to the head of the L2 chain, unless the existing safe head is not canonical. // The unsafe head is set to the head of the L2 chain, unless the existing safe head is not canonical.
func (eq *EngineQueue) ResetStep(ctx context.Context, l1Fetcher L1Fetcher) error { func (eq *EngineQueue) ResetStep(ctx context.Context, l1Fetcher L1Fetcher) error {
if !eq.resetting {
eq.resetting = true
head, err := eq.engine.L2BlockRefHead(ctx) l2Head, err := eq.engine.L2BlockRefHead(ctx)
if err != nil { if err != nil {
eq.log.Error("failed to get L2 engine head to start finding reset point from", "err", err) eq.log.Error("failed to find the L2 Head block", "err", err)
return nil return nil
} }
eq.unsafeHead = head unsafe, safe, err := sync.FindL2Heads(ctx, l2Head, eq.cfg.SeqWindowSize, l1Fetcher, eq.engine, &eq.cfg.Genesis)
if err != nil {
// TODO: this should be different for safe head. eq.log.Error("failed to find the L2 Heads to start from", "err", err)
// We can't trust the origin data of the unsafe chain.
// We should query the engine for its current safe-head.
eq.safeHead = head
return nil return nil
} }
l1Origin, err := l1Fetcher.L1BlockRefByHash(ctx, safe.L1Origin.Hash)
// check if the block origin is canonical if err != nil {
if canonicalRef, err := l1Fetcher.L1BlockRefByNumber(ctx, eq.safeHead.L1Origin.Number); errors.Is(err, ethereum.NotFound) { eq.log.Error("failed to fetch the new L1 progress", "err", err, "origin", safe.L1Origin)
// if our view of the l1 chain is lagging behind, we may get this error return nil
eq.log.Warn("engine safe head is ahead of L1 view", "block", eq.safeHead, "origin", eq.safeHead.L1Origin) }
} else if err != nil { if safe.Time < l1Origin.Time {
eq.log.Warn("failed to get L1 block ref to check if origin of l2 block is canonical", "err", err, "num", eq.safeHead.L1Origin.Number) return fmt.Errorf("cannot reset block derivation to start at L2 block %s with time %d older than its L1 origin %s with time %d, time invariant is broken",
} else { safe, safe.Time, l1Origin, l1Origin.Time)
// if we find the safe head, then we found the canon chain
if canonicalRef.Hash == eq.safeHead.L1Origin.Hash {
eq.resetting = false
// if the unsafe head was broken, then restore it to start from the safe head
if eq.unsafeHead == (eth.L2BlockRef{}) {
eq.unsafeHead = eq.safeHead
} }
eq.unsafeHead = unsafe
eq.safeHead = safe
eq.progress = Progress{ eq.progress = Progress{
Origin: canonicalRef, Origin: l1Origin,
Closed: false, Closed: false,
} }
if eq.safeHead.Time < canonicalRef.Time {
return fmt.Errorf("cannot reset block derivation to start at L2 block %s with time %d older than its L1 origin %s with time %d, time invariant is broken",
eq.safeHead, eq.safeHead.Time, canonicalRef, canonicalRef.Time)
}
return io.EOF return io.EOF
} else {
// if the safe head is not canonical, then the unsafe head will not be either
eq.unsafeHead = eth.L2BlockRef{}
}
}
// Don't walk past genesis. If we were at the L2 genesis, but could not find its L1 origin,
// the L2 chain is building on the wrong L1 branch.
if eq.safeHead.Hash == eq.cfg.Genesis.L2.Hash || eq.safeHead.Number == eq.cfg.Genesis.L2.Number {
return fmt.Errorf("the L2 engine is coupled to unrecognized L1 chain: %v", eq.cfg.Genesis)
}
// Pull L2 parent for next iteration
block, err := eq.engine.L2BlockRefByHash(ctx, eq.safeHead.ParentHash)
if err != nil {
eq.log.Error("failed to fetch L2 block by hash during reset", "parent", eq.safeHead.ParentHash, "err", err)
return nil
}
eq.safeHead = block
return nil
} }
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
) )
type L1Fetcher interface { type L1Fetcher interface {
L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error)
L1BlockRefByNumberFetcher L1BlockRefByNumberFetcher
L1BlockRefByHashFetcher L1BlockRefByHashFetcher
L1ReceiptsFetcher L1ReceiptsFetcher
......
// The sync package is responsible for reconciling L1 and L2.
//
// The Ethereum chain is a DAG of blocks with the root block being the genesis block. At any given
// time, the head (or tip) of the chain can change if an offshoot/branch of the chain has a higher
// total difficulty. This is known as a re-organization of the canonical chain. Each block points to
// a parent block and the node is responsible for deciding which block is the head and thus the
// mapping from block number to canonical block.
//
// The Optimism (L2) chain has similar properties, but also retains references to the Ethereum (L1)
// chain. Each L2 block retains a reference to an L1 block (its "L1 origin", i.e. L1 block
// associated with the epoch that the L2 block belongs to) and to its parent L2 block. The L2 chain
// node must satisfy the following validity rules:
//
// 1. l2block.number == l2block.l2parent.block.number + 1
// 2. l2block.l1Origin.number >= l2block.l2parent.l1Origin.number
// 3. l2block.l1Origin is in the canonical chain on L1
// 4. l1_rollup_genesis is an ancestor of l2block.l1Origin
//
// During normal operation, both the L1 and L2 canonical chains can change, due to a re-organisation
// or due to an extension (new L1 or L2 block).
//
// When one of these changes occurs, the rollup node needs to determine what the new L2 head blocks
// should be. We track two L2 head blocks:
//
// - The *unsafe L2 block*: This is the highest L2 block whose L1 origin is a plausible (1)
// extension of the canonical L1 chain (as known to the op-node).
// - The *safe L2 block*: This is the highest L2 block whose epoch's sequencing window is
// complete within the canonical L1 chain (as known to the op-node).
//
// (1) Plausible meaning that the blockhash of the L2 block's L1 origin (as reported in the L1
// Attributes deposit within the L2 block) is not canonical at another height in the L1 chain,
// and the same holds for all its ancestors.
//
// In particular, in the case of L1 extension, the L2 unsafe head will generally remain the same,
// but in the case of an L1 re-org, we need to search for the new safe and unsafe L2 block.
package sync
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
)
type L1Chain interface {
L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error)
L1BlockRefByNumber(ctx context.Context, number uint64) (eth.L1BlockRef, error)
}
type L2Chain interface {
L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error)
}
var WrongChainErr = errors.New("wrong chain")
var TooDeepReorgErr = errors.New("reorg is too deep")
const MaxReorgDepth = 500
// isCanonical returns the following values:
// - `aheadOrCanonical: true if the supplied block is ahead of the known head of the L1 chain,
// or canonical in the L1 chain.
// - `canonical`: true if the block is canonical in the L1 chain.
func isAheadOrCanonical(ctx context.Context, l1 L1Chain, block eth.BlockID) (aheadOrCanonical bool, canonical bool, err error) {
if l1Head, err := l1.L1HeadBlockRef(ctx); err != nil {
return false, false, err
} else if block.Number > l1Head.Number {
return true, false, nil
} else if canonical, err := l1.L1BlockRefByNumber(ctx, block.Number); err != nil {
return false, false, err
} else {
canonical := canonical.Hash == block.Hash
return canonical, canonical, nil
}
}
// FindL2Heads walks back from `start` (the previous unsafe L2 block) and finds the unsafe and safe
// L2 blocks.
//
// - The *unsafe L2 block*: This is the highest L2 block whose L1 origin is a plausible (1)
// extension of the canonical L1 chain (as known to the op-node).
// - The *safe L2 block*: This is the highest L2 block whose epoch's sequencing window is
// complete within the canonical L1 chain (as known to the op-node).
//
// (1) Plausible meaning that the blockhash of the L2 block's L1 origin (as reported in the L1
// Attributes deposit within the L2 block) is not canonical at another height in the L1 chain,
// and the same holds for all its ancestors.
func FindL2Heads(ctx context.Context, start eth.L2BlockRef, seqWindowSize uint64,
l1 L1Chain, l2 L2Chain, genesis *rollup.Genesis) (unsafe eth.L2BlockRef, safe eth.L2BlockRef, err error) {
// Loop 1. Walk the L2 chain backwards until we find an L2 block whose L1 origin is canonical.
// Current L2 block.
n := start
// Number of blocks between n and start.
reorgDepth := 0
// Blockhash of L1 origin hash for the L2 block during the previous iteration, 0 for first
// iteration. When this changes as we walk the L2 chain backwards, it means we're seeing a different
// (earlier) epoch.
var prevL1OriginHash common.Hash
// The highest L2 ancestor of `start` (or `start` itself) whose ancestors are not (yet) known
// to have a non-canonical L1 origin. Empty if no such candidate is known yet. Guaranteed to be
// set after exiting from Loop 1.
var highestPlausibleCanonicalOrigin eth.L2BlockRef
for {
// Check if l1Origin is canonical when we get to a new epoch.
if prevL1OriginHash != n.L1Origin.Hash {
prevL1OriginHash = n.L1Origin.Hash
if plausible, canonical, err := isAheadOrCanonical(ctx, l1, n.L1Origin); err != nil {
return eth.L2BlockRef{}, eth.L2BlockRef{}, err
} else if !plausible {
// L1 origin nor ahead of L1 head nor canonical, discard previous candidate and
// keep looking.
highestPlausibleCanonicalOrigin = eth.L2BlockRef{}
} else {
if highestPlausibleCanonicalOrigin == (eth.L2BlockRef{}) {
// No highest plausible candidate, make L2 block new candidate.
highestPlausibleCanonicalOrigin = n
}
if canonical {
break
}
}
}
// Don't walk past genesis. If we were at the L2 genesis, but could not find its L1 origin,
// the L2 chain is building on the wrong L1 branch.
if n.Hash == genesis.L2.Hash || n.Number == genesis.L2.Number {
return eth.L2BlockRef{}, eth.L2BlockRef{}, WrongChainErr
}
// Pull L2 parent for next iteration
n, err = l2.L2BlockRefByHash(ctx, n.ParentHash)
if err != nil {
return eth.L2BlockRef{}, eth.L2BlockRef{},
fmt.Errorf("failed to fetch L2 block by hash %v: %w", n.ParentHash, err)
}
reorgDepth++
if reorgDepth >= MaxReorgDepth {
// If the reorg depth is too large, something is fishy.
// This can legitimately happen if L1 goes down for a while. But in that case,
// restarting the L2 node with a bigger configured MaxReorgDepth is an acceptable
// stopgap solution.
// Currently this can also happen if the L2 node is down for a while, but in the future
// state sync should prevent this issue.
return eth.L2BlockRef{}, eth.L2BlockRef{}, TooDeepReorgErr
}
}
// Loop 2. Walk from the L1 origin of the `n` block (*) back to the L1 block that starts the
// sequencing window ending at that block. Instead of iterating on L1 blocks, we actually
// iterate on L2 blocks, because we want to find the safe L2 head, i.e. the highest L2 block
// whose L1 origin is the start of the sequencing window.
// (*) `n` being at this stage the highest L2 block whose L1 origin is canonical.
// Depth counter: we need to walk back `seqWindowSize` L1 blocks in order to find the start
// of the sequencing window.
depth := uint64(1)
// Before entering the loop: `prevL1OriginHash == n.L1Origin.Hash`
// The original definitions of `n` and `prevL1OriginHash` still hold.
for {
// Advance depth if we change to a different (earlier) epoch.
if n.L1Origin.Hash != prevL1OriginHash {
depth++
prevL1OriginHash = n.L1Origin.Hash
}
// Found an L2 block whose L1 origin is the start of the sequencing window.
// Note: We also ensure that we are on the block number with the 0 seq number.
// This is a little hacky, but kinda works. The issue is about where the
// batch queue should start building.
if depth == seqWindowSize && n.SequenceNumber == 0 {
return highestPlausibleCanonicalOrigin, n, nil
}
// Genesis is always safe.
if n.Hash == genesis.L2.Hash || n.Number == genesis.L2.Number {
safe = eth.L2BlockRef{Hash: genesis.L2.Hash, Number: genesis.L2.Number,
Time: genesis.L2Time, L1Origin: genesis.L1, SequenceNumber: 0}
return highestPlausibleCanonicalOrigin, safe, nil
}
// Pull L2 parent for next iteration.
n, err = l2.L2BlockRefByHash(ctx, n.ParentHash)
if err != nil {
return eth.L2BlockRef{}, eth.L2BlockRef{},
fmt.Errorf("failed to fetch L2 block by hash %v: %w", n.ParentHash, err)
}
}
}
package sync
import (
"context"
"testing"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var _ L1Chain = (*testutils.FakeChainSource)(nil)
var _ L2Chain = (*testutils.FakeChainSource)(nil)
// generateFakeL2 creates a fake L2 chain with the following conditions:
// - The L2 chain is based off of the L1 chain
// - The actual L1 chain is the New L1 chain
// - Both heads are at the tip of their respective chains
func (c *syncStartTestCase) generateFakeL2(t *testing.T) (*testutils.FakeChainSource, eth.L2BlockRef, rollup.Genesis) {
log := testlog.Logger(t, log.LvlError)
chain := testutils.NewFakeChainSource([]string{c.L1, c.NewL1}, []string{c.L2}, int(c.GenesisL1Num), log)
chain.SetL2Head(len(c.L2) - 1)
genesis := testutils.FakeGenesis(c.GenesisL1, c.GenesisL2, int(c.GenesisL1Num))
head, err := chain.L2BlockRefByNumber(context.Background(), nil)
require.Nil(t, err)
chain.ReorgL1()
for i := 0; i < len(c.NewL1)-1; i++ {
chain.AdvanceL1()
}
return chain, head, genesis
}
type syncStartTestCase struct {
Name string
L1 string // L1 Chain prior to a re-org or other change
L2 string // L2 Chain that follows from L1Chain
NewL1 string // New L1 chain
GenesisL1 rune
GenesisL1Num uint64
GenesisL2 rune
SeqWindowSize uint64
SafeL2Head rune
UnsafeL2Head rune
ExpectedErr error
}
func refToRune(r eth.BlockID) rune {
return rune(r.Hash.Bytes()[0])
}
func (c *syncStartTestCase) Run(t *testing.T) {
chain, l2Head, genesis := c.generateFakeL2(t)
unsafeL2Head, safeHead, err := FindL2Heads(context.Background(), l2Head, c.SeqWindowSize, chain, chain, &genesis)
if c.ExpectedErr != nil {
require.Error(t, err, "Expecting an error in this test case")
require.ErrorIs(t, c.ExpectedErr, err, "Unexpected error")
} else {
require.NoError(t, err)
expectedUnsafeHead := refToRune(unsafeL2Head.ID())
require.Equal(t, string(c.UnsafeL2Head), string(expectedUnsafeHead), "Unsafe L2 Head not equal")
expectedSafeHead := refToRune(safeHead.ID())
require.Equal(t, string(c.SafeL2Head), string(expectedSafeHead), "Safe L2 Head not equal")
}
}
func TestFindSyncStart(t *testing.T) {
testCases := []syncStartTestCase{
{
Name: "already synced",
GenesisL1Num: 0,
L1: "ab",
L2: "AB",
NewL1: "ab",
GenesisL1: 'a',
GenesisL2: 'A',
UnsafeL2Head: 'B',
SeqWindowSize: 2,
SafeL2Head: 'A',
ExpectedErr: nil,
},
{
Name: "small reorg long chain",
GenesisL1Num: 0,
L1: "abcdefgh",
L2: "ABCDEFGH",
NewL1: "abcdefgx",
GenesisL1: 'a',
GenesisL2: 'A',
UnsafeL2Head: 'G',
SeqWindowSize: 2,
SafeL2Head: 'F',
ExpectedErr: nil,
},
{
Name: "L1 Chain ahead",
GenesisL1Num: 0,
L1: "abcde",
L2: "ABCD",
NewL1: "abcde",
GenesisL1: 'a',
GenesisL2: 'A',
UnsafeL2Head: 'D',
SeqWindowSize: 3,
SafeL2Head: 'B',
ExpectedErr: nil,
},
{
Name: "L2 Chain ahead after reorg",
GenesisL1Num: 0,
L1: "abxyz",
L2: "ABXYZ",
NewL1: "abx",
GenesisL1: 'a',
GenesisL2: 'A',
UnsafeL2Head: 'Z',
SeqWindowSize: 2,
SafeL2Head: 'B',
ExpectedErr: nil,
},
{
Name: "genesis",
GenesisL1Num: 0,
L1: "a",
L2: "A",
NewL1: "a",
GenesisL1: 'a',
GenesisL2: 'A',
UnsafeL2Head: 'A',
SeqWindowSize: 2,
SafeL2Head: 'A',
ExpectedErr: nil,
},
{
Name: "reorg one step back",
GenesisL1Num: 0,
L1: "abcd",
L2: "ABCD",
NewL1: "abcx",
GenesisL1: 'a',
GenesisL2: 'A',
UnsafeL2Head: 'C',
SeqWindowSize: 3,
SafeL2Head: 'A',
ExpectedErr: nil,
},
{
Name: "reorg two steps back",
GenesisL1Num: 0,
L1: "abc",
L2: "ABC",
NewL1: "axy",
GenesisL1: 'a',
GenesisL2: 'A',
UnsafeL2Head: 'A',
SeqWindowSize: 2,
SafeL2Head: 'A',
ExpectedErr: nil,
},
{
Name: "reorg three steps back",
GenesisL1Num: 0,
L1: "abcdef",
L2: "ABCDEF",
NewL1: "abcxyz",
GenesisL1: 'a',
GenesisL2: 'A',
UnsafeL2Head: 'C',
SeqWindowSize: 2,
SafeL2Head: 'B',
ExpectedErr: nil,
},
{
Name: "unexpected L1 chain",
GenesisL1Num: 0,
L1: "abcdef",
L2: "ABCDEF",
NewL1: "xyzwio",
GenesisL1: 'a',
GenesisL2: 'A',
UnsafeL2Head: 0,
ExpectedErr: WrongChainErr,
},
{
Name: "unexpected L2 chain",
GenesisL1Num: 0,
L1: "abcdef",
L2: "ABCDEF",
NewL1: "xyzwio",
GenesisL1: 'a',
GenesisL2: 'X',
UnsafeL2Head: 0,
ExpectedErr: WrongChainErr,
},
{
Name: "offset L2 genesis",
GenesisL1Num: 3,
L1: "abcdef",
L2: "DEF",
NewL1: "abcdef",
GenesisL1: 'd',
GenesisL2: 'D',
UnsafeL2Head: 'F',
SeqWindowSize: 2,
SafeL2Head: 'E',
ExpectedErr: nil,
},
{
Name: "offset L2 genesis reorg",
GenesisL1Num: 3,
L1: "abcdefgh",
L2: "DEFGH",
NewL1: "abcdxyzw",
GenesisL1: 'd',
GenesisL2: 'D',
UnsafeL2Head: 'D',
SeqWindowSize: 2,
SafeL2Head: 'D',
ExpectedErr: nil,
},
{
Name: "reorg past offset genesis",
GenesisL1Num: 3,
L1: "abcdefgh",
L2: "DEFGH",
NewL1: "abxyzwio",
GenesisL1: 'd',
GenesisL2: 'D',
UnsafeL2Head: 0,
ExpectedErr: WrongChainErr,
},
}
for _, testCase := range testCases {
t.Run(testCase.Name, testCase.Run)
}
}
...@@ -22,6 +22,11 @@ func (m *MockL1Source) ExpectInfoByHash(hash common.Hash, info eth.L1Info, err e ...@@ -22,6 +22,11 @@ func (m *MockL1Source) ExpectInfoByHash(hash common.Hash, info eth.L1Info, err e
m.Mock.On("InfoByHash", hash).Once().Return(&info, &err) m.Mock.On("InfoByHash", hash).Once().Return(&info, &err)
} }
func (m *MockL1Source) L1HeadBlockRef(ctx context.Context) (eth.L1BlockRef, error) {
out := m.Mock.MethodCalled("L1HeadBlockRef")
return out[0].(eth.L1BlockRef), *out[1].(*error)
}
func (m *MockL1Source) L1BlockRefByNumber(ctx context.Context, u uint64) (eth.L1BlockRef, error) { func (m *MockL1Source) L1BlockRefByNumber(ctx context.Context, u uint64) (eth.L1BlockRef, error) {
out := m.Mock.MethodCalled("L1BlockRefByNumber", u) out := m.Mock.MethodCalled("L1BlockRefByNumber", u)
return out[0].(eth.L1BlockRef), *out[1].(*error) return out[0].(eth.L1BlockRef), *out[1].(*error)
......
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