finalizer.go 10.8 KB
Newer Older
1 2 3 4 5 6
package finality

import (
	"context"
	"fmt"
	"sync"
7
	"time"
8 9 10 11 12

	"github.com/ethereum/go-ethereum/log"

	"github.com/ethereum-optimism/optimism/op-node/rollup"
	"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
13
	"github.com/ethereum-optimism/optimism/op-node/rollup/engine"
14
	"github.com/ethereum-optimism/optimism/op-node/rollup/event"
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
	"github.com/ethereum-optimism/optimism/op-service/eth"
)

// defaultFinalityLookback defines the amount of L1<>L2 relations to track for finalization purposes, one per L1 block.
//
// When L1 finalizes blocks, it finalizes finalityLookback blocks behind the L1 head.
// Non-finality may take longer, but when it does finalize again, it is within this range of the L1 head.
// Thus we only need to retain the L1<>L2 derivation relation data of this many L1 blocks.
//
// In the event of older finalization signals, misconfiguration, or insufficient L1<>L2 derivation relation data,
// then we may miss the opportunity to finalize more L2 blocks.
// This does not cause any divergence, it just causes lagging finalization status.
//
// The beacon chain on mainnet has 32 slots per epoch,
// and new finalization events happen at most 4 epochs behind the head.
// And then we add 1 to make pruning easier by leaving room for a new item without pruning the 32*4.
const defaultFinalityLookback = 4*32 + 1

// finalityDelay is the number of L1 blocks to traverse before trying to finalize L2 blocks again.
// We do not want to do this too often, since it requires fetching a L1 block by number, so no cache data.
const finalityDelay = 64

37
// calcFinalityLookback calculates the default finality lookback based on DA challenge window if altDA
38 39
// mode is activated or L1 finality lookback.
func calcFinalityLookback(cfg *rollup.Config) uint64 {
40
	// in alt-da mode the longest finality lookback is a commitment is challenged on the last block of
41
	// the challenge window in which case it will be both challenge + resolve window.
42 43 44
	if cfg.AltDAEnabled() {
		lkb := cfg.AltDAConfig.DAChallengeWindow + cfg.AltDAConfig.DAResolveWindow + 1
		// in the case only if the altDA windows are longer than the default finality lookback
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
		if lkb > defaultFinalityLookback {
			return lkb
		}
	}
	return defaultFinalityLookback
}

type FinalityData struct {
	// The last L2 block that was fully derived and inserted into the L2 engine while processing this L1 block.
	L2Block eth.L2BlockRef
	// The L1 block this stage was at when inserting the L2 block.
	// When this L1 block is finalized, the L2 chain up to this block can be fully reproduced from finalized L1 data.
	L1Block eth.BlockID
}

type FinalizerEngine interface {
	Finalized() eth.L2BlockRef
	SetFinalizedHead(eth.L2BlockRef)
}

type FinalizerL1Interface interface {
	L1BlockRefByNumber(context.Context, uint64) (eth.L1BlockRef, error)
}

type Finalizer struct {
	mu sync.Mutex

	log log.Logger

74 75
	ctx context.Context

76
	emitter event.Emitter
77

78 79 80 81
	// finalizedL1 is the currently perceived finalized L1 block.
	// This may be ahead of the current traversed origin when syncing.
	finalizedL1 eth.L1BlockRef

82 83 84
	// lastFinalizedL2 maintains how far we finalized, so we don't have to emit re-attempts.
	lastFinalizedL2 eth.L2BlockRef

85 86 87 88 89 90 91 92 93 94 95 96
	// triedFinalizeAt tracks at which L1 block number we last tried to finalize during sync.
	triedFinalizeAt uint64

	// Tracks which L2 blocks where last derived from which L1 block. At most finalityLookback large.
	finalityData []FinalityData

	// Maximum amount of L2 blocks to store in finalityData.
	finalityLookback uint64

	l1Fetcher FinalizerL1Interface
}

97
func NewFinalizer(ctx context.Context, log log.Logger, cfg *rollup.Config, l1Fetcher FinalizerL1Interface) *Finalizer {
98 99
	lookback := calcFinalityLookback(cfg)
	return &Finalizer{
100
		ctx:              ctx,
101 102 103 104 105 106 107 108 109
		log:              log,
		finalizedL1:      eth.L1BlockRef{},
		triedFinalizeAt:  0,
		finalityData:     make([]FinalityData, 0, lookback),
		finalityLookback: lookback,
		l1Fetcher:        l1Fetcher,
	}
}

