sequencer.go 7.24 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
package driver

import (
	"context"
	"fmt"
	"time"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/log"

	"github.com/ethereum-optimism/optimism/op-node/eth"
	"github.com/ethereum-optimism/optimism/op-node/rollup"
	"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
)

type Downloader interface {
	InfoByHash(ctx context.Context, hash common.Hash) (eth.BlockInfo, error)
19
	FetchReceipts(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Receipts, error)
20 21
}

22 23
type L1OriginSelectorIface interface {
	FindL1Origin(ctx context.Context, l1Head eth.L1BlockRef, l2Head eth.L2BlockRef) (eth.L1BlockRef, error)
24 25
}

26 27 28 29 30
// Sequencer implements the sequencing interface of the driver: it starts and completes block building jobs.
type Sequencer struct {
	log    log.Logger
	config *rollup.Config

31
	engine derive.EngineControl
32

33 34
	attrBuilder      derive.AttributesBuilder
	l1OriginSelector L1OriginSelectorIface
35

36 37 38 39
	// timeNow enables sequencer testing to mock the time
	timeNow func() time.Time

	nextAction time.Time
40 41
}

42
func NewSequencer(log log.Logger, cfg *rollup.Config, engine derive.EngineControl, attributesBuilder derive.AttributesBuilder, l1OriginSelector L1OriginSelectorIface) *Sequencer {
43
	return &Sequencer{
44 45 46 47 48 49
		log:              log,
		config:           cfg,
		engine:           engine,
		timeNow:          time.Now,
		attrBuilder:      attributesBuilder,
		l1OriginSelector: l1OriginSelector,
50 51 52 53
	}
}

// StartBuildingBlock initiates a block building job on top of the given L2 head, safe and finalized blocks, and using the provided l1Origin.
54 55 56 57 58 59 60 61 62 63
func (d *Sequencer) StartBuildingBlock(ctx context.Context, l1Head eth.L1BlockRef) error {
	l2Head := d.engine.UnsafeL2Head()

	// Figure out which L1 origin block we're going to be building on top of.
	l1Origin, err := d.l1OriginSelector.FindL1Origin(ctx, l1Head, l2Head)
	if err != nil {
		d.log.Error("Error finding next L1 Origin", "err", err)
		return err
	}

64 65 66 67
	if !(l2Head.L1Origin.Hash == l1Origin.ParentHash || l2Head.L1Origin.Hash == l1Origin.Hash) {
		return fmt.Errorf("cannot build new L2 block with L1 origin %s (parent L1 %s) on current L2 head %s with L1 origin %s", l1Origin, l1Origin.ParentHash, l2Head, l2Head.L1Origin)
	}

68 69 70 71 72
	d.log.Info("creating new block", "parent", l2Head, "l1Origin", l1Origin)

	fetchCtx, cancel := context.WithTimeout(ctx, time.Second*20)
	defer cancel()

73
	attrs, err := d.attrBuilder.PreparePayloadAttributes(fetchCtx, l2Head, l1Origin.ID())
74 75 76 77 78 79 80 81
	if err != nil {
		return err
	}

	// If our next L2 block timestamp is beyond the Sequencer drift threshold, then we must produce
	// empty blocks (other than the L1 info deposit and any user deposits). We handle this by
	// setting NoTxPool to true, which will cause the Sequencer to not include any transactions
	// from the transaction pool.
82 83 84 85 86
	attrs.NoTxPool = uint64(attrs.Timestamp) > l1Origin.Time+d.config.MaxSequencerDrift

	d.log.Debug("prepared attributes for new block",
		"num", l2Head.Number+1, "time", uint64(attrs.Timestamp),
		"origin", l1Origin, "origin_time", l1Origin.Time, "noTxPool", attrs.NoTxPool)
87 88

	// Start a payload building process.
89
	errTyp, err := d.engine.StartPayload(ctx, l2Head, attrs, false)
90 91 92 93 94 95 96 97 98 99
	if err != nil {
		return fmt.Errorf("failed to start building on top of L2 chain %s, error (%d): %w", l2Head, errTyp, err)
	}
	return nil
}

