Commit 465a10bc authored by protolambda's avatar protolambda

op-node: refactor sequencer to use EngineControl, test sequencing with chaos-monkey-like test

parent faf253dd
package actions
import (
"context"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/metrics"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/rollup/driver"
)
// MockL1OriginSelector is a shim to override the origin as sequencer, so we can force it to stay on an older origin.
type MockL1OriginSelector struct {
actual *driver.L1OriginSelector
originOverride eth.L1BlockRef // override which origin gets picked
}
func (m *MockL1OriginSelector) FindL1Origin(ctx context.Context, l1Head eth.L1BlockRef, l2Head eth.L2BlockRef) (eth.L1BlockRef, error) {
if m.originOverride != (eth.L1BlockRef{}) {
return m.originOverride, nil
}
return m.actual.FindL1Origin(ctx, l1Head, l2Head)
}
// L2Sequencer is an actor that functions like a rollup node,
// without the full P2P/API/Node stack, but just the derivation state, and simplified driver with sequencing ability.
type L2Sequencer struct {
L2Verifier
sequencer *driver.Sequencer
l1OriginSelector *driver.L1OriginSelector
seqOldOrigin bool // stay on current L1 origin when sequencing a block, unless forced to adopt the next origin
sequencer *driver.Sequencer
failL2GossipUnsafeBlock error // mock error
mockL1OriginSelector *MockL1OriginSelector
}
func NewL2Sequencer(t Testing, log log.Logger, l1 derive.L1Fetcher, eng L2API, cfg *rollup.Config, seqConfDepth uint64) *L2Sequencer {
ver := NewL2Verifier(t, log, l1, eng, cfg)
attrBuilder := derive.NewFetchingAttributesBuilder(cfg, l1, eng)
l1OriginSelector := &MockL1OriginSelector{
actual: driver.NewL1OriginSelector(log, cfg, l1, seqConfDepth),
}
return &L2Sequencer{
L2Verifier: *ver,
sequencer: driver.NewSequencer(log, cfg, eng, ver.derivation, attrBuilder, metrics.NoopMetrics),
l1OriginSelector: driver.NewL1OriginSelector(log, cfg, l1, seqConfDepth),
seqOldOrigin: false,
sequencer: driver.NewSequencer(log, cfg, ver.derivation, attrBuilder, l1OriginSelector),
mockL1OriginSelector: l1OriginSelector,
failL2GossipUnsafeBlock: nil,
}
}
......@@ -47,22 +62,7 @@ func (s *L2Sequencer) ActL2StartBlock(t Testing) {
return
}
parent := s.derivation.UnsafeL2Head()
var origin eth.L1BlockRef
if s.seqOldOrigin {
// force old origin, for testing purposes
oldOrigin, err := s.l1.L1BlockRefByHash(t.Ctx(), parent.L1Origin.Hash)
require.NoError(t, err, "failed to get current origin: %s", parent.L1Origin)
origin = oldOrigin
s.seqOldOrigin = false // don't repeat this
} else {
// select origin the real way
l1Origin, err := s.l1OriginSelector.FindL1Origin(t.Ctx(), s.l1State.L1Head(), parent)
require.NoError(t, err)
origin = l1Origin
}
err := s.sequencer.StartBuildingBlock(t.Ctx(), origin)
err := s.sequencer.StartBuildingBlock(t.Ctx(), s.l1State.L1Head())
require.NoError(t, err, "failed to start block building")
s.l2Building = true
......@@ -76,24 +76,21 @@ func (s *L2Sequencer) ActL2EndBlock(t Testing) {
}
s.l2Building = false
payload, err := s.sequencer.CompleteBuildingBlock(t.Ctx())
_, err := s.sequencer.CompleteBuildingBlock(t.Ctx())
// TODO: there may be legitimate temporary errors here, if we mock engine API RPC-failure.
// For advanced tests we can catch those and print a warning instead.
require.NoError(t, err)
ref, err := derive.PayloadToBlockRef(payload, &s.rollupCfg.Genesis)
require.NoError(t, err, "payload must convert to block ref")
s.derivation.SetUnsafeHead(ref)
// TODO: action-test publishing of payload on p2p
}
// ActL2KeepL1Origin makes the sequencer use the current L1 origin, even if the next origin is available.
func (s *L2Sequencer) ActL2KeepL1Origin(t Testing) {
if s.seqOldOrigin { // don't do this twice
t.InvalidAction("already decided to keep old L1 origin")
return
}
s.seqOldOrigin = true
parent := s.derivation.UnsafeL2Head()
// force old origin, for testing purposes
oldOrigin, err := s.l1.L1BlockRefByHash(t.Ctx(), parent.L1Origin.Hash)
require.NoError(t, err, "failed to get current origin: %s", parent.L1Origin)
s.mockL1OriginSelector.originOverride = oldOrigin
}
// ActBuildToL1Head builds empty blocks until (incl.) the L1 head becomes the L2 origin
......@@ -109,7 +106,7 @@ func (s *L2Sequencer) ActBuildToL1Head(t Testing) {
func (s *L2Sequencer) ActBuildToL1HeadExcl(t Testing) {
for {
s.ActL2PipelineFull(t)
nextOrigin, err := s.l1OriginSelector.FindL1Origin(t.Ctx(), s.l1State.L1Head(), s.derivation.UnsafeL2Head())
nextOrigin, err := s.mockL1OriginSelector.FindL1Origin(t.Ctx(), s.l1State.L1Head(), s.derivation.UnsafeL2Head())
require.NoError(t, err)
if nextOrigin.Number >= s.l1State.L1Head().Number {
break
......
......@@ -148,13 +148,6 @@ func (dp *DerivationPipeline) BuildingPayload() (onto eth.L2BlockRef, id eth.Pay
return dp.eng.BuildingPayload()
}
// SetUnsafeHead changes the forkchoice state unsafe head, without changing the engine.
//
// deprecated: use the EngineControl interface instead.
func (dp *DerivationPipeline) SetUnsafeHead(head eth.L2BlockRef) {
dp.eng.SetUnsafeHead(head)
}
// AddUnsafePayload schedules an execution payload to be processed, ahead of deriving it from L1
func (dp *DerivationPipeline) AddUnsafePayload(payload *eth.ExecutionPayload) {
dp.eng.AddUnsafePayload(payload)
......
......@@ -14,7 +14,6 @@ import (
type Metrics interface {
RecordPipelineReset()
RecordSequencingError()
RecordPublishingError()
RecordDerivationError()
......@@ -28,9 +27,8 @@ type Metrics interface {
SetDerivationIdle(idle bool)
RecordL1ReorgDepth(d uint64)
CountSequencedTxs(count int)
SequencerMetrics
EngineMetrics
}
type L1Chain interface {
......@@ -48,7 +46,6 @@ type L2Chain interface {
type DerivationPipeline interface {
Reset()
Step(ctx context.Context) error
SetUnsafeHead(head eth.L2BlockRef)
AddUnsafePayload(payload *eth.ExecutionPayload)
Finalize(ref eth.L1BlockRef)
FinalizedL1() eth.L1BlockRef
......@@ -68,14 +65,12 @@ type L1StateIface interface {
L1Finalized() eth.L1BlockRef
}
type L1OriginSelectorIface interface {
FindL1Origin(ctx context.Context, l1Head eth.L1BlockRef, l2Head eth.L2BlockRef) (eth.L1BlockRef, error)
}
type SequencerIface interface {
StartBuildingBlock(ctx context.Context, l1Origin eth.L1BlockRef) error
StartBuildingBlock(ctx context.Context, l1Head eth.L1BlockRef) error
CompleteBuildingBlock(ctx context.Context) (*eth.ExecutionPayload, error)
PlanNextSequencerAction(sequenceErr error) (delay time.Duration, seal bool, onto eth.BlockID)
PlanNextSequencerAction() time.Duration
RunNextSequencerAction(ctx context.Context, l1Head eth.L1BlockRef) *eth.ExecutionPayload
BuildingOnto() eth.L2BlockRef
}
type Network interface {
......@@ -90,7 +85,10 @@ func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, ne
verifConfDepth := NewConfDepth(driverCfg.VerifierConfDepth, l1State.L1Head, l1)
derivationPipeline := derive.NewDerivationPipeline(log, cfg, verifConfDepth, l2, metrics)
attrBuilder := derive.NewFetchingAttributesBuilder(cfg, l1, l2)
sequencer := NewSequencer(log, cfg, l2, derivationPipeline, attrBuilder, metrics)
engine := derivationPipeline
meteredEngine := NewMeteredEngine(cfg, engine, metrics, log)
sequencer := NewSequencer(log, cfg, meteredEngine, attrBuilder, findL1Origin)
return &Driver{
l1State: l1State,
derivation: derivationPipeline,
......@@ -106,7 +104,6 @@ func NewDriver(driverCfg *Config, cfg *rollup.Config, l2 L2Chain, l1 L1Chain, ne
snapshotLog: snapshotLog,
l1: l1,
l2: l2,
l1OriginSelector: findL1Origin,
sequencer: sequencer,
network: network,
metrics: metrics,
......
This diff is collapsed.
This diff is collapsed.
......@@ -25,8 +25,6 @@ type SyncStatus = eth.SyncStatus
// sealingDuration defines the expected time it takes to seal the block
const sealingDuration = time.Millisecond * 50
var UninitializedL1StateErr = errors.New("the L1 Head in L1 State is not initialized yet")
type Driver struct {
l1State L1StateIface
......@@ -71,11 +69,10 @@ type Driver struct {
// L2 Signals:
unsafeL2Payloads chan *eth.ExecutionPayload
l1 L1Chain
l2 L2Chain
l1OriginSelector L1OriginSelectorIface
sequencer SequencerIface
network Network // may be nil, network for is optional
l1 L1Chain
l2 L2Chain
sequencer SequencerIface
network Network // may be nil, network for is optional
metrics Metrics
log log.Logger
......@@ -142,75 +139,6 @@ func (s *Driver) OnUnsafeL2Payload(ctx context.Context, payload *eth.ExecutionPa
}
}
// startNewL2Block starts sequencing a new L2 block on top of the unsafe L2 Head.
func (s *Driver) startNewL2Block(ctx context.Context) error {
l2Head := s.derivation.UnsafeL2Head()
l1Head := s.l1State.L1Head()
if l1Head == (eth.L1BlockRef{}) {
return UninitializedL1StateErr
}
// Figure out which L1 origin block we're going to be building on top of.
l1Origin, err := s.l1OriginSelector.FindL1Origin(ctx, l1Head, l2Head)
if err != nil {
s.log.Error("Error finding next L1 Origin", "err", err)
return err
}
// Rollup is configured to not start producing blocks until a specific L1 block has been
// reached. Don't produce any blocks until we're at that genesis block.
if l1Origin.Number < s.config.Genesis.L1.Number {
s.log.Info("Skipping block production because the next L1 Origin is behind the L1 genesis", "next", l1Origin.ID(), "genesis", s.config.Genesis.L1)
return fmt.Errorf("the L1 origin %s cannot be before genesis at %s", l1Origin, s.config.Genesis.L1)
}
// Should never happen. Sequencer will halt if we get into this situation somehow.
nextL2Time := l2Head.Time + s.config.BlockTime
if nextL2Time < l1Origin.Time {
s.log.Error("Cannot build L2 block for time before L1 origin",
"l2Unsafe", l2Head, "nextL2Time", nextL2Time, "l1Origin", l1Origin, "l1OriginTime", l1Origin.Time)
return fmt.Errorf("cannot build L2 block on top %s for time %d before L1 origin %s at time %d",
l2Head, nextL2Time, l1Origin, l1Origin.Time)
}
// Start creating the new block.
return s.sequencer.StartBuildingBlock(ctx, l1Origin)
}
// completeNewBlock completes a previously started L2 block sequencing job.
func (s *Driver) completeNewBlock(ctx context.Context) error {
payload, err := s.sequencer.CompleteBuildingBlock(ctx)
if err != nil {
s.metrics.RecordSequencingError()
s.log.Error("Failed to seal block as sequencer", "err", err)
return err
}
// Generate an L2 block ref from the payload.
newUnsafeL2Head, err := derive.PayloadToBlockRef(payload, &s.config.Genesis)
if err != nil {
s.metrics.RecordSequencingError()
s.log.Error("Sequenced payload cannot be transformed into valid L2 block reference", "err", err)
return fmt.Errorf("sequenced payload cannot be transformed into valid L2 block reference: %w", err)
}
// Update our L2 head block based on the new unsafe block we just generated.
s.derivation.SetUnsafeHead(newUnsafeL2Head)
s.log.Info("Sequenced new l2 block", "l2_unsafe", newUnsafeL2Head, "l1_origin", newUnsafeL2Head.L1Origin, "txs", len(payload.Transactions), "time", newUnsafeL2Head.Time)
s.metrics.CountSequencedTxs(len(payload.Transactions))
if s.network != nil {
if err := s.network.PublishL2Payload(ctx, payload); err != nil {
s.log.Warn("failed to publish newly created block", "id", payload.ID(), "err", err)
s.metrics.RecordPublishingError()
// publishing of unsafe data via p2p is optional. Errors are not severe enough to change/halt sequencing but should be logged and metered.
}
}
return nil
}
// the eventLoop responds to L1 changes and internal timers to produce L2 blocks.
func (s *Driver) eventLoop() {
defer s.wg.Done()
......@@ -259,34 +187,23 @@ func (s *Driver) eventLoop() {
// L1 chain that we need to handle.
reqStep()
blockTime := time.Duration(s.config.BlockTime) * time.Second
var sequenceErr error
var sequenceErrTime time.Time
sequencerTimer := time.NewTimer(0)
var sequencerCh <-chan time.Time
var sequencingPlannedOnto eth.BlockID
var sequencerSealNext bool
planSequencerAction := func() {
delay, seal, onto := s.sequencer.PlanNextSequencerAction(sequenceErr)
if sequenceErr != nil && time.Since(sequenceErrTime) > delay {
sequenceErr = nil
}
delay := s.sequencer.PlanNextSequencerAction()
sequencerCh = sequencerTimer.C
if len(sequencerCh) > 0 { // empty if not already drained before resetting
<-sequencerCh
}
sequencerTimer.Reset(delay)
sequencingPlannedOnto = onto
sequencerSealNext = seal
}
for {
// If we are sequencing, update the trigger for the next sequencer action.
// If we are sequencing, and the L1 state is ready, update the trigger for the next sequencer action.
// This may adjust at any time based on fork-choice changes or previous errors.
if s.driverConfig.SequencerEnabled && !s.driverConfig.SequencerStopped {
if s.driverConfig.SequencerEnabled && !s.driverConfig.SequencerStopped && s.l1State.L1Head() != (eth.L1BlockRef{}) {
// update sequencer time if the head changed
if sequencingPlannedOnto != s.derivation.UnsafeL2Head().ID() {
if s.sequencer.BuildingOnto().ID() != s.derivation.UnsafeL2Head().ID() {
planSequencerAction()
}
} else {
......@@ -295,22 +212,14 @@ func (s *Driver) eventLoop() {
select {
case <-sequencerCh:
s.log.Info("sequencing now!", "seal", sequencerSealNext, "idle_derivation", s.idleDerivation)
if sequencerSealNext {
// try to seal the current block task, and allow it to take up to 3 block times.
// If this fails we will simply start a new block building job.
ctx, cancel := context.WithTimeout(ctx, 3*blockTime)
sequenceErr = s.completeNewBlock(ctx)
cancel()
} else {
// Start the block building, don't allow the starting of sequencing to get stuck for more the time of 1 block.
ctx, cancel := context.WithTimeout(ctx, blockTime)
sequenceErr = s.startNewL2Block(ctx)
cancel()
}
if sequenceErr != nil {
s.log.Error("sequencing error", "err", sequenceErr)
sequenceErrTime = time.Now()
payload := s.sequencer.RunNextSequencerAction(ctx, s.l1State.L1Head())
if s.network != nil && payload != nil {
// Publishing of unsafe data via p2p is optional.
// Errors are not severe enough to change/halt sequencing but should be logged and metered.
if err := s.network.PublishL2Payload(ctx, payload); err != nil {
s.log.Warn("failed to publish newly created block", "id", payload.ID(), "err", err)
s.metrics.RecordPublishingError()
}
}
planSequencerAction() // schedule the next sequencer action to keep the sequencing looping
case payload := <-s.unsafeL2Payloads:
......@@ -386,8 +295,8 @@ func (s *Driver) eventLoop() {
} else {
s.log.Info("Sequencer has been started")
s.driverConfig.SequencerStopped = false
sequencingPlannedOnto = eth.BlockID{}
close(resp.err)
planSequencerAction() // resume sequencing
}
case respCh := <-s.stopSequencer:
if s.driverConfig.SequencerStopped {
......
// On develop
package driver
import (
"context"
"errors"
"math/big"
"math/rand"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/metrics"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/testutils"
)
type TestDummyOutputImpl struct {
willError bool
cfg *rollup.Config
l1Origin eth.L1BlockRef
l2Head eth.L2BlockRef
}
func (d *TestDummyOutputImpl) PlanNextSequencerAction(sequenceErr error) (delay time.Duration, seal bool, onto eth.BlockID) {
return 0, d.l1Origin != (eth.L1BlockRef{}), d.l2Head.ParentID()
}
func (d *TestDummyOutputImpl) StartBuildingBlock(ctx context.Context, l1Origin eth.L1BlockRef) error {
d.l1Origin = l1Origin
return nil
}
func (d *TestDummyOutputImpl) CompleteBuildingBlock(ctx context.Context) (*eth.ExecutionPayload, error) {
// If we're meant to error, return one
if d.willError {
return nil, errors.New("the TestDummyOutputImpl.createNewBlock operation failed")
}
info := &testutils.MockBlockInfo{
InfoHash: d.l1Origin.Hash,
InfoParentHash: d.l1Origin.ParentHash,
InfoCoinbase: common.Address{},
InfoRoot: common.Hash{},
InfoNum: d.l1Origin.Number,
InfoTime: d.l1Origin.Time,
InfoMixDigest: [32]byte{},
InfoBaseFee: big.NewInt(123),
InfoReceiptRoot: common.Hash{},
}
infoTx, err := derive.L1InfoDepositBytes(d.l2Head.SequenceNumber, info, eth.SystemConfig{})
if err != nil {
panic(err)
}
payload := eth.ExecutionPayload{
ParentHash: d.l2Head.Hash,
FeeRecipient: common.Address{},
StateRoot: eth.Bytes32{},
ReceiptsRoot: eth.Bytes32{},
LogsBloom: eth.Bytes256{},
PrevRandao: eth.Bytes32{},
BlockNumber: eth.Uint64Quantity(d.l2Head.Number + 1),
GasLimit: 0,
GasUsed: 0,
Timestamp: eth.Uint64Quantity(d.l2Head.Time + d.cfg.BlockTime),
ExtraData: nil,
BaseFeePerGas: eth.Uint256Quantity{},
BlockHash: common.Hash{123},
Transactions: []eth.Data{infoTx},
}
return &payload, nil
}
var _ SequencerIface = (*TestDummyOutputImpl)(nil)
type TestDummyDerivationPipeline struct {
DerivationPipeline
l2Head eth.L2BlockRef
l2SafeHead eth.L2BlockRef
l2Finalized eth.L2BlockRef
}
func (d TestDummyDerivationPipeline) Reset() {}
func (d TestDummyDerivationPipeline) Step(ctx context.Context) error { return nil }
func (d TestDummyDerivationPipeline) SetUnsafeHead(head eth.L2BlockRef) {}
func (d TestDummyDerivationPipeline) AddUnsafePayload(payload *eth.ExecutionPayload) {}
func (d TestDummyDerivationPipeline) Finalized() eth.L2BlockRef { return d.l2Head }
func (d TestDummyDerivationPipeline) SafeL2Head() eth.L2BlockRef { return d.l2SafeHead }
func (d TestDummyDerivationPipeline) UnsafeL2Head() eth.L2BlockRef { return d.l2Finalized }
type TestDummyL1OriginSelector struct {
retval eth.L1BlockRef
}
func (l TestDummyL1OriginSelector) FindL1Origin(ctx context.Context, l1Head eth.L1BlockRef, l2Head eth.L2BlockRef) (eth.L1BlockRef, error) {
return l.retval, nil
}
// TestRejectCreateBlockBadTimestamp tests that a block creation with invalid timestamps will be caught.
// This does not test:
// - The findL1Origin call (it is hardcoded to be the head)
// - The outputInterface used to create a new block from a given payload.
// - The DerivationPipeline setting unsafe head (a mock provider is used to pretend to set it)
// - Metrics (only mocked enough to let the method proceed)
// - Publishing (network is set to nil so publishing won't occur)
func TestRejectCreateBlockBadTimestamp(t *testing.T) {
// Create our random provider
rng := rand.New(rand.NewSource(rand.Int63()))
// Create our context for methods to execute under
ctx := context.Background()
// Create our fake L1/L2 heads and link them accordingly
l1HeadRef := testutils.RandomBlockRef(rng)
l2HeadRef := testutils.RandomL2BlockRef(rng)
l2l1OriginBlock := l1HeadRef
l2HeadRef.L1Origin = l2l1OriginBlock.ID()
// Create a rollup config
cfg := rollup.Config{
BlockTime: uint64(60),
Genesis: rollup.Genesis{
L1: l1HeadRef.ID(),
L2: l2HeadRef.ID(),
L2Time: 0x7000, // dummy value
},
}
// Patch our timestamp so we fail
l2HeadRef.Time = l2l1OriginBlock.Time - (cfg.BlockTime * 2)
// Create our outputter
outputProvider := &TestDummyOutputImpl{cfg: &cfg, l2Head: l2HeadRef, willError: false}
// Create our state
s := Driver{
l1State: &L1State{
l1Head: l1HeadRef,
log: log.New(),
metrics: metrics.NoopMetrics,
},
log: log.New(),
l1OriginSelector: TestDummyL1OriginSelector{retval: l1HeadRef},
config: &cfg,
sequencer: outputProvider,
derivation: TestDummyDerivationPipeline{},
metrics: metrics.NoopMetrics,
}
// Create a new block
// - L2Head's L1Origin, its timestamp should be greater than L1 genesis.
// - L2Head timestamp + BlockTime should be greater than or equal to the L1 Time.
err := s.startNewL2Block(ctx)
if err == nil {
err = s.completeNewBlock(ctx)
}
// Verify the L1Origin's block number is greater than L1 genesis in our config.
if l2l1OriginBlock.Number < s.config.Genesis.L1.Number {
require.NoError(t, err, "L1Origin block number should be greater than the L1 genesis block number")
}
// Verify the new L2 block to create will have a time stamp equal or newer than our L1 origin block we derive from.
if l2HeadRef.Time+cfg.BlockTime < l2l1OriginBlock.Time {
// If not, we expect a specific error.
// TODO: This isn't the cleanest, we should construct + compare the whole error message.
require.NotNil(t, err)
require.Contains(t, err.Error(), "cannot build L2 block on top")
require.Contains(t, err.Error(), "for time")
require.Contains(t, err.Error(), "before L1 origin")
return
}
// If we expected the outputter to error, capture that here
if outputProvider.willError {
require.NotNil(t, err, "outputInterface failed to createNewBlock, so createNewL2Block should also have failed")
return
}
// Otherwise we should have no error.
require.NoError(t, err, "error raised in TestRejectCreateBlockBadTimestamp")
}
// FuzzRejectCreateBlockBadTimestamp is a property test derived from the TestRejectCreateBlockBadTimestamp unit test.
// It fuzzes timestamps and block times to find a configuration to violate error checking.
func FuzzRejectCreateBlockBadTimestamp(f *testing.F) {
f.Fuzz(func(t *testing.T, randSeed int64, l2Time uint64, blockTime uint64, forceOutputFail bool, currentL2HeadTime uint64) {
// Create our random provider
rng := rand.New(rand.NewSource(randSeed))
// Create our context for methods to execute under
ctx := context.Background()
// Create our fake L1/L2 heads and link them accordingly
l1HeadRef := testutils.RandomBlockRef(rng)
l2HeadRef := testutils.RandomL2BlockRef(rng)
l2l1OriginBlock := l1HeadRef
l2HeadRef.L1Origin = l2l1OriginBlock.ID()
// TODO: Cap our block time so it doesn't overflow
if blockTime > 0x100000 {
blockTime = 0x100000
}
// Create a rollup config
cfg := rollup.Config{
BlockTime: blockTime,
Genesis: rollup.Genesis{
L1: l1HeadRef.ID(),
L2: l2HeadRef.ID(),
L2Time: l2Time, // dummy value
},
}
// Patch our timestamp so we fail
l2HeadRef.Time = currentL2HeadTime
// Create our outputter
outputProvider := &TestDummyOutputImpl{cfg: &cfg, l2Head: l2HeadRef, willError: forceOutputFail}
// Create our state
s := Driver{
l1State: &L1State{
l1Head: l1HeadRef,
log: log.New(),
metrics: metrics.NoopMetrics,
},
log: log.New(),
l1OriginSelector: TestDummyL1OriginSelector{retval: l1HeadRef},
config: &cfg,
sequencer: outputProvider,
derivation: TestDummyDerivationPipeline{},
metrics: metrics.NoopMetrics,
}
// Create a new block
// - L2Head's L1Origin, its timestamp should be greater than L1 genesis.
// - L2Head timestamp + BlockTime should be greater than or equal to the L1 Time.
err := s.startNewL2Block(ctx)
if err == nil {
err = s.completeNewBlock(ctx)
}
// Verify the L1Origin's timestamp is greater than L1 genesis in our config.
if l2l1OriginBlock.Number < s.config.Genesis.L1.Number {
require.NoError(t, err)
return
}
// Verify the new L2 block to create will have a time stamp equal or newer than our L1 origin block we derive from.
if l2HeadRef.Time+cfg.BlockTime < l2l1OriginBlock.Time {
// If not, we expect a specific error.
// TODO: This isn't the cleanest, we should construct + compare the whole error message.
require.NotNil(t, err)
require.Contains(t, err.Error(), "cannot build L2 block on top")
require.Contains(t, err.Error(), "for time")
require.Contains(t, err.Error(), "before L1 origin")
return
}
// Otherwise we should have no error.
require.Nil(t, err)
// If we expected the outputter to error, capture that here
if outputProvider.willError {
require.NotNil(t, err, "outputInterface failed to createNewBlock, so createNewL2Block should also have failed")
return
}
// Otherwise we should have no error.
require.NoError(t, err, "L1Origin block number should be greater than the L1 genesis block number")
})
}
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