github.com/ethereum-optimism/optimism@v1.7.2/op-node/rollup/sync/start.go (about)

     1  // Package sync is responsible for reconciling L1 and L2.
     2  //
     3  // The Ethereum chain is a DAG of blocks with the root block being the genesis block. At any given
     4  // time, the head (or tip) of the chain can change if an offshoot/branch of the chain has a higher
     5  // total difficulty. This is known as a re-organization of the canonical chain. Each block points to
     6  // a parent block and the node is responsible for deciding which block is the head and thus the
     7  // mapping from block number to canonical block.
     8  //
     9  // The Optimism (L2) chain has similar properties, but also retains references to the Ethereum (L1)
    10  // chain. Each L2 block retains a reference to an L1 block (its "L1 origin", i.e. L1 block
    11  // associated with the epoch that the L2 block belongs to) and to its parent L2 block. The L2 chain
    12  // node must satisfy the following validity rules:
    13  //
    14  //  1. l2block.number == l2block.l2parent.block.number + 1
    15  //  2. l2block.l1Origin.number >= l2block.l2parent.l1Origin.number
    16  //  3. l2block.l1Origin is in the canonical chain on L1
    17  //  4. l1_rollup_genesis is an ancestor of l2block.l1Origin
    18  //
    19  // During normal operation, both the L1 and L2 canonical chains can change, due to a re-organisation
    20  // or due to an extension (new L1 or L2 block).
    21  //
    22  // In particular, in the case of L1 extension, the L2 unsafe head will generally remain the same,
    23  // but in the case of an L1 re-org, we need to search for the new safe and unsafe L2 block.
    24  package sync
    25  
    26  import (
    27  	"context"
    28  	"errors"
    29  	"fmt"
    30  
    31  	"github.com/ethereum/go-ethereum"
    32  	"github.com/ethereum/go-ethereum/common"
    33  	"github.com/ethereum/go-ethereum/log"
    34  
    35  	"github.com/ethereum-optimism/optimism/op-node/rollup"
    36  	"github.com/ethereum-optimism/optimism/op-service/eth"
    37  )
    38  
    39  type L1Chain interface {
    40  	L1BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L1BlockRef, error)
    41  	L1BlockRefByNumber(ctx context.Context, number uint64) (eth.L1BlockRef, error)
    42  	L1BlockRefByHash(ctx context.Context, hash common.Hash) (eth.L1BlockRef, error)
    43  }
    44  
    45  type L2Chain interface {
    46  	L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error)
    47  	L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error)
    48  }
    49  
    50  var ReorgFinalizedErr = errors.New("cannot reorg finalized block")
    51  var WrongChainErr = errors.New("wrong chain")
    52  var TooDeepReorgErr = errors.New("reorg is too deep")
    53  
    54  const MaxReorgSeqWindows = 5
    55  
    56  type FindHeadsResult struct {
    57  	Unsafe    eth.L2BlockRef
    58  	Safe      eth.L2BlockRef
    59  	Finalized eth.L2BlockRef
    60  }
    61  
    62  // currentHeads returns the current finalized, safe and unsafe heads of the execution engine.
    63  // If nothing has been marked finalized yet, the finalized head defaults to the genesis block.
    64  // If nothing has been marked safe yet, the safe head defaults to the finalized block.
    65  func currentHeads(ctx context.Context, cfg *rollup.Config, l2 L2Chain) (*FindHeadsResult, error) {
    66  	finalized, err := l2.L2BlockRefByLabel(ctx, eth.Finalized)
    67  	if errors.Is(err, ethereum.NotFound) {
    68  		// default to genesis if we have not finalized anything before.
    69  		finalized, err = l2.L2BlockRefByHash(ctx, cfg.Genesis.L2.Hash)
    70  	}
    71  	if err != nil {
    72  		return nil, fmt.Errorf("failed to find the finalized L2 block: %w", err)
    73  	}
    74  
    75  	safe, err := l2.L2BlockRefByLabel(ctx, eth.Safe)
    76  	if errors.Is(err, ethereum.NotFound) {
    77  		safe = finalized
    78  	} else if err != nil {
    79  		return nil, fmt.Errorf("failed to find the safe L2 block: %w", err)
    80  	}
    81  
    82  	unsafe, err := l2.L2BlockRefByLabel(ctx, eth.Unsafe)
    83  	if err != nil {
    84  		return nil, fmt.Errorf("failed to find the L2 head block: %w", err)
    85  	}
    86  	return &FindHeadsResult{
    87  		Unsafe:    unsafe,
    88  		Safe:      safe,
    89  		Finalized: finalized,
    90  	}, nil
    91  }
    92  
    93  // FindL2Heads walks back from `start` (the previous unsafe L2 block) and finds
    94  // the finalized, unsafe and safe L2 blocks.
    95  //
    96  //   - The *unsafe L2 block*: This is the highest L2 block whose L1 origin is a *plausible*
    97  //     extension of the canonical L1 chain (as known to the op-node).
    98  //   - The *safe L2 block*: This is the highest L2 block whose epoch's sequencing window is
    99  //     complete within the canonical L1 chain (as known to the op-node).
   100  //   - The *finalized L2 block*: This is the L2 block which is known to be fully derived from
   101  //     finalized L1 block data.
   102  //
   103  // Plausible: meaning that the blockhash of the L2 block's L1 origin
   104  // (as reported in the L1 Attributes deposit within the L2 block) is not canonical at another height in the L1 chain,
   105  // and the same holds for all its ancestors.
   106  func FindL2Heads(ctx context.Context, cfg *rollup.Config, l1 L1Chain, l2 L2Chain, lgr log.Logger, syncCfg *Config) (result *FindHeadsResult, err error) {
   107  	// Fetch current L2 forkchoice state
   108  	result, err = currentHeads(ctx, cfg, l2)
   109  	if err != nil {
   110  		return nil, fmt.Errorf("failed to fetch current L2 forkchoice state: %w", err)
   111  	}
   112  
   113  	lgr.Info("Loaded current L2 heads", "unsafe", result.Unsafe, "safe", result.Safe, "finalized", result.Finalized,
   114  		"unsafe_origin", result.Unsafe.L1Origin, "safe_origin", result.Safe.L1Origin)
   115  
   116  	// Remember original unsafe block to determine reorg depth
   117  	prevUnsafe := result.Unsafe
   118  
   119  	// Current L2 block.
   120  	n := result.Unsafe
   121  
   122  	var highestL2WithCanonicalL1Origin eth.L2BlockRef // the highest L2 block with confirmed canonical L1 origin
   123  	var l1Block eth.L1BlockRef                        // the L1 block at the height of the L1 origin of the current L2 block n.
   124  	var ahead bool                                    // when "n", the L2 block, has a L1 origin that is not visible in our L1 chain source yet
   125  
   126  	ready := false // when we found the block after the safe head, and we just need to return the parent block.
   127  
   128  	// Each loop iteration we traverse further from the unsafe head towards the finalized head.
   129  	// Once we pass the previous safe head and we have seen enough canonical L1 origins to fill a sequence window worth of data,
   130  	// then we return the last L2 block of the epoch before that as safe head.
   131  	// Each loop iteration we traverse a single L2 block, and we check if the L1 origins are consistent.
   132  	for {
   133  		// Fetch L1 information if we never had it, or if we do not have it for the current origin.
   134  		// Optimization: as soon as we have a previous L1 block, try to traverse L1 by hash instead of by number, to fill the cache.
   135  		if n.L1Origin.Hash == l1Block.ParentHash {
   136  			b, err := l1.L1BlockRefByHash(ctx, n.L1Origin.Hash)
   137  			if err != nil {
   138  				// Exit, find-sync start should start over, to move to an available L1 chain with block-by-number / not-found case.
   139  				return nil, fmt.Errorf("failed to retrieve L1 block: %w", err)
   140  			}
   141  			lgr.Info("Walking back L1Block by hash", "curr", l1Block, "next", b, "l2block", n)
   142  			l1Block = b
   143  			ahead = false
   144  		} else if l1Block == (eth.L1BlockRef{}) || n.L1Origin.Hash != l1Block.Hash {
   145  			b, err := l1.L1BlockRefByNumber(ctx, n.L1Origin.Number)
   146  			// if L2 is ahead of L1 view, then consider it a "plausible" head
   147  			notFound := errors.Is(err, ethereum.NotFound)
   148  			if err != nil && !notFound {
   149  				return nil, fmt.Errorf("failed to retrieve block %d from L1 for comparison against %s: %w", n.L1Origin.Number, n.L1Origin.Hash, err)
   150  			}
   151  			l1Block = b
   152  			ahead = notFound
   153  			lgr.Info("Walking back L1Block by number", "curr", l1Block, "next", b, "l2block", n)
   154  		}
   155  
   156  		lgr.Trace("walking sync start", "l2block", n)
   157  
   158  		// Don't walk past genesis. If we were at the L2 genesis, but could not find its L1 origin,
   159  		// the L2 chain is building on the wrong L1 branch.
   160  		if n.Number == cfg.Genesis.L2.Number {
   161  			// Check L2 traversal against L2 Genesis data, to make sure the engine is on the correct chain, instead of attempting sync with different L2 destination.
   162  			if n.Hash != cfg.Genesis.L2.Hash {
   163  				return nil, fmt.Errorf("%w L2: genesis: %s, got %s", WrongChainErr, cfg.Genesis.L2, n)
   164  			}
   165  			// Check L1 comparison against L1 Genesis data, to make sure the L1 data is from the correct chain, instead of attempting sync with different L1 source.
   166  			if !ahead && l1Block.Hash != cfg.Genesis.L1.Hash {
   167  				return nil, fmt.Errorf("%w L1: genesis: %s, got %s", WrongChainErr, cfg.Genesis.L1, l1Block)
   168  			}
   169  		}
   170  		// Check L2 traversal against finalized data
   171  		if (n.Number == result.Finalized.Number) && (n.Hash != result.Finalized.Hash) {
   172  			return nil, fmt.Errorf("%w: finalized %s, got: %s", ReorgFinalizedErr, result.Finalized, n)
   173  		}
   174  
   175  		// If we don't have a usable unsafe head, then set it
   176  		if result.Unsafe == (eth.L2BlockRef{}) {
   177  			result.Unsafe = n
   178  			// Check we are not reorging L2 incredibly deep
   179  			if n.L1Origin.Number+(MaxReorgSeqWindows*cfg.SyncLookback()) < prevUnsafe.L1Origin.Number {
   180  				// If the reorg depth is too large, something is fishy.
   181  				// This can legitimately happen if L1 goes down for a while. But in that case,
   182  				// restarting the L2 node with a bigger configured MaxReorgDepth is an acceptable
   183  				// stopgap solution.
   184  				return nil, fmt.Errorf("%w: traversed back to L2 block %s, but too deep compared to previous unsafe block %s", TooDeepReorgErr, n, prevUnsafe)
   185  			}
   186  		}
   187  
   188  		if ahead {
   189  			// keep the unsafe head if we can't tell if its L1 origin is canonical or not yet.
   190  		} else if l1Block.Hash == n.L1Origin.Hash {
   191  			// if L2 matches canonical chain, even if unsafe,
   192  			// then we can start finding a span of L1 blocks to cover the sequence window,
   193  			// which may help avoid rewinding the existing safe head unnecessarily.
   194  			if highestL2WithCanonicalL1Origin == (eth.L2BlockRef{}) {
   195  				highestL2WithCanonicalL1Origin = n
   196  			}
   197  		} else {
   198  			// L1 origin neither ahead of L1 head nor canonical, discard previous candidate and keep looking.
   199  			result.Unsafe = eth.L2BlockRef{}
   200  			highestL2WithCanonicalL1Origin = eth.L2BlockRef{}
   201  		}
   202  
   203  		// If the L2 block is at least as old as the previous safe head, and we have seen at least a full sequence window worth of L1 blocks to confirm
   204  		if n.Number <= result.Safe.Number && n.L1Origin.Number+cfg.SyncLookback() < highestL2WithCanonicalL1Origin.L1Origin.Number && n.SequenceNumber == 0 {
   205  			ready = true
   206  		}
   207  
   208  		// Don't traverse further than the finalized head to find a safe head
   209  		if n.Number == result.Finalized.Number {
   210  			lgr.Info("Hit finalized L2 head, returning immediately", "unsafe", result.Unsafe, "safe", result.Safe,
   211  				"finalized", result.Finalized, "unsafe_origin", result.Unsafe.L1Origin, "safe_origin", result.Safe.L1Origin)
   212  			result.Safe = n
   213  			return result, nil
   214  		}
   215  
   216  		if syncCfg.SkipSyncStartCheck && highestL2WithCanonicalL1Origin.Hash == n.Hash {
   217  			lgr.Info("Found highest L2 block with canonical L1 origin. Skip further sanity check and jump to the safe head")
   218  			n = result.Safe
   219  			continue
   220  		}
   221  		// Pull L2 parent for next iteration
   222  		parent, err := l2.L2BlockRefByHash(ctx, n.ParentHash)
   223  		if err != nil {
   224  			return nil, fmt.Errorf("failed to fetch L2 block by hash %v: %w", n.ParentHash, err)
   225  		}
   226  
   227  		// Check the L1 origin relation
   228  		if parent.L1Origin != n.L1Origin {
   229  			// sanity check that the L1 origin block number is coherent
   230  			if parent.L1Origin.Number+1 != n.L1Origin.Number {
   231  				return nil, fmt.Errorf("l2 parent %s of %s has L1 origin %s that is not before %s", parent, n, parent.L1Origin, n.L1Origin)
   232  			}
   233  			// sanity check that the later sequence number is 0, if it changed between the L2 blocks
   234  			if n.SequenceNumber != 0 {
   235  				return nil, fmt.Errorf("l2 block %s has parent %s with different L1 origin %s, but non-zero sequence number %d", n, parent, parent.L1Origin, n.SequenceNumber)
   236  			}
   237  			// if the L1 origin is known to be canonical, then the parent must be too
   238  			if l1Block.Hash == n.L1Origin.Hash && l1Block.ParentHash != parent.L1Origin.Hash {
   239  				return nil, fmt.Errorf("parent L2 block %s has origin %s but expected %s", parent, parent.L1Origin, l1Block.ParentHash)
   240  			}
   241  		} else {
   242  			if parent.SequenceNumber+1 != n.SequenceNumber {
   243  				return nil, fmt.Errorf("sequence number inconsistency %d <> %d between l2 blocks %s and %s", parent.SequenceNumber, n.SequenceNumber, parent, n)
   244  			}
   245  		}
   246  
   247  		n = parent
   248  
   249  		// once we found the block at seq nr 0 that is more than a full seq window behind the common chain post-reorg, then use the parent block as safe head.
   250  		if ready {
   251  			result.Safe = n
   252  			return result, nil
   253  		}
   254  	}
   255  }