110 111 112 113
func (fi *Finalizer) AttachEmitter(em event.Emitter) {
	fi.emitter = em
}

114 115 116 117 118 119 120 121 122
// FinalizedL1 identifies the L1 chain (incl.) that included and/or produced all the finalized L2 blocks.
// This may return a zeroed ID if no finalization signals have been seen yet.
func (fi *Finalizer) FinalizedL1() (out eth.L1BlockRef) {
	fi.mu.Lock()
	defer fi.mu.Unlock()
	out = fi.finalizedL1
	return
}

123 124 125 126 127 128 129 130 131 132 133 134 135 136
type FinalizeL1Event struct {
	FinalizedL1 eth.L1BlockRef
}

func (ev FinalizeL1Event) String() string {
	return "finalized-l1"
}

type TryFinalizeEvent struct{}

func (ev TryFinalizeEvent) String() string {
	return "try-finalize"
}

137
func (fi *Finalizer) OnEvent(ev event.Event) bool {
138 139 140 141 142 143 144 145 146 147 148 149 150
	switch x := ev.(type) {
	case FinalizeL1Event:
		fi.onL1Finalized(x.FinalizedL1)
	case engine.SafeDerivedEvent:
		fi.onDerivedSafeBlock(x.Safe, x.DerivedFrom)
	case derive.DeriverIdleEvent:
		fi.onDerivationIdle(x.Origin)
	case rollup.ResetEvent:
		fi.onReset()
	case TryFinalizeEvent:
		fi.tryFinalize()
	case engine.ForkchoiceUpdateEvent:
		fi.lastFinalizedL2 = x.FinalizedL2Head
151 152
	default:
		return false
153
	}
154
	return true
155 156 157 158
}

// onL1Finalized applies a L1 finality signal
func (fi *Finalizer) onL1Finalized(l1Origin eth.L1BlockRef) {
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
	fi.mu.Lock()
	defer fi.mu.Unlock()
	prevFinalizedL1 := fi.finalizedL1
	if l1Origin.Number < fi.finalizedL1.Number {
		fi.log.Error("ignoring old L1 finalized block signal! Is the L1 provider corrupted?",
			"prev_finalized_l1", prevFinalizedL1, "signaled_finalized_l1", l1Origin)
		return
	}

	if fi.finalizedL1 != l1Origin {
		// reset triedFinalizeAt, so we give finalization a shot with the new signal
		fi.triedFinalizeAt = 0

		// remember the L1 finalization signal
		fi.finalizedL1 = l1Origin
	}

176 177
	// when the L1 change we can suggest to try to finalize, as the pre-condition for L2 finality has now changed
	fi.emitter.Emit(TryFinalizeEvent{})
178 179
}

180
// onDerivationIdle is called when the pipeline is exhausted of new data (i.e. no more L2 blocks to derive from).
181 182 183 184 185 186 187
//
// Since finality applies to all L2 blocks fully derived from the same block,
// it optimal to only check after the derivation from the L1 block has been exhausted.
//
// This will look at what has been buffered so far,
// sanity-check we are on the finalizing L1 chain,
// and finalize any L2 blocks that were fully derived from known finalized L1 blocks.
188
func (fi *Finalizer) onDerivationIdle(derivedFrom eth.L1BlockRef) {
189 190 191
	fi.mu.Lock()
	defer fi.mu.Unlock()
	if fi.finalizedL1 == (eth.L1BlockRef{}) {
192
		return // if no L1 information is finalized yet, then skip this
193 194 195
	}
	// If we recently tried finalizing, then don't try again just yet, but traverse more of L1 first.
	if fi.triedFinalizeAt != 0 && derivedFrom.Number <= fi.triedFinalizeAt+finalityDelay {
196
		return
197
	}
198
	fi.log.Debug("processing L1 finality information", "l1_finalized", fi.finalizedL1, "derived_from", derivedFrom, "previous", fi.triedFinalizeAt)
199
	fi.triedFinalizeAt = derivedFrom.Number
200
	fi.emitter.Emit(TryFinalizeEvent{})
201 202
}

