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)
if err != nil {
eq.log.Error("failed to get L2 engine head to start finding reset point from", "err", err)
return nil
}
eq.unsafeHead = head
// TODO: this should be different for safe head.
// 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
}
// check if the block origin is canonical l2Head, err := eq.engine.L2BlockRefHead(ctx)
if canonicalRef, err := l1Fetcher.L1BlockRefByNumber(ctx, eq.safeHead.L1Origin.Number); errors.Is(err, ethereum.NotFound) { if err != nil {
// if our view of the l1 chain is lagging behind, we may get this error eq.log.Error("failed to find the L2 Head block", "err", err)
eq.log.Warn("engine safe head is ahead of L1 view", "block", eq.safeHead, "origin", eq.safeHead.L1Origin) return nil
} else if err != nil {
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)
} else {
// 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.progress = Progress{
Origin: canonicalRef,
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
} else {
// if the safe head is not canonical, then the unsafe head will not be either
eq.unsafeHead = eth.L2BlockRef{}
}
} }
unsafe, safe, err := sync.FindL2Heads(ctx, l2Head, eq.cfg.SeqWindowSize, l1Fetcher, eq.engine, &eq.cfg.Genesis)
// Don't walk past genesis. If we were at the L2 genesis, but could not find its L1 origin, if err != nil {
// the L2 chain is building on the wrong L1 branch. eq.log.Error("failed to find the L2 Heads to start from", "err", err)
if eq.safeHead.Hash == eq.cfg.Genesis.L2.Hash || eq.safeHead.Number == eq.cfg.Genesis.L2.Number { return nil
return fmt.Errorf("the L2 engine is coupled to unrecognized L1 chain: %v", eq.cfg.Genesis)
} }
l1Origin, err := l1Fetcher.L1BlockRefByHash(ctx, safe.L1Origin.Hash)
// Pull L2 parent for next iteration
block, err := eq.engine.L2BlockRefByHash(ctx, eq.safeHead.ParentHash)
if err != nil { if err != nil {
eq.log.Error("failed to fetch L2 block by hash during reset", "parent", eq.safeHead.ParentHash, "err", err) eq.log.Error("failed to fetch the new L1 progress", "err", err, "origin", safe.L1Origin)
return nil return nil
} }
eq.safeHead = block if safe.Time < l1Origin.Time {
return nil 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",
safe, safe.Time, l1Origin, l1Origin.Time)
}
eq.unsafeHead = unsafe
eq.safeHead = safe
eq.progress = Progress{
Origin: l1Origin,
Closed: false,
}
return io.EOF
} }
...@@ -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