From 586ab88da3cc0b0123e987c58bf5ed92a946b029 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 4 Sep 2025 13:46:25 +0200 Subject: [PATCH 1/6] feat(op-node): UnsafeAllowOldPayloads flag Such flags allows to gossip and receive unsafe payloads which timestamp is older than 60 seconds. Useful for testing purposes. --- op-node/flags/flags.go | 10 ++++++++ op-node/node/config.go | 1 + op-node/node/node.go | 9 ++++--- op-node/node/runtime_config.go | 19 ++++++++++----- op-node/p2p/gossip.go | 33 ++++++++++++++------------ op-node/service.go | 3 ++- op-service/testutils/runtime_config.go | 5 ++++ 7 files changed, 53 insertions(+), 27 deletions(-) diff --git a/op-node/flags/flags.go b/op-node/flags/flags.go index c45b72a9e2cc7..b94387f6cb421 100644 --- a/op-node/flags/flags.go +++ b/op-node/flags/flags.go @@ -455,6 +455,15 @@ var ( Category: RollupCategory, Hidden: true, } + /* Unsafe flags */ + UnsafeAllowOldPayloads = &cli.BoolFlag{ + Name: "unsafe-allow-old-payloads", + Usage: "Allows to gossip and receive over p2p unsafe payloads whose timestamp is older 60 seconds compared to now.", + EnvVars: prefixEnvVars("UNSAFE_ALLOW_OLD_PAYLOADS"), + Value: false, + Category: RollupCategory, + Hidden: false, + } ) var requiredFlags = []cli.Flag{ @@ -512,6 +521,7 @@ var optionalFlags = []cli.Flag{ InteropRPCPort, InteropJWTSecret, IgnoreMissingPectraBlobSchedule, + UnsafeAllowOldPayloads, } var DeprecatedFlags = []cli.Flag{ diff --git a/op-node/node/config.go b/op-node/node/config.go index c525fb3b2fb80..af570bcdacf82 100644 --- a/op-node/node/config.go +++ b/op-node/node/config.go @@ -79,6 +79,7 @@ type Config struct { IgnoreMissingPectraBlobSchedule bool FetchWithdrawalRootFromState bool + UnsafeAllowOldPayloads bool } // ConductorRPCFunc retrieves the endpoint. The RPC may not immediately be available. diff --git a/op-node/node/node.go b/op-node/node/node.go index 8b0d8eb055a31..ec2479be8f326 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -4,12 +4,13 @@ import ( "context" "errors" "fmt" - "github.com/ethereum/go-ethereum/common" "io" gosync "sync" "sync/atomic" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/hashicorp/go-multierror" "github.com/libp2p/go-libp2p/core/peer" @@ -271,7 +272,7 @@ func (n *OpNode) initRegistry(ctx context.Context, cfg *Config) error { func (n *OpNode) initRuntimeConfig(ctx context.Context, cfg *Config) error { // attempt to load runtime config, repeat N times - n.runCfg = NewRuntimeConfig(n.log, n.l1Source, &cfg.Rollup, n.registrySource) + n.runCfg = NewRuntimeConfig(n.log, n.l1Source, &cfg.Rollup, n.registrySource, cfg.UnsafeAllowOldPayloads) confDepth := cfg.Driver.VerifierConfDepth reload := func(ctx context.Context) (eth.L1BlockRef, error) { @@ -688,7 +689,6 @@ func (n *OpNode) PublishNewFrag(ctx context.Context, from peer.ID, frag *eth.Sig } func (n *OpNode) PublishSealFrag(ctx context.Context, from peer.ID, seal *eth.SignedSeal) error { - n.tracer.OnPublishSealFrag(ctx, from, seal) // publish to p2p, if we are running p2p at all @@ -704,7 +704,6 @@ func (n *OpNode) PublishSealFrag(ctx context.Context, from peer.ID, seal *eth.Si } func (n *OpNode) PublishEnv(ctx context.Context, from peer.ID, env *eth.SignedEnv) error { - n.tracer.OnPublishEnv(ctx, from, env) // publish to p2p, if we are running p2p at all @@ -783,7 +782,7 @@ func (n *OpNode) OnEnv(ctx context.Context, from peer.ID, env *eth.SignedEnv) er func (n *OpNode) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) error { if p2pNode := n.getP2PNodeIfEnabled(); p2pNode != nil && p2pNode.AltSyncEnabled() { - if unixTimeStale(start.Time, 12*time.Hour) { + if !n.cfg.UnsafeAllowOldPayloads && unixTimeStale(start.Time, 12*time.Hour) { n.log.Debug( "ignoring request to sync L2 range, timestamp is too old for p2p", "start", start, diff --git a/op-node/node/runtime_config.go b/op-node/node/runtime_config.go index d7fdd44eee6c4..3916bcaefeaba 100644 --- a/op-node/node/runtime_config.go +++ b/op-node/node/runtime_config.go @@ -60,6 +60,8 @@ type RuntimeConfig struct { // if this is invalidated with a reorg the data will have to be reloaded. l1Ref eth.L1BlockRef + unsafeAllowOldPayloads bool + runtimeConfigData } @@ -74,13 +76,14 @@ type runtimeConfigData struct { var _ p2p.GossipRuntimeConfig = (*RuntimeConfig)(nil) -func NewRuntimeConfig(log log.Logger, l1Client RuntimeCfgL1Source, rollupCfg *rollup.Config, registryClient RuntimeCfgRegistrySource) *RuntimeConfig { +func NewRuntimeConfig(log log.Logger, l1Client RuntimeCfgL1Source, rollupCfg *rollup.Config, registryClient RuntimeCfgRegistrySource, unsafeAllowOldPayloads bool) *RuntimeConfig { return &RuntimeConfig{ - log: log, - l1Client: l1Client, - rollupCfg: rollupCfg, - registryClient: registryClient, - runtimeConfigData: runtimeConfigData{}, + log: log, + l1Client: l1Client, + rollupCfg: rollupCfg, + registryClient: registryClient, + runtimeConfigData: runtimeConfigData{}, + unsafeAllowOldPayloads: unsafeAllowOldPayloads, } } @@ -99,6 +102,10 @@ func (r *RuntimeConfig) GatewayForBlock(ctx context.Context, blockNumber uint64) return addr, nil } +func (r *RuntimeConfig) UnsafeAllowOldPayloads() bool { + return r.unsafeAllowOldPayloads +} + func (r *RuntimeConfig) FetchNextNGateways(ctx context.Context, n uint64, maxRetries uint64) error { return r.registryClient.FetchNextNGateways(ctx, n, maxRetries) } diff --git a/op-node/p2p/gossip.go b/op-node/p2p/gossip.go index 64d8fdbf9c53e..ad6ba430b74d8 100644 --- a/op-node/p2p/gossip.go +++ b/op-node/p2p/gossip.go @@ -7,10 +7,11 @@ import ( "encoding/binary" "errors" "fmt" - "github.com/ethereum/go-ethereum/crypto" "sync" "time" + "github.com/ethereum/go-ethereum/crypto" + "github.com/golang/snappy" lru "github.com/hashicorp/golang-lru/v2" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -49,8 +50,10 @@ const ( // Message domains, the msg id function uncompresses to keep data monomorphic, // but invalid compressed data will need a unique different id. -var MessageDomainInvalidSnappy = [4]byte{0, 0, 0, 0} -var MessageDomainValidSnappy = [4]byte{1, 0, 0, 0} +var ( + MessageDomainInvalidSnappy = [4]byte{0, 0, 0, 0} + MessageDomainValidSnappy = [4]byte{1, 0, 0, 0} +) type GossipSetupConfigurables interface { PeerScoringParams() *ScoringParams @@ -62,6 +65,7 @@ type GossipRuntimeConfig interface { P2PSequencerAddress() common.Address GatewayForBlock(ctx context.Context, blockNumber uint64) (common.Address, error) FetchNextNGateways(ctx context.Context, n uint64, maxRetries uint64) error + UnsafeAllowOldPayloads() bool } //go:generate mockery --name GossipMetricer @@ -404,7 +408,6 @@ func verifyGatewaySignature(log log.Logger, signatureBytes []byte, messageBytes } func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRuntimeConfig, blockVersion eth.BlockVersion) pubsub.ValidatorEx { - // Seen block hashes per block height // uint64 -> *seenBlocks blockHeightLRU, err := lru.New[uint64, *seenBlocks](1000) @@ -472,10 +475,12 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti // rounding down to seconds is fine here. now := uint64(time.Now().Unix()) - // [REJECT] if the `payload.timestamp` is older than 60 seconds in the past - if uint64(payload.Timestamp) < now-60 { - log.Warn("payload is too old", "timestamp", uint64(payload.Timestamp)) - return pubsub.ValidationReject + if !runCfg.UnsafeAllowOldPayloads() { + // [REJECT] if the `payload.timestamp` is older than 60 seconds in the past + if uint64(payload.Timestamp) < now-60 { + log.Warn("payload is too old", "timestamp", uint64(payload.Timestamp)) + return pubsub.ValidationReject + } } // [REJECT] if the `payload.timestamp` is more than 5 seconds into the future @@ -703,7 +708,7 @@ type publisher struct { var _ GossipOut = (*publisher)(nil) func combinePeers(allPeers ...[]peer.ID) []peer.ID { - var seen = make(map[peer.ID]bool) + seen := make(map[peer.ID]bool) var res []peer.ID for _, peers := range allPeers { for _, p := range peers { @@ -933,7 +938,6 @@ func newNewFragTopic(ctx context.Context, topicId string, ps *pubsub.PubSub, log validator, pubsub.WithValidatorTimeout(3*time.Second), pubsub.WithValidatorConcurrency(4)) - if err != nil { return nil, fmt.Errorf("failed to register gossip topic: %w", err) } @@ -971,7 +975,6 @@ func sealFragFragTopic(ctx context.Context, topicId string, ps *pubsub.PubSub, l validator, pubsub.WithValidatorTimeout(3*time.Second), pubsub.WithValidatorConcurrency(4)) - if err != nil { return nil, fmt.Errorf("failed to register gossip topic: %w", err) } @@ -1009,7 +1012,6 @@ func newEnvTopic(ctx context.Context, topicId string, ps *pubsub.PubSub, log log validator, pubsub.WithValidatorTimeout(3*time.Second), pubsub.WithValidatorConcurrency(4)) - if err != nil { return nil, fmt.Errorf("failed to register gossip topic: %w", err) } @@ -1047,7 +1049,6 @@ func newBlockTopic(ctx context.Context, topicId string, ps *pubsub.PubSub, log l validator, pubsub.WithValidatorTimeout(3*time.Second), pubsub.WithValidatorConcurrency(4)) - if err != nil { return nil, fmt.Errorf("failed to register gossip topic: %w", err) } @@ -1080,8 +1081,10 @@ func newBlockTopic(ctx context.Context, topicId string, ps *pubsub.PubSub, log l }, nil } -type TopicSubscriber func(ctx context.Context, sub *pubsub.Subscription) -type MessageHandler func(ctx context.Context, from peer.ID, msg any) error +type ( + TopicSubscriber func(ctx context.Context, sub *pubsub.Subscription) + MessageHandler func(ctx context.Context, from peer.ID, msg any) error +) func NewFragHandler(onNewFrag func(ctx context.Context, from peer.ID, msg *eth.SignedNewFrag) error) MessageHandler { return func(ctx context.Context, from peer.ID, msg any) error { diff --git a/op-node/service.go b/op-node/service.go index fac52eb43eedc..54c9307b92bd2 100644 --- a/op-node/service.go +++ b/op-node/service.go @@ -85,7 +85,7 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) { conductorRPCEndpoint := ctx.String(flags.ConductorRpcFlag.Name) cfg := &node.Config{ L1: l1Endpoint, - Registry: registryEndpoint, + Registry: registryEndpoint, L2: l2Endpoint, Rollup: *rollupConfig, Driver: *driverConfig, @@ -122,6 +122,7 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) { IgnoreMissingPectraBlobSchedule: ctx.Bool(flags.IgnoreMissingPectraBlobSchedule.Name), FetchWithdrawalRootFromState: ctx.Bool(flags.FetchWithdrawalRootFromState.Name), + UnsafeAllowOldPayloads: ctx.Bool(flags.UnsafeAllowOldPayloads.Name), } if err := cfg.LoadPersisted(log); err != nil { diff --git a/op-service/testutils/runtime_config.go b/op-service/testutils/runtime_config.go index e3d3541d1f99e..111c0a03f103b 100644 --- a/op-service/testutils/runtime_config.go +++ b/op-service/testutils/runtime_config.go @@ -4,6 +4,7 @@ import ( "context" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" ) type MockRuntimeConfig struct { @@ -22,3 +23,7 @@ func (m *MockRuntimeConfig) GatewayForBlock(ctx context.Context, blockNumber uin func (m *MockRuntimeConfig) FetchNextNGateways(ctx context.Context, n uint64, maxRetries uint64) error { return nil } + +func (m *MockRuntimeConfig) UnsafeAllowOldPayloads(timestamp hexutil.Uint64) bool { + return false +} From 6ff9d7320fd06f41c0350f9c4e59fe6d126987b4 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Tue, 9 Sep 2025 21:28:53 +0200 Subject: [PATCH 2/6] feat(op-node): UnsafeIsChainReplication flag --- op-node/flags/flags.go | 10 +++++----- op-node/node/config.go | 2 +- op-node/node/node.go | 16 ++++++++++++++-- op-node/node/runtime_config.go | 10 +++++----- op-node/p2p/gossip.go | 4 ++-- op-node/rollup/driver/state.go | 3 ++- op-node/service.go | 2 +- op-service/testutils/runtime_config.go | 3 +-- 8 files changed, 31 insertions(+), 19 deletions(-) diff --git a/op-node/flags/flags.go b/op-node/flags/flags.go index b94387f6cb421..7ca19a3b967a8 100644 --- a/op-node/flags/flags.go +++ b/op-node/flags/flags.go @@ -456,10 +456,10 @@ var ( Hidden: true, } /* Unsafe flags */ - UnsafeAllowOldPayloads = &cli.BoolFlag{ - Name: "unsafe-allow-old-payloads", - Usage: "Allows to gossip and receive over p2p unsafe payloads whose timestamp is older 60 seconds compared to now.", - EnvVars: prefixEnvVars("UNSAFE_ALLOW_OLD_PAYLOADS"), + UnsafeIsChainReplication = &cli.BoolFlag{ + Name: "unsafe-is-chain-replication", + Usage: "Whether the node is running in chain replication testing mode", + EnvVars: prefixEnvVars("UNSAFE_IS_CHAIN_REPLICATION"), Value: false, Category: RollupCategory, Hidden: false, @@ -521,7 +521,7 @@ var optionalFlags = []cli.Flag{ InteropRPCPort, InteropJWTSecret, IgnoreMissingPectraBlobSchedule, - UnsafeAllowOldPayloads, + UnsafeIsChainReplication, } var DeprecatedFlags = []cli.Flag{ diff --git a/op-node/node/config.go b/op-node/node/config.go index af570bcdacf82..3fdfaf8eef4e5 100644 --- a/op-node/node/config.go +++ b/op-node/node/config.go @@ -79,7 +79,7 @@ type Config struct { IgnoreMissingPectraBlobSchedule bool FetchWithdrawalRootFromState bool - UnsafeAllowOldPayloads bool + UnsafeIsChainReplication bool } // ConductorRPCFunc retrieves the endpoint. The RPC may not immediately be available. diff --git a/op-node/node/node.go b/op-node/node/node.go index ec2479be8f326..91d54cf67c3a7 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -272,7 +272,7 @@ func (n *OpNode) initRegistry(ctx context.Context, cfg *Config) error { func (n *OpNode) initRuntimeConfig(ctx context.Context, cfg *Config) error { // attempt to load runtime config, repeat N times - n.runCfg = NewRuntimeConfig(n.log, n.l1Source, &cfg.Rollup, n.registrySource, cfg.UnsafeAllowOldPayloads) + n.runCfg = NewRuntimeConfig(n.log, n.l1Source, &cfg.Rollup, n.registrySource, cfg.UnsafeIsChainReplication) confDepth := cfg.Driver.VerifierConfDepth reload := func(ctx context.Context) (eth.L1BlockRef, error) { @@ -621,6 +621,10 @@ func (n *OpNode) onEvent(ev event.Event) bool { } func (n *OpNode) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) { + // CHANGE(thedevbirb): allow chain replication without deviation due to L1 state. + if n.runCfg.unsafeChainReplication { + return + } n.tracer.OnNewL1Head(ctx, sig) if n.l2Driver == nil { @@ -635,6 +639,10 @@ func (n *OpNode) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) { } func (n *OpNode) OnNewL1Safe(ctx context.Context, sig eth.L1BlockRef) { + // CHANGE(thedevbirb): allow chain replication without deviation due to L1 state. + if n.runCfg.unsafeChainReplication { + return + } if n.l2Driver == nil { return } @@ -647,6 +655,10 @@ func (n *OpNode) OnNewL1Safe(ctx context.Context, sig eth.L1BlockRef) { } func (n *OpNode) OnNewL1Finalized(ctx context.Context, sig eth.L1BlockRef) { + // CHANGE(thedevbirb): allow chain replication without deviation due to L1 state. + if n.runCfg.unsafeChainReplication { + return + } if n.l2Driver == nil { return } @@ -782,7 +794,7 @@ func (n *OpNode) OnEnv(ctx context.Context, from peer.ID, env *eth.SignedEnv) er func (n *OpNode) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) error { if p2pNode := n.getP2PNodeIfEnabled(); p2pNode != nil && p2pNode.AltSyncEnabled() { - if !n.cfg.UnsafeAllowOldPayloads && unixTimeStale(start.Time, 12*time.Hour) { + if !n.runCfg.unsafeChainReplication && unixTimeStale(start.Time, 12*time.Hour) { n.log.Debug( "ignoring request to sync L2 range, timestamp is too old for p2p", "start", start, diff --git a/op-node/node/runtime_config.go b/op-node/node/runtime_config.go index 3916bcaefeaba..11acbf1b547bf 100644 --- a/op-node/node/runtime_config.go +++ b/op-node/node/runtime_config.go @@ -60,7 +60,7 @@ type RuntimeConfig struct { // if this is invalidated with a reorg the data will have to be reloaded. l1Ref eth.L1BlockRef - unsafeAllowOldPayloads bool + unsafeChainReplication bool runtimeConfigData } @@ -76,14 +76,14 @@ type runtimeConfigData struct { var _ p2p.GossipRuntimeConfig = (*RuntimeConfig)(nil) -func NewRuntimeConfig(log log.Logger, l1Client RuntimeCfgL1Source, rollupCfg *rollup.Config, registryClient RuntimeCfgRegistrySource, unsafeAllowOldPayloads bool) *RuntimeConfig { +func NewRuntimeConfig(log log.Logger, l1Client RuntimeCfgL1Source, rollupCfg *rollup.Config, registryClient RuntimeCfgRegistrySource, UnsafeIsChainReplication bool) *RuntimeConfig { return &RuntimeConfig{ log: log, l1Client: l1Client, rollupCfg: rollupCfg, registryClient: registryClient, runtimeConfigData: runtimeConfigData{}, - unsafeAllowOldPayloads: unsafeAllowOldPayloads, + unsafeChainReplication: UnsafeIsChainReplication, } } @@ -102,8 +102,8 @@ func (r *RuntimeConfig) GatewayForBlock(ctx context.Context, blockNumber uint64) return addr, nil } -func (r *RuntimeConfig) UnsafeAllowOldPayloads() bool { - return r.unsafeAllowOldPayloads +func (r *RuntimeConfig) UnsafeIsChainReplication() bool { + return r.unsafeChainReplication } func (r *RuntimeConfig) FetchNextNGateways(ctx context.Context, n uint64, maxRetries uint64) error { diff --git a/op-node/p2p/gossip.go b/op-node/p2p/gossip.go index ad6ba430b74d8..9099901a6ed7b 100644 --- a/op-node/p2p/gossip.go +++ b/op-node/p2p/gossip.go @@ -65,7 +65,7 @@ type GossipRuntimeConfig interface { P2PSequencerAddress() common.Address GatewayForBlock(ctx context.Context, blockNumber uint64) (common.Address, error) FetchNextNGateways(ctx context.Context, n uint64, maxRetries uint64) error - UnsafeAllowOldPayloads() bool + UnsafeIsChainReplication() bool } //go:generate mockery --name GossipMetricer @@ -475,7 +475,7 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti // rounding down to seconds is fine here. now := uint64(time.Now().Unix()) - if !runCfg.UnsafeAllowOldPayloads() { + if !runCfg.UnsafeIsChainReplication() { // [REJECT] if the `payload.timestamp` is older than 60 seconds in the past if uint64(payload.Timestamp) < now-60 { log.Warn("payload is too old", "timestamp", uint64(payload.Timestamp)) diff --git a/op-node/rollup/driver/state.go b/op-node/rollup/driver/state.go index d4787d886b954..b4fb76afff067 100644 --- a/op-node/rollup/driver/state.go +++ b/op-node/rollup/driver/state.go @@ -449,7 +449,8 @@ func (s *SyncDeriver) SyncStep() { return false } else { s.Emitter.Emit(rollup.CriticalErrorEvent{ - Err: fmt.Errorf("unexpected error on SyncStep event Drain: %w", err)}) + Err: fmt.Errorf("unexpected error on SyncStep event Drain: %w", err), + }) return false } } diff --git a/op-node/service.go b/op-node/service.go index 54c9307b92bd2..515f85cfd2a65 100644 --- a/op-node/service.go +++ b/op-node/service.go @@ -122,7 +122,7 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) { IgnoreMissingPectraBlobSchedule: ctx.Bool(flags.IgnoreMissingPectraBlobSchedule.Name), FetchWithdrawalRootFromState: ctx.Bool(flags.FetchWithdrawalRootFromState.Name), - UnsafeAllowOldPayloads: ctx.Bool(flags.UnsafeAllowOldPayloads.Name), + UnsafeIsChainReplication: ctx.Bool(flags.UnsafeIsChainReplication.Name), } if err := cfg.LoadPersisted(log); err != nil { diff --git a/op-service/testutils/runtime_config.go b/op-service/testutils/runtime_config.go index 111c0a03f103b..fefb633651cff 100644 --- a/op-service/testutils/runtime_config.go +++ b/op-service/testutils/runtime_config.go @@ -4,7 +4,6 @@ import ( "context" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" ) type MockRuntimeConfig struct { @@ -24,6 +23,6 @@ func (m *MockRuntimeConfig) FetchNextNGateways(ctx context.Context, n uint64, ma return nil } -func (m *MockRuntimeConfig) UnsafeAllowOldPayloads(timestamp hexutil.Uint64) bool { +func (m *MockRuntimeConfig) UnsafeIsChainReplication() bool { return false } From 38d2185644e9e9d4b376d2c870bd9d59493fb4e3 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Tue, 9 Sep 2025 22:15:58 +0200 Subject: [PATCH 3/6] test: disable derivation pipeline step --- op-node/flags/flags.go | 5 +++-- op-node/rollup/derive/pipeline.go | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/op-node/flags/flags.go b/op-node/flags/flags.go index 7ca19a3b967a8..dffb8451b5d4e 100644 --- a/op-node/flags/flags.go +++ b/op-node/flags/flags.go @@ -457,8 +457,9 @@ var ( } /* Unsafe flags */ UnsafeIsChainReplication = &cli.BoolFlag{ - Name: "unsafe-is-chain-replication", - Usage: "Whether the node is running in chain replication testing mode", + Name: "unsafe-is-chain-replication", + Usage: "Whether the node is running in chain replication mode, to replay past L2 blocks. Such flag disables some internal mechanisms, " + + "like enforcing payloads not older than 60 seconds and syncing and deriving the chain via L1 data.", EnvVars: prefixEnvVars("UNSAFE_IS_CHAIN_REPLICATION"), Value: false, Category: RollupCategory, diff --git a/op-node/rollup/derive/pipeline.go b/op-node/rollup/derive/pipeline.go index fa2f04dba8e2d..9b7ddd9ff3468 100644 --- a/op-node/rollup/derive/pipeline.go +++ b/op-node/rollup/derive/pipeline.go @@ -174,6 +174,9 @@ func (dp *DerivationPipeline) Step(ctx context.Context, pendingSafeHead eth.L2Bl } }() + // CHANGE(thedevbirb): disable it for chain replication + return nil, io.EOF + // if any stages need to be reset, do that first. if dp.resetting < len(dp.stages) { if !dp.engineIsReset { From 4962e9bd6133355d80d614857aece57ab21ceba8 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 10 Sep 2025 09:43:50 +0200 Subject: [PATCH 4/6] feat(op-node): use BOP_REPLAY flag to modify behaviour for chain replication --- op-node/flags/flags.go | 11 ----------- op-node/node/config.go | 1 - op-node/node/node.go | 15 +++++++++------ op-node/node/runtime_config.go | 19 ++++++------------- op-node/p2p/gossip.go | 5 +++-- op-node/rollup/derive/pipeline.go | 7 +++++-- op-node/rollup/sync/config.go | 6 ++++-- op-node/service.go | 1 - op-service/testutils/runtime_config.go | 4 ---- 9 files changed, 27 insertions(+), 42 deletions(-) diff --git a/op-node/flags/flags.go b/op-node/flags/flags.go index dffb8451b5d4e..c45b72a9e2cc7 100644 --- a/op-node/flags/flags.go +++ b/op-node/flags/flags.go @@ -455,16 +455,6 @@ var ( Category: RollupCategory, Hidden: true, } - /* Unsafe flags */ - UnsafeIsChainReplication = &cli.BoolFlag{ - Name: "unsafe-is-chain-replication", - Usage: "Whether the node is running in chain replication mode, to replay past L2 blocks. Such flag disables some internal mechanisms, " + - "like enforcing payloads not older than 60 seconds and syncing and deriving the chain via L1 data.", - EnvVars: prefixEnvVars("UNSAFE_IS_CHAIN_REPLICATION"), - Value: false, - Category: RollupCategory, - Hidden: false, - } ) var requiredFlags = []cli.Flag{ @@ -522,7 +512,6 @@ var optionalFlags = []cli.Flag{ InteropRPCPort, InteropJWTSecret, IgnoreMissingPectraBlobSchedule, - UnsafeIsChainReplication, } var DeprecatedFlags = []cli.Flag{ diff --git a/op-node/node/config.go b/op-node/node/config.go index 3fdfaf8eef4e5..c525fb3b2fb80 100644 --- a/op-node/node/config.go +++ b/op-node/node/config.go @@ -79,7 +79,6 @@ type Config struct { IgnoreMissingPectraBlobSchedule bool FetchWithdrawalRootFromState bool - UnsafeIsChainReplication bool } // ConductorRPCFunc retrieves the endpoint. The RPC may not immediately be available. diff --git a/op-node/node/node.go b/op-node/node/node.go index 91d54cf67c3a7..c90480b63ca87 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "os" gosync "sync" "sync/atomic" "time" @@ -272,7 +273,7 @@ func (n *OpNode) initRegistry(ctx context.Context, cfg *Config) error { func (n *OpNode) initRuntimeConfig(ctx context.Context, cfg *Config) error { // attempt to load runtime config, repeat N times - n.runCfg = NewRuntimeConfig(n.log, n.l1Source, &cfg.Rollup, n.registrySource, cfg.UnsafeIsChainReplication) + n.runCfg = NewRuntimeConfig(n.log, n.l1Source, &cfg.Rollup, n.registrySource) confDepth := cfg.Driver.VerifierConfDepth reload := func(ctx context.Context) (eth.L1BlockRef, error) { @@ -622,7 +623,7 @@ func (n *OpNode) onEvent(ev event.Event) bool { func (n *OpNode) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) { // CHANGE(thedevbirb): allow chain replication without deviation due to L1 state. - if n.runCfg.unsafeChainReplication { + if _, ok := os.LookupEnv("BOP_REPLAY"); ok { return } n.tracer.OnNewL1Head(ctx, sig) @@ -640,7 +641,7 @@ func (n *OpNode) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) { func (n *OpNode) OnNewL1Safe(ctx context.Context, sig eth.L1BlockRef) { // CHANGE(thedevbirb): allow chain replication without deviation due to L1 state. - if n.runCfg.unsafeChainReplication { + if _, ok := os.LookupEnv("BOP_REPLAY"); ok { return } if n.l2Driver == nil { @@ -656,7 +657,7 @@ func (n *OpNode) OnNewL1Safe(ctx context.Context, sig eth.L1BlockRef) { func (n *OpNode) OnNewL1Finalized(ctx context.Context, sig eth.L1BlockRef) { // CHANGE(thedevbirb): allow chain replication without deviation due to L1 state. - if n.runCfg.unsafeChainReplication { + if _, ok := os.LookupEnv("BOP_REPLAY"); ok { return } if n.l2Driver == nil { @@ -793,8 +794,10 @@ func (n *OpNode) OnEnv(ctx context.Context, from peer.ID, env *eth.SignedEnv) er } func (n *OpNode) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) error { - if p2pNode := n.getP2PNodeIfEnabled(); p2pNode != nil && p2pNode.AltSyncEnabled() { - if !n.runCfg.unsafeChainReplication && unixTimeStale(start.Time, 12*time.Hour) { + // CHANGE(thedevbirb): for chain replication, ignoring sending p2p syncing requests which may block the event loop. + _, isReplay := os.LookupEnv("BOP_REPLAY") + if p2pNode := n.getP2PNodeIfEnabled(); p2pNode != nil && p2pNode.AltSyncEnabled() && !isReplay { + if unixTimeStale(start.Time, 12*time.Hour) { n.log.Debug( "ignoring request to sync L2 range, timestamp is too old for p2p", "start", start, diff --git a/op-node/node/runtime_config.go b/op-node/node/runtime_config.go index 11acbf1b547bf..d7fdd44eee6c4 100644 --- a/op-node/node/runtime_config.go +++ b/op-node/node/runtime_config.go @@ -60,8 +60,6 @@ type RuntimeConfig struct { // if this is invalidated with a reorg the data will have to be reloaded. l1Ref eth.L1BlockRef - unsafeChainReplication bool - runtimeConfigData } @@ -76,14 +74,13 @@ type runtimeConfigData struct { var _ p2p.GossipRuntimeConfig = (*RuntimeConfig)(nil) -func NewRuntimeConfig(log log.Logger, l1Client RuntimeCfgL1Source, rollupCfg *rollup.Config, registryClient RuntimeCfgRegistrySource, UnsafeIsChainReplication bool) *RuntimeConfig { +func NewRuntimeConfig(log log.Logger, l1Client RuntimeCfgL1Source, rollupCfg *rollup.Config, registryClient RuntimeCfgRegistrySource) *RuntimeConfig { return &RuntimeConfig{ - log: log, - l1Client: l1Client, - rollupCfg: rollupCfg, - registryClient: registryClient, - runtimeConfigData: runtimeConfigData{}, - unsafeChainReplication: UnsafeIsChainReplication, + log: log, + l1Client: l1Client, + rollupCfg: rollupCfg, + registryClient: registryClient, + runtimeConfigData: runtimeConfigData{}, } } @@ -102,10 +99,6 @@ func (r *RuntimeConfig) GatewayForBlock(ctx context.Context, blockNumber uint64) return addr, nil } -func (r *RuntimeConfig) UnsafeIsChainReplication() bool { - return r.unsafeChainReplication -} - func (r *RuntimeConfig) FetchNextNGateways(ctx context.Context, n uint64, maxRetries uint64) error { return r.registryClient.FetchNextNGateways(ctx, n, maxRetries) } diff --git a/op-node/p2p/gossip.go b/op-node/p2p/gossip.go index 9099901a6ed7b..26e54ff34c7c6 100644 --- a/op-node/p2p/gossip.go +++ b/op-node/p2p/gossip.go @@ -7,6 +7,7 @@ import ( "encoding/binary" "errors" "fmt" + "os" "sync" "time" @@ -65,7 +66,6 @@ type GossipRuntimeConfig interface { P2PSequencerAddress() common.Address GatewayForBlock(ctx context.Context, blockNumber uint64) (common.Address, error) FetchNextNGateways(ctx context.Context, n uint64, maxRetries uint64) error - UnsafeIsChainReplication() bool } //go:generate mockery --name GossipMetricer @@ -475,7 +475,8 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti // rounding down to seconds is fine here. now := uint64(time.Now().Unix()) - if !runCfg.UnsafeIsChainReplication() { + // CHANGE(thedevbirb): for chain replication, allow old blocks. + if _, ok := os.LookupEnv("BOP_REPLAY"); !ok { // [REJECT] if the `payload.timestamp` is older than 60 seconds in the past if uint64(payload.Timestamp) < now-60 { log.Warn("payload is too old", "timestamp", uint64(payload.Timestamp)) diff --git a/op-node/rollup/derive/pipeline.go b/op-node/rollup/derive/pipeline.go index 9b7ddd9ff3468..3e5ef10eb987f 100644 --- a/op-node/rollup/derive/pipeline.go +++ b/op-node/rollup/derive/pipeline.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "os" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" @@ -174,8 +175,10 @@ func (dp *DerivationPipeline) Step(ctx context.Context, pendingSafeHead eth.L2Bl } }() - // CHANGE(thedevbirb): disable it for chain replication - return nil, io.EOF + // CHANGE(thedevbirb): for chain replication we must ignore deriving the chain from L1 data. + if _, ok := os.LookupEnv("BOP_REPLAY"); ok { + return nil, io.EOF + } // if any stages need to be reset, do that first. if dp.resetting < len(dp.stages) { diff --git a/op-node/rollup/sync/config.go b/op-node/rollup/sync/config.go index 4e36092aaaf3c..51b34061d32fb 100644 --- a/op-node/rollup/sync/config.go +++ b/op-node/rollup/sync/config.go @@ -22,8 +22,10 @@ const ( ELSyncString string = "execution-layer" ) -var Modes = []Mode{CLSync, ELSync} -var ModeStrings = []string{CLSyncString, ELSyncString} +var ( + Modes = []Mode{CLSync, ELSync} + ModeStrings = []string{CLSyncString, ELSyncString} +) func StringToMode(s string) (Mode, error) { switch strings.ToLower(s) { diff --git a/op-node/service.go b/op-node/service.go index 515f85cfd2a65..ef6b12ef4144a 100644 --- a/op-node/service.go +++ b/op-node/service.go @@ -122,7 +122,6 @@ func NewConfig(ctx *cli.Context, log log.Logger) (*node.Config, error) { IgnoreMissingPectraBlobSchedule: ctx.Bool(flags.IgnoreMissingPectraBlobSchedule.Name), FetchWithdrawalRootFromState: ctx.Bool(flags.FetchWithdrawalRootFromState.Name), - UnsafeIsChainReplication: ctx.Bool(flags.UnsafeIsChainReplication.Name), } if err := cfg.LoadPersisted(log); err != nil { diff --git a/op-service/testutils/runtime_config.go b/op-service/testutils/runtime_config.go index fefb633651cff..e3d3541d1f99e 100644 --- a/op-service/testutils/runtime_config.go +++ b/op-service/testutils/runtime_config.go @@ -22,7 +22,3 @@ func (m *MockRuntimeConfig) GatewayForBlock(ctx context.Context, blockNumber uin func (m *MockRuntimeConfig) FetchNextNGateways(ctx context.Context, n uint64, maxRetries uint64) error { return nil } - -func (m *MockRuntimeConfig) UnsafeIsChainReplication() bool { - return false -} From 1e545641929fb26476e885dd9c70be2d627e4fd4 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 11 Sep 2025 10:05:16 +0200 Subject: [PATCH 5/6] chore(op-node): bump fetch next N gateways to 6; more reliable --- op-node/node/node.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/op-node/node/node.go b/op-node/node/node.go index c90480b63ca87..2e538c8d701c7 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -250,7 +250,7 @@ func (n *OpNode) initRegistry(ctx context.Context, cfg *Config) error { } // Initially fetch the current gateway + n gateways into the future - err = n.registrySource.FetchNextNGateways(ctx, 2, 3) + err = n.registrySource.FetchNextNGateways(ctx, 6, 3) if err != nil { return fmt.Errorf("failed to fetch initial gateways: %w", err) } @@ -261,7 +261,7 @@ func (n *OpNode) initRegistry(ctx context.Context, cfg *Config) error { fetchCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - if err := n.registrySource.FetchNextNGateways(fetchCtx, 2, 3); err != nil { + if err := n.registrySource.FetchNextNGateways(fetchCtx, 6, 3); err != nil { n.log.Warn("registry fetch error", "err", err) } time.Sleep(time.Second) From 86fd3cc574e77576ee2457d79b90eb4ab85daffe Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 17 Sep 2025 09:57:17 +0200 Subject: [PATCH 6/6] fix(op-node): use global variable instead of checking envs everytime --- op-node/cmd/main.go | 6 ++++++ op-node/node/node.go | 11 +++++------ op-node/p2p/gossip.go | 4 ++-- op-node/params/globals.go | 6 ++++++ 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 op-node/params/globals.go diff --git a/op-node/cmd/main.go b/op-node/cmd/main.go index b82b3f6babce2..613b5b102cfe1 100644 --- a/op-node/cmd/main.go +++ b/op-node/cmd/main.go @@ -18,6 +18,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/flags" "github.com/ethereum-optimism/optimism/op-node/metrics" "github.com/ethereum-optimism/optimism/op-node/node" + "github.com/ethereum-optimism/optimism/op-node/params" "github.com/ethereum-optimism/optimism/op-node/version" opservice "github.com/ethereum-optimism/optimism/op-service" "github.com/ethereum-optimism/optimism/op-service/cliapp" @@ -94,6 +95,11 @@ func RollupNodeMain(ctx *cli.Context, closeApp context.CancelCauseFunc) (cliapp. cfg.Rollup.LogDescription(log, chaincfg.L2ChainIDToNetworkDisplayName) } + // CHANGE(thedevbirb): assess whether we're in chain replication mode at startup. + if _, ok := os.LookupEnv("BOP_REPLAY"); ok { + params.BopReplay = true + } + n, err := node.New(ctx.Context, cfg, log, VersionWithMeta, m) if err != nil { return nil, fmt.Errorf("unable to create the rollup node: %w", err) diff --git a/op-node/node/node.go b/op-node/node/node.go index 2e538c8d701c7..8a5033959da53 100644 --- a/op-node/node/node.go +++ b/op-node/node/node.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "os" gosync "sync" "sync/atomic" "time" @@ -24,6 +23,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/metrics" "github.com/ethereum-optimism/optimism/op-node/node/safedb" "github.com/ethereum-optimism/optimism/op-node/p2p" + "github.com/ethereum-optimism/optimism/op-node/params" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/conductor" "github.com/ethereum-optimism/optimism/op-node/rollup/driver" @@ -623,7 +623,7 @@ func (n *OpNode) onEvent(ev event.Event) bool { func (n *OpNode) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) { // CHANGE(thedevbirb): allow chain replication without deviation due to L1 state. - if _, ok := os.LookupEnv("BOP_REPLAY"); ok { + if params.BopReplay { return } n.tracer.OnNewL1Head(ctx, sig) @@ -641,7 +641,7 @@ func (n *OpNode) OnNewL1Head(ctx context.Context, sig eth.L1BlockRef) { func (n *OpNode) OnNewL1Safe(ctx context.Context, sig eth.L1BlockRef) { // CHANGE(thedevbirb): allow chain replication without deviation due to L1 state. - if _, ok := os.LookupEnv("BOP_REPLAY"); ok { + if params.BopReplay { return } if n.l2Driver == nil { @@ -657,7 +657,7 @@ func (n *OpNode) OnNewL1Safe(ctx context.Context, sig eth.L1BlockRef) { func (n *OpNode) OnNewL1Finalized(ctx context.Context, sig eth.L1BlockRef) { // CHANGE(thedevbirb): allow chain replication without deviation due to L1 state. - if _, ok := os.LookupEnv("BOP_REPLAY"); ok { + if params.BopReplay { return } if n.l2Driver == nil { @@ -795,8 +795,7 @@ func (n *OpNode) OnEnv(ctx context.Context, from peer.ID, env *eth.SignedEnv) er func (n *OpNode) RequestL2Range(ctx context.Context, start, end eth.L2BlockRef) error { // CHANGE(thedevbirb): for chain replication, ignoring sending p2p syncing requests which may block the event loop. - _, isReplay := os.LookupEnv("BOP_REPLAY") - if p2pNode := n.getP2PNodeIfEnabled(); p2pNode != nil && p2pNode.AltSyncEnabled() && !isReplay { + if p2pNode := n.getP2PNodeIfEnabled(); p2pNode != nil && p2pNode.AltSyncEnabled() && !params.BopReplay { if unixTimeStale(start.Time, 12*time.Hour) { n.log.Debug( "ignoring request to sync L2 range, timestamp is too old for p2p", diff --git a/op-node/p2p/gossip.go b/op-node/p2p/gossip.go index 26e54ff34c7c6..117e15ad4ac1e 100644 --- a/op-node/p2p/gossip.go +++ b/op-node/p2p/gossip.go @@ -7,7 +7,6 @@ import ( "encoding/binary" "errors" "fmt" - "os" "sync" "time" @@ -23,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum-optimism/optimism/op-node/params" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/eth" opsigner "github.com/ethereum-optimism/optimism/op-service/signer" @@ -476,7 +476,7 @@ func BuildBlocksValidator(log log.Logger, cfg *rollup.Config, runCfg GossipRunti now := uint64(time.Now().Unix()) // CHANGE(thedevbirb): for chain replication, allow old blocks. - if _, ok := os.LookupEnv("BOP_REPLAY"); !ok { + if !params.BopReplay { // [REJECT] if the `payload.timestamp` is older than 60 seconds in the past if uint64(payload.Timestamp) < now-60 { log.Warn("payload is too old", "timestamp", uint64(payload.Timestamp)) diff --git a/op-node/params/globals.go b/op-node/params/globals.go new file mode 100644 index 0000000000000..e34d0252543cf --- /dev/null +++ b/op-node/params/globals.go @@ -0,0 +1,6 @@ +package params + +// CHANGE(thedevbirb): A global variable set only at node startup that assess +// whether the node is running in chain replication mode, leading to some +// syncing and safety functionality to be disabled. +var BopReplay = false