203 204 205 206 207 208
func (fi *Finalizer) tryFinalize() {
	fi.mu.Lock()
	defer fi.mu.Unlock()

	// overwritten if we finalize
	finalizedL2 := fi.lastFinalizedL2 // may be zeroed if nothing was finalized since startup.
209 210 211 212 213 214 215 216 217 218
	var finalizedDerivedFrom eth.BlockID
	// go through the latest inclusion data, and find the last L2 block that was derived from a finalized L1 block
	for _, fd := range fi.finalityData {
		if fd.L2Block.Number > finalizedL2.Number && fd.L1Block.Number <= fi.finalizedL1.Number {
			finalizedL2 = fd.L2Block
			finalizedDerivedFrom = fd.L1Block
			// keep iterating, there may be later L2 blocks that can also be finalized
		}
	}
	if finalizedDerivedFrom != (eth.BlockID{}) {
219 220
		ctx, cancel := context.WithTimeout(fi.ctx, time.Second*10)
		defer cancel()
221 222 223 224 225 226
		// Sanity check the finality signal of L1.
		// Even though the signal is trusted and we do the below check also,
		// the signal itself has to be canonical to proceed.
		// TODO(#10724): This check could be removed if the finality signal is fully trusted, and if tests were more flexible for this case.
		signalRef, err := fi.l1Fetcher.L1BlockRefByNumber(ctx, fi.finalizedL1.Number)
		if err != nil {
227 228
			fi.emitter.Emit(rollup.L1TemporaryErrorEvent{Err: fmt.Errorf("failed to check if on finalizing L1 chain, could not fetch block %d: %w", fi.finalizedL1.Number, err)})
			return
229 230
		}
		if signalRef.Hash != fi.finalizedL1.Hash {
231 232
			fi.emitter.Emit(rollup.ResetEvent{Err: fmt.Errorf("need to reset, we assumed %s is finalized, but canonical chain is %s", fi.finalizedL1, signalRef)})
			return
233 234 235 236 237 238
		}

		// Sanity check we are indeed on the finalizing chain, and not stuck on something else.
		// We assume that the block-by-number query is consistent with the previously received finalized chain signal
		derivedRef, err := fi.l1Fetcher.L1BlockRefByNumber(ctx, finalizedDerivedFrom.Number)
		if err != nil {
239 240
			fi.emitter.Emit(rollup.L1TemporaryErrorEvent{Err: fmt.Errorf("failed to check if on finalizing L1 chain, could not fetch block %d: %w", finalizedDerivedFrom.Number, err)})
			return
241 242
		}
		if derivedRef.Hash != finalizedDerivedFrom.Hash {
243 244 245
			fi.emitter.Emit(rollup.ResetEvent{Err: fmt.Errorf("need to reset, we are on %s, not on the finalizing L1 chain %s (towards %s)",
				finalizedDerivedFrom, derivedRef, fi.finalizedL1)})
			return
246
		}
247
		fi.emitter.Emit(engine.PromoteFinalizedEvent{Ref: finalizedL2})
248 249 250
	}
}

251
// onDerivedSafeBlock buffers the L1 block the safe head was fully derived from,
252
// to finalize it once the derived-from L1 block, or a later L1 block, finalizes.
253
func (fi *Finalizer) onDerivedSafeBlock(l2Safe eth.L2BlockRef, derivedFrom eth.L1BlockRef) {
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
	fi.mu.Lock()
	defer fi.mu.Unlock()
	// remember the last L2 block that we fully derived from the given finality data
	if len(fi.finalityData) == 0 || fi.finalityData[len(fi.finalityData)-1].L1Block.Number < derivedFrom.Number {
		// prune finality data if necessary, before appending any data.
		if uint64(len(fi.finalityData)) >= fi.finalityLookback {
			fi.finalityData = append(fi.finalityData[:0], fi.finalityData[1:fi.finalityLookback]...)
		}
		// append entry for new L1 block
		fi.finalityData = append(fi.finalityData, FinalityData{
			L2Block: l2Safe,
			L1Block: derivedFrom.ID(),
		})
		last := &fi.finalityData[len(fi.finalityData)-1]
		fi.log.Debug("extended finality-data", "last_l1", last.L1Block, "last_l2", last.L2Block)
	} else {
		// if it's a new L2 block that was derived from the same latest L1 block, then just update the entry
		last := &fi.finalityData[len(fi.finalityData)-1]
		if last.L2Block != l2Safe { // avoid logging if there are no changes
			last.L2Block = l2Safe
			fi.log.Debug("updated finality-data", "last_l1", last.L1Block, "last_l2", last.L2Block)
		}
	}
}

279
// onReset clears the recent history of safe-L2 blocks used for finalization,
280
// to avoid finalizing any reorged-out L2 blocks.
281
func (fi *Finalizer) onReset() {
282 283 284 285 286 287
	fi.mu.Lock()
	defer fi.mu.Unlock()
	fi.finalityData = fi.finalityData[:0]
	fi.triedFinalizeAt = 0
	// no need to reset finalizedL1, it's finalized after all
}