Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion op-e2e/actions/l1_miner.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import (
type L1Miner struct {
L1Replica

// L1 block building preferences
prefCoinbase common.Address

// L1 block building data
l1BuildingHeader *types.Header // block header that we add txs to for block building
l1BuildingState *state.StateDB // state used for block building
Expand Down Expand Up @@ -55,7 +58,7 @@ func (s *L1Miner) ActL1StartBlock(timeDelta uint64) Action {
}
header := &types.Header{
ParentHash: parentHash,
Coinbase: parent.Coinbase,
Coinbase: s.prefCoinbase,
Difficulty: common.Big0,
Number: new(big.Int).Add(parent.Number, common.Big1),
GasLimit: parent.GasLimit,
Expand Down Expand Up @@ -114,6 +117,13 @@ func (s *L1Miner) ActL1IncludeTx(from common.Address) Action {
}
}

func (s *L1Miner) ActL1SetFeeRecipient(coinbase common.Address) {
s.prefCoinbase = coinbase
if s.l1Building {
s.l1BuildingHeader.Coinbase = coinbase
}
}

// ActL1EndBlock finishes the new L1 block, and applies it to the chain as unsafe block
func (s *L1Miner) ActL1EndBlock(t Testing) {
if !s.l1Building {
Expand Down
33 changes: 21 additions & 12 deletions op-e2e/actions/l1_replica.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,27 @@ func NewL1Replica(log log.Logger, genesis *core.Genesis) *L1Replica {

// ActL1RewindToParent rewinds the L1 chain to parent block of head
func (s *L1Replica) ActL1RewindToParent(t Testing) {
head := s.l1Chain.CurrentHeader().Number.Uint64()
if head == 0 {
t.InvalidAction("cannot rewind L1 past genesis")
return
}
finalized := s.l1Chain.CurrentFinalizedBlock()
if finalized != nil && head <= finalized.NumberU64() {
t.InvalidAction("cannot rewind head of chain past finalized block %d", finalized.NumberU64())
return
}
if err := s.l1Chain.SetHead(head - 1); err != nil {
t.Fatalf("failed to rewind L1 chain to nr %d: %v", head-1, err)
s.ActL1RewindDepth(1)(t)
}

func (s *L1Replica) ActL1RewindDepth(depth uint64) Action {
return func(t Testing) {
if depth == 0 {
return
}
head := s.l1Chain.CurrentHeader().Number.Uint64()
if head < depth {
t.InvalidAction("cannot rewind L1 past genesis (current: %d, rewind depth: %d)", head, depth)
return
}
finalized := s.l1Chain.CurrentFinalizedBlock()
if finalized != nil && head < finalized.NumberU64()+depth {
t.InvalidAction("cannot rewind head of chain past finalized block %d with rewind depth %d", finalized.NumberU64(), depth)
return
}
if err := s.l1Chain.SetHead(head - depth); err != nil {
t.Fatalf("failed to rewind L1 chain to nr %d: %v", head-depth, err)
}
}
}

Expand Down
102 changes: 102 additions & 0 deletions op-e2e/actions/l2_verifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package actions

import (
"errors"
"io"

"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"
"github.com/ethereum-optimism/optimism/op-node/testutils"
)

// L2Verifier is an actor that functions like a rollup node,
// without the full P2P/API/Node stack, but just the derivation state, and simplified driver.
type L2Verifier struct {
log log.Logger

eng derive.Engine

// L2 rollup
derivation *derive.DerivationPipeline

l1Head eth.L1BlockRef
l1Safe eth.L1BlockRef
l1Finalized eth.L1BlockRef

l2PipelineIdle bool
l2Building bool

rollupCfg *rollup.Config
}

func NewL2Verifier(log log.Logger, l1 derive.L1Fetcher, eng derive.Engine, cfg *rollup.Config) *L2Verifier {
pipeline := derive.NewDerivationPipeline(log, cfg, l1, eng, &testutils.TestDerivationMetrics{})
pipeline.Reset()
return &L2Verifier{
log: log,
eng: eng,
derivation: pipeline,
l2PipelineIdle: true,
l2Building: false,
rollupCfg: cfg,
}
}

func (s *L2Verifier) SyncStatus() *eth.SyncStatus {
return &eth.SyncStatus{
CurrentL1: s.derivation.Progress().Origin,
HeadL1: s.l1Head,
SafeL1: s.l1Safe,
FinalizedL1: s.l1Finalized,
UnsafeL2: s.derivation.UnsafeL2Head(),
SafeL2: s.derivation.SafeL2Head(),
FinalizedL2: s.derivation.Finalized(),
}
}

// TODO: actions to change L1 head/safe/finalized state. Depends on driver refactor work.

// ActL2PipelineStep runs one iteration of the L2 derivation pipeline
func (s *L2Verifier) ActL2PipelineStep(t Testing) {
if s.l2Building {
t.InvalidAction("cannot derive new data while building L2 block")
return
}

s.l2PipelineIdle = false
err := s.derivation.Step(t.Ctx())
if err == io.EOF {
s.l2PipelineIdle = true
return
} else if err != nil && errors.Is(err, derive.NotEnoughData) {
return
} else if err != nil && errors.Is(err, derive.ErrReset) {
s.log.Warn("Derivation pipeline is reset", "err", err)
s.derivation.Reset()
return
} else if err != nil && errors.Is(err, derive.ErrTemporary) {
s.log.Warn("Derivation process temporary error", "err", err)
return
} else if err != nil && errors.Is(err, derive.ErrCritical) {
t.Fatalf("derivation failed critically: %v", err)
} else {
return
}
}

func (s *L2Verifier) ActL2PipelineFull(t Testing) {
s.l2PipelineIdle = false
for !s.l2PipelineIdle {
s.ActL2PipelineStep(t)
}
}

// ActL2UnsafeGossipReceive creates an action that can receive an unsafe execution payload, like gossipsub
func (s *L2Verifier) ActL2UnsafeGossipReceive(payload *eth.ExecutionPayload) Action {
return func(t Testing) {
s.derivation.AddUnsafePayload(payload)
}
}
93 changes: 93 additions & 0 deletions op-e2e/actions/l2_verifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package actions

import (
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"

"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/sources"
"github.com/ethereum-optimism/optimism/op-node/testlog"
)

func setupVerifierTest(t Testing, sd *e2eutils.SetupData, log log.Logger) (*L1Miner, *L2Engine, *L2Verifier) {
jwtPath := e2eutils.WriteDefaultJWT(t)

miner := NewL1Miner(log, sd.L1Cfg)

l1F, err := sources.NewL1Client(miner.RPCClient(), log, nil, sources.L1ClientDefaultConfig(sd.RollupCfg, false))
require.NoError(t, err)
engine := NewL2Engine(log, sd.L2Cfg, sd.RollupCfg.Genesis.L1, jwtPath)
l2Cl, err := sources.NewEngineClient(engine.RPCClient(), log, nil, sources.EngineClientDefaultConfig(sd.RollupCfg))
require.NoError(t, err)

verifier := NewL2Verifier(log, l1F, l2Cl, sd.RollupCfg)
return miner, engine, verifier
}

func TestL2Verifier_SequenceWindow(gt *testing.T) {
t := NewDefaultTesting(gt)
p := &e2eutils.TestParams{
MaxSequencerDrift: 10,
SequencerWindowSize: 24,
ChannelTimeout: 10,
}
dp := e2eutils.MakeDeployParams(t, p)
sd := e2eutils.Setup(t, dp, defaultAlloc)
log := testlog.Logger(t, log.LvlDebug)
miner, engine, verifier := setupVerifierTest(t, sd, log)
miner.ActL1SetFeeRecipient(common.Address{'A'})

// Make two sequence windows worth of empty L1 blocks. After we pass the first sequence window, the L2 chain should get blocks
for miner.l1Chain.CurrentBlock().NumberU64() < sd.RollupCfg.SeqWindowSize*2 {
miner.ActL1StartBlock(10)(t)
miner.ActL1EndBlock(t)

verifier.ActL2PipelineFull(t)

l1Head := miner.l1Chain.CurrentBlock().NumberU64()
expectedL1Origin := uint64(0)
// as soon as we complete the sequence window, we force-adopt the L1 origin
if l1Head >= sd.RollupCfg.SeqWindowSize {
expectedL1Origin = l1Head - sd.RollupCfg.SeqWindowSize + 1
}
require.Equal(t, expectedL1Origin, verifier.SyncStatus().SafeL2.L1Origin.Number, "L1 origin is forced in, given enough L1 blocks pass by")
require.LessOrEqual(t, miner.l1Chain.GetBlockByNumber(expectedL1Origin).Time(), engine.l2Chain.CurrentBlock().Time(), "L2 time higher than L1 origin time")
}
tip2N := verifier.SyncStatus()

// Do a deep L1 reorg as deep as a sequence window, this should affect the safe L2 chain
miner.ActL1RewindDepth(sd.RollupCfg.SeqWindowSize)(t)

// Without new L1 block, the L1 appears to not be synced, and the node shouldn't reorg
verifier.ActL2PipelineFull(t)
require.Equal(t, tip2N.SafeL2, verifier.SyncStatus().SafeL2, "still the same after verifier work")

// Make a new empty L1 block with different data than there was before.
miner.ActL1SetFeeRecipient(common.Address{'B'})
miner.ActL1StartBlock(10)(t)
miner.ActL1EndBlock(t)
reorgL1Block := miner.l1Chain.CurrentBlock()

// Still no reorg, we need more L1 blocks first, before the reorged L1 block is forced in by sequence window
verifier.ActL2PipelineFull(t)
require.Equal(t, tip2N.SafeL2, verifier.SyncStatus().SafeL2)

for miner.l1Chain.CurrentBlock().NumberU64() < sd.RollupCfg.SeqWindowSize*2 {
miner.ActL1StartBlock(10)(t)
miner.ActL1EndBlock(t)
}

// workaround: in L1Traversal we only recognize the reorg once we see origin N+1, we don't reorg to shorter L1 chains
miner.ActL1StartBlock(10)(t)
miner.ActL1EndBlock(t)

// Now it will reorg
verifier.ActL2PipelineFull(t)

// due to workaround we synced one more L1 block, so we need to compare against the parent of that
got := miner.l1Chain.GetBlockByHash(miner.l1Chain.GetBlockByHash(verifier.SyncStatus().SafeL2.L1Origin.Hash).ParentHash())
require.Equal(t, reorgL1Block.Hash(), got.Hash(), "must have reorged L2 chain to the new L1 chain")
}