From 2b22c35cabf2e17b579e6d0f5dc8530d2c28d180 Mon Sep 17 00:00:00 2001 From: "fletcher.fan" Date: Wed, 14 Jan 2026 16:25:59 +0800 Subject: [PATCH] Force batch points around MPT fork to isolate the first post-fork block --- node/core/batch.go | 84 ++++++++++++++++++++++++++++++++++ node/core/executor.go | 8 ++++ node/types/retryable_client.go | 19 +++++--- 3 files changed, 104 insertions(+), 7 deletions(-) diff --git a/node/core/batch.go b/node/core/batch.go index 987c4bf03..9c851956e 100644 --- a/node/core/batch.go +++ b/node/core/batch.go @@ -178,6 +178,17 @@ func (e *Executor) CalculateCapWithProposalBlock(currentBlockBytes []byte, curre return false, err } + // MPT fork: force batch points on the 1st and 2nd post-fork blocks, so the 1st post-fork block + // becomes a single-block batch: [H1, H2). + force, err := e.forceBatchPointForMPTFork(height, block.Timestamp, block.StateRoot, block.Hash) + if err != nil { + return false, err + } + if force { + e.logger.Info("MPT fork: force batch point", "height", height, "timestamp", block.Timestamp) + return true, nil + } + var exceeded bool if e.isBatchUpgraded(block.Timestamp) { exceeded, err = e.batchingCache.batchData.WillExceedCompressedSizeLimit(e.batchingCache.currentBlockContext, e.batchingCache.currentTxsPayload) @@ -187,6 +198,79 @@ func (e *Executor) CalculateCapWithProposalBlock(currentBlockBytes []byte, curre return exceeded, err } +// forceBatchPointForMPTFork forces batch points at the 1st and 2nd block after the MPT fork time. +// +// Design goals: +// - Minimal change: only affects batch-point decision logic. +// - Stability: CalculateCapWithProposalBlock can be called multiple times at the same height; return must be consistent. +// - Performance: after handling (or skipping beyond) the fork boundary, no more HeaderByNumber calls are made. +func (e *Executor) forceBatchPointForMPTFork(height uint64, blockTime uint64, stateRoot common.Hash, blockHash common.Hash) (bool, error) { + // If we already decided to force at this height, keep returning true without extra RPCs. + if e.mptForkForceHeight == height && height != 0 { + return true, nil + } + // If fork boundary is already handled and this isn't a forced height, fast exit. + if e.mptForkStage >= 2 { + return false, nil + } + + // Ensure we have fork time cached (0 means disabled). + if e.mptForkTime == 0 { + e.mptForkTime = e.l2Client.MPTForkTime() + } + forkTime := e.mptForkTime + if forkTime == 0 || blockTime < forkTime { + return false, nil + } + if height == 0 { + return false, nil + } + + // Check parent block time to detect the 1st post-fork block (H1). + parent, err := e.l2Client.HeaderByNumber(context.Background(), big.NewInt(int64(height-1))) + if err != nil { + return false, err + } + if parent.Time < forkTime { + // Log H1 (the 1st post-fork block) state root + // This stateRoot is intended to be used as the Rollup contract "genesis state root" + // when we reset/re-initialize the genesis state root during the MPT upgrade. + e.logger.Info( + "MPT_FORK_H1_GENESIS_STATE_ROOT", + "height", height, + "timestamp", blockTime, + "forkTime", forkTime, + "stateRoot", stateRoot.Hex(), + "blockHash", blockHash.Hex(), + ) + e.mptForkStage = 1 + e.mptForkForceHeight = height + return true, nil + } + + // If parent is already post-fork, we may be at the 2nd post-fork block (H2) or later. + if height < 2 { + // We cannot be H2; mark done to avoid future calls. + e.mptForkStage = 2 + return false, nil + } + + grandParent, err := e.l2Client.HeaderByNumber(context.Background(), big.NewInt(int64(height-2))) + if err != nil { + return false, err + } + if grandParent.Time < forkTime { + // This is H2 (2nd post-fork block). + e.mptForkStage = 2 + e.mptForkForceHeight = height + return true, nil + } + + // Beyond H2: nothing to do (can't retroactively fix). Mark done for performance. + e.mptForkStage = 2 + return false, nil +} + func (e *Executor) AppendBlsData(height int64, batchHash []byte, data l2node.BlsData) error { if len(batchHash) != 32 { return fmt.Errorf("wrong batchHash length. expected: 32, actual: %d", len(batchHash)) diff --git a/node/core/executor.go b/node/core/executor.go index 43aeb21a6..6b31706ae 100644 --- a/node/core/executor.go +++ b/node/core/executor.go @@ -56,6 +56,13 @@ type Executor struct { rollupABI *abi.ABI batchingCache *BatchingCache + // MPT fork handling: force batch points at the 1st and 2nd block after fork. + // This state machine exists to avoid repeated HeaderByNumber calls after the fork is handled, + // while keeping results stable if CalculateCapWithProposalBlock is called multiple times at the same height. + mptForkTime uint64 // cached from geth eth_config.morph.mptForkTime (0 means disabled/unknown) + mptForkStage uint8 // 0: not handled, 1: forced H1, 2: done (forced H2 or skipped beyond H2) + mptForkForceHeight uint64 // if equals current height, must return true (stability across multiple calls) + logger tmlog.Logger metrics *Metrics } @@ -141,6 +148,7 @@ func NewExecutor(newSyncFunc NewSyncerFunc, config *Config, tmPubKey crypto.PubK batchingCache: NewBatchingCache(), UpgradeBatchTime: config.UpgradeBatchTime, blsKeyCheckForkHeight: config.BlsKeyCheckForkHeight, + mptForkTime: l2Client.MPTForkTime(), logger: logger, metrics: PrometheusMetrics("morphnode"), } diff --git a/node/types/retryable_client.go b/node/types/retryable_client.go index 5bdf9a738..a0c36db66 100644 --- a/node/types/retryable_client.go +++ b/node/types/retryable_client.go @@ -40,12 +40,12 @@ type configResponse struct { // forkConfig represents a single fork configuration type forkConfig struct { - ActivationTime uint64 `json:"activationTime"` - ChainId string `json:"chainId"` - ForkId string `json:"forkId"` - Precompiles map[string]string `json:"precompiles"` - SystemContracts map[string]string `json:"systemContracts"` - Morph *morphExtension `json:"morph,omitempty"` + ActivationTime uint64 `json:"activationTime"` + ChainId string `json:"chainId"` + ForkId string `json:"forkId"` + Precompiles map[string]string `json:"precompiles"` + SystemContracts map[string]string `json:"systemContracts"` + Morph *morphExtension `json:"morph,omitempty"` } // morphExtension contains Morph-specific configuration fields @@ -131,6 +131,12 @@ type RetryableClient struct { logger tmlog.Logger } +// MPTForkTime returns the configured MPT fork/switch timestamp fetched from geth (eth_config). +// Note: this is a local value stored in the client; it does not perform any RPC. +func (rc *RetryableClient) MPTForkTime() uint64 { + return rc.switchTime +} + // NewRetryableClient creates a new retryable client that fetches switch time from geth. // The l2EthAddr is used to fetch the switch time via eth_config API. // Will retry calling the api, if the connection is refused. @@ -191,7 +197,6 @@ func NewRetryableClient(authClient *authclient.Client, ethClient *ethclient.Clie return rc } - func (rc *RetryableClient) aClient() *authclient.Client { if !rc.switched.Load() { return rc.authClient