// CompleteBuildingBlock takes the current block that is being built, and asks the engine to complete the building, seal the block, and persist it as canonical.
// Warning: the safe and finalized L2 blocks as viewed during the initiation of the block building are reused for completion of the block building.
// The Execution engine should not change the safe and finalized blocks between start and completion of block building.
func (d *Sequencer) CompleteBuildingBlock(ctx context.Context) (*eth.ExecutionPayload, error) {
100
	payload, errTyp, err := d.engine.ConfirmPayload(ctx)
101
	if err != nil {
102
		return nil, fmt.Errorf("failed to complete building block: error (%d): %w", errTyp, err)
103 104 105 106
	}
	return payload, nil
}

107 108 109 110 111 112
// CancelBuildingBlock cancels the current open block building job.
// This sequencer only maintains one block building job at a time.
func (d *Sequencer) CancelBuildingBlock(ctx context.Context) {
	// force-cancel, we can always continue block building, and any error is logged by the engine state
	_ = d.engine.CancelPayload(ctx, true)
}
113

114 115 116 117 118 119 120 121 122 123 124
// PlanNextSequencerAction returns a desired delay till the RunNextSequencerAction call.
func (d *Sequencer) PlanNextSequencerAction() time.Duration {
	head := d.engine.UnsafeL2Head()
	now := d.timeNow()

	buildingOnto, buildingID, _ := d.engine.BuildingPayload()

	// We may have to wait till the next sequencing action, e.g. upon an error.
	// If the head changed we need to respond and will not delay the sequencing.
	if delay := d.nextAction.Sub(now); delay > 0 && buildingOnto.Hash == head.Hash {
		return delay
125
	}
126

127
	blockTime := time.Duration(d.config.BlockTime) * time.Second
128
	payloadTime := time.Unix(int64(head.Time+d.config.BlockTime), 0)
129
	remainingTime := payloadTime.Sub(now)
130 131 132

	// If we started building a block already, and if that work is still consistent,
	// then we would like to finish it by sealing the block.
133
	if buildingID != (eth.PayloadID{}) && buildingOnto.Hash == head.Hash {
134 135
		// if we started building already, then we will schedule the sealing.
		if remainingTime < sealingDuration {
136
			return 0 // if there's not enough time for sealing, don't wait.
137 138
		} else {
			// finish with margin of sealing duration before payloadTime
139
			return remainingTime - sealingDuration
140 141 142 143 144
		}
	} else {
		// if we did not yet start building, then we will schedule the start.
		if remainingTime > blockTime {
			// if we have too much time, then wait before starting the build
145
			return remainingTime - blockTime
146 147
		} else {
			// otherwise start instantly
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
			return 0
		}
	}
}

// BuildingOnto returns the L2 head reference that the latest block is or was being built on top of.
func (d *Sequencer) BuildingOnto() eth.L2BlockRef {
	ref, _, _ := d.engine.BuildingPayload()
	return ref
}

// RunNextSequencerAction starts new block building work, or seals existing work,
// and is best timed by first awaiting the delay returned by PlanNextSequencerAction.
// If a new block is successfully sealed, it will be returned for publishing, nil otherwise.
func (d *Sequencer) RunNextSequencerAction(ctx context.Context, l1Head eth.L1BlockRef) *eth.ExecutionPayload {
	if _, buildingID, _ := d.engine.BuildingPayload(); buildingID != (eth.PayloadID{}) {
		payload, err := d.CompleteBuildingBlock(ctx)
		if err != nil {
			d.log.Error("sequencer failed to seal new block", "err", err)
			d.nextAction = d.timeNow().Add(time.Second)
			if buildingID != (eth.PayloadID{}) { // don't keep stale block building jobs around, try to cancel them
				d.CancelBuildingBlock(ctx)
			}
			return nil
		} else {
			d.log.Info("sequencer successfully built a new block", "block", payload.ID(), "time", uint64(payload.Timestamp), "txs", len(payload.Transactions))
			return payload
		}
	} else {
		err := d.StartBuildingBlock(ctx, l1Head)
		if err != nil {
			d.log.Error("sequencer failed to start building new block", "err", err)
			d.nextAction = d.timeNow().Add(time.Second)
		} else {
			d.log.Info("sequencer started building new block", "payload_id", buildingID)
183
		}
184
		return nil
185 186
	}
}