Commit 028edbe5 authored by protolambda's avatar protolambda Committed by GitHub

op-e2e: action tests - l2 verifier (#3634)

parent 48aa3d14
......@@ -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
......@@ -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,
......@@ -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 {
......
......@@ -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)
}
}
}
......
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)
}
}
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")
}
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