Commit 47da14af authored by Sebastian Stammler's avatar Sebastian Stammler Committed by GitHub

op-node/rollup: Implement Holocene invalid payload attributes handling (#12621)

* op-node/rollup: Implement Holocene invalid payload handling

* op-node: update comment about block-processing errors

---------
Co-authored-by: default avatarprotolambda <proto@protolambda.com>
parent 630f8466
package helpers package helpers
import ( import (
"math/rand"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
e2ecfg "github.com/ethereum-optimism/optimism/op-e2e/config"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/sync" "github.com/ethereum-optimism/optimism/op-node/rollup/sync"
...@@ -16,7 +19,8 @@ type Env struct { ...@@ -16,7 +19,8 @@ type Env struct {
Log log.Logger Log log.Logger
Logs *testlog.CapturingHandler Logs *testlog.CapturingHandler
SetupData *e2eutils.SetupData DeployParams *e2eutils.DeployParams
SetupData *e2eutils.SetupData
Miner *L1Miner Miner *L1Miner
Seq *L2Sequencer Seq *L2Sequencer
...@@ -24,6 +28,9 @@ type Env struct { ...@@ -24,6 +28,9 @@ type Env struct {
Verifier *L2Verifier Verifier *L2Verifier
VerifEngine *L2Engine VerifEngine *L2Engine
Batcher *L2Batcher Batcher *L2Batcher
Alice *CrossLayerUser
AddressCorpora []common.Address
} }
type EnvOpt struct { type EnvOpt struct {
...@@ -49,8 +56,9 @@ const DefaultFork = rollup.Holocene ...@@ -49,8 +56,9 @@ const DefaultFork = rollup.Holocene
// SetupEnv sets up a default action test environment. If no fork is specified, the default fork as // SetupEnv sets up a default action test environment. If no fork is specified, the default fork as
// specified by the package variable [defaultFork] is used. // specified by the package variable [defaultFork] is used.
func SetupEnv(t StatefulTesting, opts ...EnvOpt) (env Env) { func SetupEnv(t Testing, opts ...EnvOpt) (env Env) {
dp := e2eutils.MakeDeployParams(t, DefaultRollupTestParams()) dp := e2eutils.MakeDeployParams(t, DefaultRollupTestParams())
env.DeployParams = dp
log, logs := testlog.CaptureLogger(t, log.LevelDebug) log, logs := testlog.CaptureLogger(t, log.LevelDebug)
env.Log, env.Logs = log, logs env.Log, env.Logs = log, logs
...@@ -64,6 +72,8 @@ func SetupEnv(t StatefulTesting, opts ...EnvOpt) (env Env) { ...@@ -64,6 +72,8 @@ func SetupEnv(t StatefulTesting, opts ...EnvOpt) (env Env) {
sd := e2eutils.Setup(t, dp, DefaultAlloc) sd := e2eutils.Setup(t, dp, DefaultAlloc)
env.SetupData = sd env.SetupData = sd
env.AddressCorpora = e2eutils.CollectAddresses(sd, dp)
env.Miner, env.SeqEngine, env.Seq = SetupSequencerTest(t, sd, log) env.Miner, env.SeqEngine, env.Seq = SetupSequencerTest(t, sd, log)
env.Miner.ActL1SetFeeRecipient(common.Address{'A'}) env.Miner.ActL1SetFeeRecipient(common.Address{'A'})
env.VerifEngine, env.Verifier = SetupVerifier(t, sd, log, env.Miner.L1Client(t, sd.RollupCfg), env.Miner.BlobStore(), &sync.Config{}) env.VerifEngine, env.Verifier = SetupVerifier(t, sd, log, env.Miner.L1Client(t, sd.RollupCfg), env.Miner.BlobStore(), &sync.Config{})
...@@ -71,9 +81,34 @@ func SetupEnv(t StatefulTesting, opts ...EnvOpt) (env Env) { ...@@ -71,9 +81,34 @@ func SetupEnv(t StatefulTesting, opts ...EnvOpt) (env Env) {
env.Batcher = NewL2Batcher(log, sd.RollupCfg, DefaultBatcherCfg(dp), env.Batcher = NewL2Batcher(log, sd.RollupCfg, DefaultBatcherCfg(dp),
rollupSeqCl, env.Miner.EthClient(), env.SeqEngine.EthClient(), env.SeqEngine.EngineClient(t, sd.RollupCfg)) rollupSeqCl, env.Miner.EthClient(), env.SeqEngine.EthClient(), env.SeqEngine.EngineClient(t, sd.RollupCfg))
alice := NewCrossLayerUser(log, dp.Secrets.Alice, rand.New(rand.NewSource(0xa57b)), e2ecfg.AllocTypeStandard)
alice.L1.SetUserEnv(env.L1UserEnv(t))
alice.L2.SetUserEnv(env.L2UserEnv(t))
env.Alice = alice
return return
} }
func (env Env) L1UserEnv(t Testing) *BasicUserEnv[*L1Bindings] {
l1EthCl := env.Miner.EthClient()
return &BasicUserEnv[*L1Bindings]{
EthCl: l1EthCl,
Signer: types.LatestSigner(env.SetupData.L1Cfg.Config),
AddressCorpora: env.AddressCorpora,
Bindings: NewL1Bindings(t, l1EthCl, e2ecfg.AllocTypeStandard),
}
}
func (env Env) L2UserEnv(t Testing) *BasicUserEnv[*L2Bindings] {
l2EthCl := env.SeqEngine.EthClient()
return &BasicUserEnv[*L2Bindings]{
EthCl: l2EthCl,
Signer: types.LatestSigner(env.SetupData.L2Cfg.Config),
AddressCorpora: env.AddressCorpora,
Bindings: NewL2Bindings(t, l2EthCl, env.SeqEngine.GethClient()),
}
}
func (env Env) ActBatchSubmitAllAndMine(t Testing) (l1InclusionBlock *types.Block) { func (env Env) ActBatchSubmitAllAndMine(t Testing) (l1InclusionBlock *types.Block) {
env.Batcher.ActSubmitAll(t) env.Batcher.ActSubmitAll(t)
batchTx := env.Batcher.LastSubmitted batchTx := env.Batcher.LastSubmitted
......
...@@ -146,8 +146,8 @@ func (s *L2Batcher) Reset() { ...@@ -146,8 +146,8 @@ func (s *L2Batcher) Reset() {
// ActL2BatchBuffer adds the next L2 block to the batch buffer. // ActL2BatchBuffer adds the next L2 block to the batch buffer.
// If the buffer is being submitted, the buffer is wiped. // If the buffer is being submitted, the buffer is wiped.
func (s *L2Batcher) ActL2BatchBuffer(t Testing) { func (s *L2Batcher) ActL2BatchBuffer(t Testing, opts ...BlockModifier) {
require.NoError(t, s.Buffer(t), "failed to add block to channel") require.NoError(t, s.Buffer(t, opts...), "failed to add block to channel")
} }
type BlockModifier = func(block *types.Block) type BlockModifier = func(block *types.Block)
......
...@@ -26,14 +26,13 @@ func runBadTxInBatchTest(gt *testing.T, testCfg *helpers.TestCfg[any]) { ...@@ -26,14 +26,13 @@ func runBadTxInBatchTest(gt *testing.T, testCfg *helpers.TestCfg[any]) {
env.Alice.L2.ActCheckReceiptStatusOfLastTx(true)(t) env.Alice.L2.ActCheckReceiptStatusOfLastTx(true)(t)
// Instruct the batcher to submit a faulty channel, with an invalid tx. // Instruct the batcher to submit a faulty channel, with an invalid tx.
err := env.Batcher.Buffer(t, func(block *types.Block) { env.Batcher.ActL2BatchBuffer(t, func(block *types.Block) {
// Replace the tx with one that has a bad signature. // Replace the tx with one that has a bad signature.
txs := block.Transactions() txs := block.Transactions()
newTx, err := txs[1].WithSignature(env.Alice.L2.Signer(), make([]byte, 65)) newTx, err := txs[1].WithSignature(env.Alice.L2.Signer(), make([]byte, 65))
txs[1] = newTx txs[1] = newTx
require.NoError(t, err) require.NoError(t, err)
}) })
require.NoError(t, err)
env.Batcher.ActL2ChannelClose(t) env.Batcher.ActL2ChannelClose(t)
env.Batcher.ActL2BatchSubmit(t) env.Batcher.ActL2BatchSubmit(t)
......
package upgrades package upgrades
import ( import (
"context"
"math/big"
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum-optimism/optimism/op-e2e/actions/helpers" "github.com/ethereum-optimism/optimism/op-e2e/actions/helpers"
"github.com/ethereum-optimism/optimism/op-e2e/system/e2esys" "github.com/ethereum-optimism/optimism/op-e2e/system/e2esys"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/stretchr/testify/require"
) )
func TestHoloceneActivationAtGenesis(gt *testing.T) { func TestHoloceneActivationAtGenesis(gt *testing.T) {
...@@ -127,3 +132,85 @@ func TestHoloceneLateActivationAndReset(gt *testing.T) { ...@@ -127,3 +132,85 @@ func TestHoloceneLateActivationAndReset(gt *testing.T) {
requirePreHoloceneActivationLogs(e2esys.RoleSeq, 4) requirePreHoloceneActivationLogs(e2esys.RoleSeq, 4)
requirePreHoloceneActivationLogs(e2esys.RoleVerif, 4) requirePreHoloceneActivationLogs(e2esys.RoleVerif, 4)
} }
func TestHoloceneInvalidPayload(gt *testing.T) {
t := helpers.NewDefaultTesting(gt)
env := helpers.SetupEnv(t, helpers.WithActiveGenesisFork(rollup.Holocene))
ctx := context.Background()
requireDepositOnlyLogs := func(role string, expNumLogs int) {
t.Helper()
recs := env.Logs.FindLogs(testlog.NewMessageContainsFilter("deposits-only attributes"), testlog.NewAttributesFilter("role", role))
require.Len(t, recs, expNumLogs)
}
// Start op-nodes
env.Seq.ActL2PipelineFull(t)
// generate and batch buffer two empty blocks
env.Seq.ActL2EmptyBlock(t) // 1 - genesis is 0
env.Batcher.ActL2BatchBuffer(t)
env.Seq.ActL2EmptyBlock(t) // 2
env.Batcher.ActL2BatchBuffer(t)
// send and include a single transaction
env.Alice.L2.ActResetTxOpts(t)
env.Alice.L2.ActSetTxToAddr(&env.DeployParams.Addresses.Bob)
env.Alice.L2.ActMakeTx(t)
env.Seq.ActL2StartBlock(t)
env.SeqEngine.ActL2IncludeTx(env.Alice.Address())(t)
env.Seq.ActL2EndBlock(t) // 3
env.Alice.L2.ActCheckReceiptStatusOfLastTx(true)(t)
l2Unsafe := env.Seq.L2Unsafe()
const invalidNum = 3
require.EqualValues(t, invalidNum, l2Unsafe.Number)
b, err := env.SeqEngine.EthClient().BlockByNumber(ctx, big.NewInt(invalidNum))
require.NoError(t, err)
require.Len(t, b.Transactions(), 2)
// buffer into the batcher, invalidating the tx via signature zeroing
env.Batcher.ActL2BatchBuffer(t, func(block *types.Block) {
// Replace the tx with one that has a bad signature.
txs := block.Transactions()
newTx, err := txs[1].WithSignature(env.Alice.L2.Signer(), make([]byte, 65))
require.NoError(t, err)
txs[1] = newTx
})
// generate two more empty blocks
env.Seq.ActL2EmptyBlock(t) // 4
env.Seq.ActL2EmptyBlock(t) // 5
require.EqualValues(t, 5, env.Seq.L2Unsafe().Number)
// submit it all
env.ActBatchSubmitAllAndMine(t)
// derive chain on sequencer
env.Seq.ActL1HeadSignal(t)
env.Seq.ActL2PipelineFull(t)
// TODO(12695): need to properly update safe after completed L1 block derivation
l2Safe := env.Seq.L2PendingSafe()
require.EqualValues(t, invalidNum, l2Safe.Number)
require.NotEqual(t, l2Safe.Hash, l2Unsafe.Hash, // old L2Unsafe above
"block-3 should have been replaced by deposit-only version")
requireDepositOnlyLogs(e2esys.RoleSeq, 2)
require.Equal(t, l2Safe, env.Seq.L2Unsafe(), "unsafe chain should have reorg'd")
b, err = env.SeqEngine.EthClient().BlockByNumber(ctx, big.NewInt(invalidNum))
require.NoError(t, err)
require.Len(t, b.Transactions(), 1)
// test that building on top of reorg'd chain and deriving further works
env.Seq.ActL2EmptyBlock(t) // 4
env.Seq.ActL2EmptyBlock(t) // 5
l2Unsafe = env.Seq.L2Unsafe()
require.EqualValues(t, 5, l2Unsafe.Number)
env.Batcher.Reset() // need to reset batcher to become aware of reorg
env.ActBatchSubmitAllAndMine(t)
env.Seq.ActL1HeadSignal(t)
env.Seq.ActL2PipelineFull(t)
require.Equal(t, l2Unsafe, env.Seq.L2Safe())
}
...@@ -62,6 +62,7 @@ func (eq *AttributesHandler) OnEvent(ev event.Event) bool { ...@@ -62,6 +62,7 @@ func (eq *AttributesHandler) OnEvent(ev event.Event) bool {
eq.onPendingSafeUpdate(x) eq.onPendingSafeUpdate(x)
case derive.DerivedAttributesEvent: case derive.DerivedAttributesEvent:
eq.attributes = x.Attributes eq.attributes = x.Attributes
eq.sentAttributes = false
eq.emitter.Emit(derive.ConfirmReceivedAttributesEvent{}) eq.emitter.Emit(derive.ConfirmReceivedAttributesEvent{})
// to make sure we have a pre-state signal to process the attributes from // to make sure we have a pre-state signal to process the attributes from
eq.emitter.Emit(engine.PendingSafeRequestEvent{}) eq.emitter.Emit(engine.PendingSafeRequestEvent{})
...@@ -71,7 +72,7 @@ func (eq *AttributesHandler) OnEvent(ev event.Event) bool { ...@@ -71,7 +72,7 @@ func (eq *AttributesHandler) OnEvent(ev event.Event) bool {
case rollup.EngineTemporaryErrorEvent: case rollup.EngineTemporaryErrorEvent:
eq.sentAttributes = false eq.sentAttributes = false
case engine.InvalidPayloadAttributesEvent: case engine.InvalidPayloadAttributesEvent:
if x.Attributes.DerivedFrom == (eth.L1BlockRef{}) { if !x.Attributes.IsDerived() {
return true // from sequencing return true // from sequencing
} }
eq.sentAttributes = false eq.sentAttributes = false
......
...@@ -2,6 +2,7 @@ package derive ...@@ -2,6 +2,7 @@ package derive
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"time" "time"
...@@ -35,17 +36,32 @@ type AttributesWithParent struct { ...@@ -35,17 +36,32 @@ type AttributesWithParent struct {
DerivedFrom eth.L1BlockRef DerivedFrom eth.L1BlockRef
} }
// WithDepositsOnly return a shallow clone with all non-Deposit transactions
// stripped from the transactions of its attributes. The order is preserved.
func (a *AttributesWithParent) WithDepositsOnly() *AttributesWithParent {
clone := *a
clone.Attributes = clone.Attributes.WithDepositsOnly()
return &clone
}
func (a *AttributesWithParent) IsDerived() bool {
return a.DerivedFrom != (eth.L1BlockRef{})
}
type AttributesQueue struct { type AttributesQueue struct {
log log.Logger log log.Logger
config *rollup.Config config *rollup.Config
builder AttributesBuilder builder AttributesBuilder
prev SingularBatchProvider prev SingularBatchProvider
batch *SingularBatch batch *SingularBatch
isLastInSpan bool isLastInSpan bool
lastAttribs *AttributesWithParent
} }
type SingularBatchProvider interface { type SingularBatchProvider interface {
ResettableStage ResettableStage
ChannelFlusher
Origin() eth.L1BlockRef Origin() eth.L1BlockRef
NextBatch(context.Context, eth.L2BlockRef) (*SingularBatch, bool, error) NextBatch(context.Context, eth.L2BlockRef) (*SingularBatch, bool, error)
} }
...@@ -85,11 +101,11 @@ func (aq *AttributesQueue) NextAttributes(ctx context.Context, parent eth.L2Bloc ...@@ -85,11 +101,11 @@ func (aq *AttributesQueue) NextAttributes(ctx context.Context, parent eth.L2Bloc
IsLastInSpan: aq.isLastInSpan, IsLastInSpan: aq.isLastInSpan,
DerivedFrom: aq.Origin(), DerivedFrom: aq.Origin(),
} }
aq.lastAttribs = &attr
aq.batch = nil aq.batch = nil
aq.isLastInSpan = false aq.isLastInSpan = false
return &attr, nil return &attr, nil
} }
} }
// createNextAttributes transforms a batch into a payload attributes. This sets `NoTxPool` and appends the batched transactions // createNextAttributes transforms a batch into a payload attributes. This sets `NoTxPool` and appends the batched transactions
...@@ -120,8 +136,35 @@ func (aq *AttributesQueue) createNextAttributes(ctx context.Context, batch *Sing ...@@ -120,8 +136,35 @@ func (aq *AttributesQueue) createNextAttributes(ctx context.Context, batch *Sing
return attrs, nil return attrs, nil
} }
func (aq *AttributesQueue) Reset(ctx context.Context, _ eth.L1BlockRef, _ eth.SystemConfig) error { func (aq *AttributesQueue) reset() {
aq.batch = nil aq.batch = nil
aq.isLastInSpan = false // overwritten later, but set for consistency aq.isLastInSpan = false // overwritten later, but set for consistency
aq.lastAttribs = nil
}
func (aq *AttributesQueue) Reset(ctx context.Context, _ eth.L1BlockRef, _ eth.SystemConfig) error {
aq.reset()
return io.EOF return io.EOF
} }
func (aq *AttributesQueue) DepositsOnlyAttributes(parent eth.BlockID, derivedFrom eth.L1BlockRef) (*AttributesWithParent, error) {
// Sanity checks - these cannot happen with correct deriver implementations.
if aq.batch != nil {
return nil, fmt.Errorf("unexpected buffered batch, parent hash: %s, epoch: %s", aq.batch.ParentHash, aq.batch.Epoch())
} else if aq.lastAttribs == nil {
return nil, errors.New("no attributes generated yet")
} else if derivedFrom != aq.lastAttribs.DerivedFrom {
return nil, fmt.Errorf(
"unexpected derivation origin, last_origin: %s, invalid_origin: %s",
aq.lastAttribs.DerivedFrom, derivedFrom)
} else if parent != aq.lastAttribs.Parent.ID() {
return nil, fmt.Errorf(
"unexpected parent: last_parent: %s, invalid_parent: %s",
aq.lastAttribs.Parent.ID(), parent)
}
aq.prev.FlushChannel() // flush all channel data in previous stages
attrs := aq.lastAttribs.WithDepositsOnly()
aq.lastAttribs = attrs
return attrs, nil
}
...@@ -27,10 +27,6 @@ import ( ...@@ -27,10 +27,6 @@ import (
// It is internally responsible for making sure that batches with L1 inclusions block outside it's // It is internally responsible for making sure that batches with L1 inclusions block outside it's
// working range are not considered or pruned. // working range are not considered or pruned.
type ChannelFlusher interface {
FlushChannel()
}
type NextBatchProvider interface { type NextBatchProvider interface {
ChannelFlusher ChannelFlusher
Origin() eth.L1BlockRef Origin() eth.L1BlockRef
...@@ -271,6 +267,12 @@ func (bq *BatchQueue) Reset(_ context.Context, base eth.L1BlockRef, _ eth.System ...@@ -271,6 +267,12 @@ func (bq *BatchQueue) Reset(_ context.Context, base eth.L1BlockRef, _ eth.System
return io.EOF return io.EOF
} }
func (bq *BatchQueue) FlushChannel() {
// We need to implement the ChannelFlusher interface with the BatchQueue but it's never called
// of which the BatchMux takes care.
panic("BatchQueue: invalid FlushChannel call")
}
func (bq *BatchQueue) AddBatch(ctx context.Context, batch Batch, parent eth.L2BlockRef) { func (bq *BatchQueue) AddBatch(ctx context.Context, batch Batch, parent eth.L2BlockRef) {
if len(bq.l1Blocks) == 0 { if len(bq.l1Blocks) == 0 {
panic(fmt.Errorf("cannot add batch with timestamp %d, no origin was prepared", batch.GetTimestamp())) panic(fmt.Errorf("cannot add batch with timestamp %d, no origin was prepared", batch.GetTimestamp()))
......
...@@ -51,6 +51,10 @@ func (ca *ChannelAssembler) Reset(context.Context, eth.L1BlockRef, eth.SystemCon ...@@ -51,6 +51,10 @@ func (ca *ChannelAssembler) Reset(context.Context, eth.L1BlockRef, eth.SystemCon
return io.EOF return io.EOF
} }
func (ca *ChannelAssembler) FlushChannel() {
ca.resetChannel()
}
func (ca *ChannelAssembler) resetChannel() { func (ca *ChannelAssembler) resetChannel() {
ca.channel = nil ca.channel = nil
} }
......
...@@ -203,6 +203,12 @@ func (cb *ChannelBank) Reset(ctx context.Context, base eth.L1BlockRef, _ eth.Sys ...@@ -203,6 +203,12 @@ func (cb *ChannelBank) Reset(ctx context.Context, base eth.L1BlockRef, _ eth.Sys
return io.EOF return io.EOF
} }
func (bq *ChannelBank) FlushChannel() {
// We need to implement the ChannelFlusher interface with the ChannelBank but it's never called
// of which the ChannelMux takes care.
panic("ChannelBank: invalid FlushChannel call")
}
type L1BlockRefByHashFetcher interface { type L1BlockRefByHashFetcher interface {
L1BlockRefByHash(context.Context, common.Hash) (eth.L1BlockRef, error) L1BlockRefByHash(context.Context, common.Hash) (eth.L1BlockRef, error)
} }
...@@ -32,6 +32,7 @@ var ( ...@@ -32,6 +32,7 @@ var (
type RawChannelProvider interface { type RawChannelProvider interface {
ResettableStage ResettableStage
ChannelFlusher
Origin() eth.L1BlockRef Origin() eth.L1BlockRef
NextRawChannel(ctx context.Context) ([]byte, error) NextRawChannel(ctx context.Context) ([]byte, error)
} }
...@@ -134,5 +135,5 @@ func (cr *ChannelInReader) Reset(ctx context.Context, _ eth.L1BlockRef, _ eth.Sy ...@@ -134,5 +135,5 @@ func (cr *ChannelInReader) Reset(ctx context.Context, _ eth.L1BlockRef, _ eth.Sy
func (cr *ChannelInReader) FlushChannel() { func (cr *ChannelInReader) FlushChannel() {
cr.nextBatchFn = nil cr.nextBatchFn = nil
// TODO(12157): cr.prev.FlushChannel() - when we do wiring with ChannelStage cr.prev.FlushChannel()
} }
...@@ -3,6 +3,7 @@ package derive ...@@ -3,6 +3,7 @@ package derive
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
...@@ -64,6 +65,18 @@ func (ev PipelineStepEvent) String() string { ...@@ -64,6 +65,18 @@ func (ev PipelineStepEvent) String() string {
return "pipeline-step" return "pipeline-step"
} }
// DepositsOnlyPayloadAttributesRequestEvent requests a deposits-only version of the attributes from
// the pipeline. It is sent by the engine deriver and received by the PipelineDeriver.
// This event got introduced with Holocene.
type DepositsOnlyPayloadAttributesRequestEvent struct {
Parent eth.BlockID
DerivedFrom eth.L1BlockRef
}
func (ev DepositsOnlyPayloadAttributesRequestEvent) String() string {
return "deposits-only-payload-attributes-request"
}
type PipelineDeriver struct { type PipelineDeriver struct {
pipeline *DerivationPipeline pipeline *DerivationPipeline
...@@ -122,8 +135,7 @@ func (d *PipelineDeriver) OnEvent(ev event.Event) bool { ...@@ -122,8 +135,7 @@ func (d *PipelineDeriver) OnEvent(ev event.Event) bool {
d.emitter.Emit(rollup.EngineTemporaryErrorEvent{Err: err}) d.emitter.Emit(rollup.EngineTemporaryErrorEvent{Err: err})
} else { } else {
if attrib != nil { if attrib != nil {
d.needAttributesConfirmation = true d.emitDerivedAttributesEvent(attrib)
d.emitter.Emit(DerivedAttributesEvent{Attributes: attrib})
} else { } else {
d.emitter.Emit(DeriverMoreEvent{}) // continue with the next step if we can d.emitter.Emit(DeriverMoreEvent{}) // continue with the next step if we can
} }
...@@ -132,8 +144,21 @@ func (d *PipelineDeriver) OnEvent(ev event.Event) bool { ...@@ -132,8 +144,21 @@ func (d *PipelineDeriver) OnEvent(ev event.Event) bool {
d.pipeline.ConfirmEngineReset() d.pipeline.ConfirmEngineReset()
case ConfirmReceivedAttributesEvent: case ConfirmReceivedAttributesEvent:
d.needAttributesConfirmation = false d.needAttributesConfirmation = false
case DepositsOnlyPayloadAttributesRequestEvent:
d.pipeline.log.Warn("Deriving deposits-only attributes", "origin", d.pipeline.Origin())
attrib, err := d.pipeline.DepositsOnlyAttributes(x.Parent, x.DerivedFrom)
if err != nil {
d.emitter.Emit(rollup.CriticalErrorEvent{Err: fmt.Errorf("deriving deposits-only attributes: %w", err)})
return true
}
d.emitDerivedAttributesEvent(attrib)
default: default:
return false return false
} }
return true return true
} }
func (d *PipelineDeriver) emitDerivedAttributesEvent(attrib *AttributesWithParent) {
d.needAttributesConfirmation = true
d.emitter.Emit(DerivedAttributesEvent{Attributes: attrib})
}
...@@ -38,6 +38,13 @@ type ResettableStage interface { ...@@ -38,6 +38,13 @@ type ResettableStage interface {
Reset(ctx context.Context, base eth.L1BlockRef, baseCfg eth.SystemConfig) error Reset(ctx context.Context, base eth.L1BlockRef, baseCfg eth.SystemConfig) error
} }
// A ChannelFlusher flushes all internal state related to the current channel and then
// calls FlushChannel on the stage it owns. Note that this is in contrast to Reset, which
// is called by the owning Pipeline in a loop over all stages.
type ChannelFlusher interface {
FlushChannel()
}
type ForkTransformer interface { type ForkTransformer interface {
Transform(rollup.ForkName) Transform(rollup.ForkName)
} }
...@@ -89,16 +96,16 @@ func NewDerivationPipeline(log log.Logger, rollupCfg *rollup.Config, l1Fetcher L ...@@ -89,16 +96,16 @@ func NewDerivationPipeline(log log.Logger, rollupCfg *rollup.Config, l1Fetcher L
dataSrc := NewDataSourceFactory(log, rollupCfg, l1Fetcher, l1Blobs, altDA) // auxiliary stage for L1Retrieval dataSrc := NewDataSourceFactory(log, rollupCfg, l1Fetcher, l1Blobs, altDA) // auxiliary stage for L1Retrieval
l1Src := NewL1Retrieval(log, dataSrc, l1Traversal) l1Src := NewL1Retrieval(log, dataSrc, l1Traversal)
frameQueue := NewFrameQueue(log, rollupCfg, l1Src) frameQueue := NewFrameQueue(log, rollupCfg, l1Src)
bank := NewChannelMux(log, spec, frameQueue, metrics) channelMux := NewChannelMux(log, spec, frameQueue, metrics)
chInReader := NewChannelInReader(rollupCfg, log, bank, metrics) chInReader := NewChannelInReader(rollupCfg, log, channelMux, metrics)
batchQueue := NewBatchMux(log, rollupCfg, chInReader, l2Source) batchMux := NewBatchMux(log, rollupCfg, chInReader, l2Source)
attrBuilder := NewFetchingAttributesBuilder(rollupCfg, l1Fetcher, l2Source) attrBuilder := NewFetchingAttributesBuilder(rollupCfg, l1Fetcher, l2Source)
attributesQueue := NewAttributesQueue(log, rollupCfg, attrBuilder, batchQueue) attributesQueue := NewAttributesQueue(log, rollupCfg, attrBuilder, batchMux)
// Reset from ResetEngine then up from L1 Traversal. The stages do not talk to each other during // Reset from ResetEngine then up from L1 Traversal. The stages do not talk to each other during
// the ResetEngine, but after the ResetEngine, this is the order in which the stages could talk to each other. // the ResetEngine, but after the ResetEngine, this is the order in which the stages could talk to each other.
// Note: The ResetEngine is the only reset that can fail. // Note: The ResetEngine is the only reset that can fail.
stages := []ResettableStage{l1Traversal, l1Src, altDA, frameQueue, bank, chInReader, batchQueue, attributesQueue} stages := []ResettableStage{l1Traversal, l1Src, altDA, frameQueue, channelMux, chInReader, batchMux, attributesQueue}
return &DerivationPipeline{ return &DerivationPipeline{
log: log, log: log,
...@@ -127,6 +134,10 @@ func (dp *DerivationPipeline) Reset() { ...@@ -127,6 +134,10 @@ func (dp *DerivationPipeline) Reset() {
dp.engineIsReset = false dp.engineIsReset = false
} }
func (dp *DerivationPipeline) DepositsOnlyAttributes(parent eth.BlockID, derivedFrom eth.L1BlockRef) (*AttributesWithParent, error) {
return dp.attrib.DepositsOnlyAttributes(parent, derivedFrom)
}
// Origin is the L1 block of the inner-most stage of the derivation pipeline, // Origin is the L1 block of the inner-most stage of the derivation pipeline,
// i.e. the L1 chain up to and including this point included and/or produced all the safe L2 blocks. // i.e. the L1 chain up to and including this point included and/or produced all the safe L2 blocks.
func (dp *DerivationPipeline) Origin() eth.L1BlockRef { func (dp *DerivationPipeline) Origin() eth.L1BlockRef {
......
...@@ -3,10 +3,9 @@ package engine ...@@ -3,10 +3,9 @@ package engine
import ( import (
"fmt" "fmt"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum-optimism/optimism/op-node/rollup" "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/derive"
"github.com/ethereum-optimism/optimism/op-service/eth"
) )
// BuildInvalidEvent is an internal engine event, to post-process upon invalid attributes. // BuildInvalidEvent is an internal engine event, to post-process upon invalid attributes.
...@@ -33,20 +32,19 @@ func (ev InvalidPayloadAttributesEvent) String() string { ...@@ -33,20 +32,19 @@ func (ev InvalidPayloadAttributesEvent) String() string {
func (eq *EngDeriver) onBuildInvalid(ev BuildInvalidEvent) { func (eq *EngDeriver) onBuildInvalid(ev BuildInvalidEvent) {
eq.log.Warn("could not process payload attributes", "err", ev.Err) eq.log.Warn("could not process payload attributes", "err", ev.Err)
// Count the number of deposits to see if the tx list is deposit only.
depositCount := 0
for _, tx := range ev.Attributes.Attributes.Transactions {
if len(tx) > 0 && tx[0] == types.DepositTxType {
depositCount += 1
}
}
// Deposit transaction execution errors are suppressed in the execution engine, but if the // Deposit transaction execution errors are suppressed in the execution engine, but if the
// block is somehow invalid, there is nothing we can do to recover & we should exit. // block is somehow invalid, there is nothing we can do to recover & we should exit.
if len(ev.Attributes.Attributes.Transactions) == depositCount { if ev.Attributes.Attributes.IsDepositsOnly() {
eq.log.Error("deposit only block was invalid", "parent", ev.Attributes.Parent, "err", ev.Err) eq.log.Error("deposit only block was invalid", "parent", ev.Attributes.Parent, "err", ev.Err)
eq.emitter.Emit(rollup.CriticalErrorEvent{Err: fmt.Errorf("failed to process block with only deposit transactions: %w", ev.Err)}) eq.emitter.Emit(rollup.CriticalErrorEvent{Err: fmt.Errorf("failed to process block with only deposit transactions: %w", ev.Err)})
return return
} }
if ev.Attributes.IsDerived() && eq.cfg.IsHolocene(ev.Attributes.DerivedFrom.Time) {
eq.emitDepositsOnlyPayloadAttributesRequest(ev.Attributes.Parent.ID(), ev.Attributes.DerivedFrom)
return
}
// Revert the pending safe head to the safe head. // Revert the pending safe head to the safe head.
eq.ec.SetPendingSafeL2Head(eq.ec.SafeL2Head()) eq.ec.SetPendingSafeL2Head(eq.ec.SafeL2Head())
// suppress the error b/c we want to retry with the next batch from the batch queue // suppress the error b/c we want to retry with the next batch from the batch queue
...@@ -61,3 +59,12 @@ func (eq *EngDeriver) onBuildInvalid(ev BuildInvalidEvent) { ...@@ -61,3 +59,12 @@ func (eq *EngDeriver) onBuildInvalid(ev BuildInvalidEvent) {
// Signal that we deemed the attributes as unfit // Signal that we deemed the attributes as unfit
eq.emitter.Emit(InvalidPayloadAttributesEvent(ev)) eq.emitter.Emit(InvalidPayloadAttributesEvent(ev))
} }
func (eq *EngDeriver) emitDepositsOnlyPayloadAttributesRequest(parent eth.BlockID, derivedFrom eth.L1BlockRef) {
eq.log.Warn("Holocene active, requesting deposits-only attributes", "parent", parent, "derived_from", derivedFrom)
// request deposits-only version
eq.emitter.Emit(derive.DepositsOnlyPayloadAttributesRequestEvent{
Parent: parent,
DerivedFrom: derivedFrom,
})
}
...@@ -25,8 +25,7 @@ type Metrics interface { ...@@ -25,8 +25,7 @@ type Metrics interface {
// forkchoice-update event, to signal the latest forkchoice to other derivers. // forkchoice-update event, to signal the latest forkchoice to other derivers.
// This helps decouple derivers from the actual engine state, // This helps decouple derivers from the actual engine state,
// while also not making the derivers wait for a forkchoice update at random. // while also not making the derivers wait for a forkchoice update at random.
type ForkchoiceRequestEvent struct { type ForkchoiceRequestEvent struct{}
}
func (ev ForkchoiceRequestEvent) String() string { func (ev ForkchoiceRequestEvent) String() string {
return "forkchoice-request" return "forkchoice-request"
...@@ -184,8 +183,7 @@ func (ev ProcessAttributesEvent) String() string { ...@@ -184,8 +183,7 @@ func (ev ProcessAttributesEvent) String() string {
return "process-attributes" return "process-attributes"
} }
type PendingSafeRequestEvent struct { type PendingSafeRequestEvent struct{}
}
func (ev PendingSafeRequestEvent) String() string { func (ev PendingSafeRequestEvent) String() string {
return "pending-safe-request" return "pending-safe-request"
...@@ -199,15 +197,13 @@ func (ev ProcessUnsafePayloadEvent) String() string { ...@@ -199,15 +197,13 @@ func (ev ProcessUnsafePayloadEvent) String() string {
return "process-unsafe-payload" return "process-unsafe-payload"
} }
type TryBackupUnsafeReorgEvent struct { type TryBackupUnsafeReorgEvent struct{}
}
func (ev TryBackupUnsafeReorgEvent) String() string { func (ev TryBackupUnsafeReorgEvent) String() string {
return "try-backup-unsafe-reorg" return "try-backup-unsafe-reorg"
} }
type TryUpdateEngineEvent struct { type TryUpdateEngineEvent struct{}
}
func (ev TryUpdateEngineEvent) String() string { func (ev TryUpdateEngineEvent) String() string {
return "try-update-engine" return "try-update-engine"
...@@ -277,7 +273,8 @@ type EngDeriver struct { ...@@ -277,7 +273,8 @@ type EngDeriver struct {
var _ event.Deriver = (*EngDeriver)(nil) var _ event.Deriver = (*EngDeriver)(nil)
func NewEngDeriver(log log.Logger, ctx context.Context, cfg *rollup.Config, func NewEngDeriver(log log.Logger, ctx context.Context, cfg *rollup.Config,
metrics Metrics, ec *EngineController) *EngDeriver { metrics Metrics, ec *EngineController,
) *EngDeriver {
return &EngDeriver{ return &EngDeriver{
log: log, log: log,
cfg: cfg, cfg: cfg,
...@@ -471,10 +468,10 @@ func (d *EngDeriver) OnEvent(ev event.Event) bool { ...@@ -471,10 +468,10 @@ func (d *EngDeriver) OnEvent(ev event.Event) bool {
d.onBuildStart(x) d.onBuildStart(x)
case BuildStartedEvent: case BuildStartedEvent:
d.onBuildStarted(x) d.onBuildStarted(x)
case BuildSealedEvent:
d.onBuildSealed(x)
case BuildSealEvent: case BuildSealEvent:
d.onBuildSeal(x) d.onBuildSeal(x)
case BuildSealedEvent:
d.onBuildSealed(x)
case BuildInvalidEvent: case BuildInvalidEvent:
d.onBuildInvalid(x) d.onBuildInvalid(x)
case BuildCancelEvent: case BuildCancelEvent:
......
...@@ -30,21 +30,31 @@ func (eq *EngDeriver) onPayloadProcess(ev PayloadProcessEvent) { ...@@ -30,21 +30,31 @@ func (eq *EngDeriver) onPayloadProcess(ev PayloadProcessEvent) {
ev.Envelope.ExecutionPayload, ev.Envelope.ParentBeaconBlockRoot) ev.Envelope.ExecutionPayload, ev.Envelope.ParentBeaconBlockRoot)
if err != nil { if err != nil {
eq.emitter.Emit(rollup.EngineTemporaryErrorEvent{ eq.emitter.Emit(rollup.EngineTemporaryErrorEvent{
Err: fmt.Errorf("failed to insert execution payload: %w", err)}) Err: fmt.Errorf("failed to insert execution payload: %w", err),
})
return return
} }
switch status.Status { switch status.Status {
case eth.ExecutionInvalid, eth.ExecutionInvalidBlockHash: case eth.ExecutionInvalid, eth.ExecutionInvalidBlockHash:
// Depending on execution engine, not all block-validity checks run immediately on build-start
// at the time of the forkchoiceUpdated engine-API call, nor during getPayload.
if ev.DerivedFrom != (eth.L1BlockRef{}) && eq.cfg.IsHolocene(ev.DerivedFrom.Time) {
eq.emitDepositsOnlyPayloadAttributesRequest(ev.Ref.ParentID(), ev.DerivedFrom)
return
}
eq.emitter.Emit(PayloadInvalidEvent{ eq.emitter.Emit(PayloadInvalidEvent{
Envelope: ev.Envelope, Envelope: ev.Envelope,
Err: eth.NewPayloadErr(ev.Envelope.ExecutionPayload, status)}) Err: eth.NewPayloadErr(ev.Envelope.ExecutionPayload, status),
})
return return
case eth.ExecutionValid: case eth.ExecutionValid:
eq.emitter.Emit(PayloadSuccessEvent(ev)) eq.emitter.Emit(PayloadSuccessEvent(ev))
return return
default: default:
eq.emitter.Emit(rollup.EngineTemporaryErrorEvent{ eq.emitter.Emit(rollup.EngineTemporaryErrorEvent{
Err: eth.NewPayloadErr(ev.Envelope.ExecutionPayload, status)}) Err: eth.NewPayloadErr(ev.Envelope.ExecutionPayload, status),
})
return return
} }
} }
...@@ -358,6 +358,31 @@ type PayloadAttributes struct { ...@@ -358,6 +358,31 @@ type PayloadAttributes struct {
EIP1559Params *Bytes8 `json:"eip1559Params,omitempty"` EIP1559Params *Bytes8 `json:"eip1559Params,omitempty"`
} }
// IsDepositsOnly returns whether all transactions of the PayloadAttributes are of Deposit
// type. Empty transactions are also considered non-Deposit transactions.
func (a *PayloadAttributes) IsDepositsOnly() bool {
for _, tx := range a.Transactions {
if len(tx) == 0 || tx[0] != types.DepositTxType {
return false
}
}
return true
}
// WithDepositsOnly return a shallow clone with all non-Deposit transactions stripped from its
// transactions. The order is preserved.
func (a *PayloadAttributes) WithDepositsOnly() *PayloadAttributes {
clone := *a
depositTxs := make([]Data, 0, len(a.Transactions))
for _, tx := range a.Transactions {
if len(tx) > 0 && tx[0] == types.DepositTxType {
depositTxs = append(depositTxs, tx)
}
}
clone.Transactions = depositTxs
return &clone
}
type ExecutePayloadStatus string type ExecutePayloadStatus string
const ( const (
......
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