Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ contracts/mainnet.json
.env

# logs
*.log
*.log

# mpt-switch-test (local testing only)
ops/mpt-switch-test
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
################## update dependencies ####################
ETHEREUM_SUBMODULE_COMMIT_OR_TAG := morph-v2.1.0
ETHEREUM_TARGET_VERSION := v1.10.14-0.20251219060125-03910bc750a2
ETHEREUM_TARGET_VERSION := v1.10.14-0.20260113015804-82683159dfd0
TENDERMINT_TARGET_VERSION := v0.3.2

ETHEREUM_MODULE_NAME := github.com/morph-l2/go-ethereum
Expand Down
2 changes: 1 addition & 1 deletion bindings/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.24.0

replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.2

require github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2
require github.com/morph-l2/go-ethereum v1.10.14-0.20260113015804-82683159dfd0

require (
github.com/VictoriaMetrics/fastcache v1.12.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions bindings/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqky
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0=
github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M=
github.com/morph-l2/go-ethereum v1.10.14-0.20260113015804-82683159dfd0 h1:tu77ClhPcySgkweTINJBoLkIdpKKjrDF+4JPMOBCBLk=
github.com/morph-l2/go-ethereum v1.10.14-0.20260113015804-82683159dfd0/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
Expand Down
2 changes: 1 addition & 1 deletion contracts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.

require (
github.com/iden3/go-iden3-crypto v0.0.16
github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2
github.com/morph-l2/go-ethereum v1.10.14-0.20260113015804-82683159dfd0
github.com/stretchr/testify v1.10.0
)

Expand Down
4 changes: 2 additions & 2 deletions contracts/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqky
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 h1:FUv9gtnvF+1AVrkoNGYbVOesi7E+STjdfD2mcqVaEY0=
github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M=
github.com/morph-l2/go-ethereum v1.10.14-0.20260113015804-82683159dfd0 h1:tu77ClhPcySgkweTINJBoLkIdpKKjrDF+4JPMOBCBLk=
github.com/morph-l2/go-ethereum v1.10.14-0.20260113015804-82683159dfd0/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
Expand Down
84 changes: 84 additions & 0 deletions node/core/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down
16 changes: 16 additions & 0 deletions node/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var (

type Config struct {
L2 *types.L2Config `json:"l2"`
L2Next *types.L2Config `json:"l2_next,omitempty"` // optional, for geth upgrade switch
L2CrossDomainMessengerAddress common.Address `json:"cross_domain_messenger_address"`
SequencerAddress common.Address `json:"sequencer_address"`
GovAddress common.Address `json:"gov_address"`
Expand All @@ -43,6 +44,7 @@ type Config struct {
func DefaultConfig() *Config {
return &Config{
L2: new(types.L2Config),
L2Next: nil, // optional, only for upgrade switch
Logger: tmlog.NewTMLogger(tmlog.NewSyncWriter(os.Stdout)),
MaxL1MessageNumPerBlock: 100,
L2CrossDomainMessengerAddress: predeploys.L2CrossDomainMessengerAddr,
Expand Down Expand Up @@ -123,6 +125,16 @@ func (c *Config) SetCliContext(ctx *cli.Context) error {
c.L2.EngineAddr = l2EngineAddr
c.L2.JwtSecret = secret

// L2Next is optional - only for upgrade switch (e.g., ZK to MPT)
l2NextEthAddr := ctx.GlobalString(flags.L2NextEthAddr.Name)
l2NextEngineAddr := ctx.GlobalString(flags.L2NextEngineAddr.Name)
if l2NextEthAddr != "" && l2NextEngineAddr != "" {
c.L2Next = &types.L2Config{
EthAddr: l2NextEthAddr,
EngineAddr: l2NextEngineAddr,
JwtSecret: secret, // same secret
}
}
if ctx.GlobalIsSet(flags.MaxL1MessageNumPerBlock.Name) {
c.MaxL1MessageNumPerBlock = ctx.GlobalUint64(flags.MaxL1MessageNumPerBlock.Name)
if c.MaxL1MessageNumPerBlock == 0 {
Expand Down Expand Up @@ -158,6 +170,10 @@ func (c *Config) SetCliContext(ctx *cli.Context) error {
c.DevSequencer = ctx.GlobalBool(flags.DevSequencer.Name)
}

if ctx.GlobalIsSet(flags.BlsKeyCheckForkHeight.Name) {
c.BlsKeyCheckForkHeight = ctx.GlobalUint64(flags.BlsKeyCheckForkHeight.Name)
}

// setup batch upgrade index and fork heights
switch {
case ctx.GlobalIsSet(flags.MainnetFlag.Name):
Expand Down
76 changes: 70 additions & 6 deletions node/core/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -71,6 +78,7 @@ func getNextL1MsgIndex(client *types.RetryableClient) (uint64, error) {
func NewExecutor(newSyncFunc NewSyncerFunc, config *Config, tmPubKey crypto.PubKey) (*Executor, error) {
logger := config.Logger
logger = logger.With("module", "executor")
// L2 geth endpoint (required - current geth)
aClient, err := authclient.DialContext(context.Background(), config.L2.EngineAddr, config.L2.JwtSecret)
if err != nil {
return nil, err
Expand All @@ -80,7 +88,31 @@ func NewExecutor(newSyncFunc NewSyncerFunc, config *Config, tmPubKey crypto.PubK
return nil, err
}

l2Client := types.NewRetryableClient(aClient, eClient, config.Logger)
// L2Next endpoint (optional - for upgrade switch)
var aNextClient *authclient.Client
var eNextClient *ethclient.Client
if config.L2Next != nil && config.L2Next.EngineAddr != "" && config.L2Next.EthAddr != "" {
aNextClient, err = authclient.DialContext(context.Background(), config.L2Next.EngineAddr, config.L2Next.JwtSecret)
if err != nil {
return nil, err
}
eNextClient, err = ethclient.Dial(config.L2Next.EthAddr)
if err != nil {
return nil, err
}
Comment on lines +91 to +102
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Close aNextClient when eNextClient dial fails.

Line 100 returns on error but leaves the auth client open.

🧹 Proposed fix
eNextClient, err = ethclient.Dial(config.L2Next.EthAddr)
if err != nil {
+	aNextClient.Close()
	return nil, err
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// L2Next endpoint (optional - for upgrade switch)
var aNextClient *authclient.Client
var eNextClient *ethclient.Client
if config.L2Next != nil && config.L2Next.EngineAddr != "" && config.L2Next.EthAddr != "" {
aNextClient, err = authclient.DialContext(context.Background(), config.L2Next.EngineAddr, config.L2Next.JwtSecret)
if err != nil {
return nil, err
}
eNextClient, err = ethclient.Dial(config.L2Next.EthAddr)
if err != nil {
return nil, err
}
// L2Next endpoint (optional - for upgrade switch)
var aNextClient *authclient.Client
var eNextClient *ethclient.Client
if config.L2Next != nil && config.L2Next.EngineAddr != "" && config.L2Next.EthAddr != "" {
aNextClient, err = authclient.DialContext(context.Background(), config.L2Next.EngineAddr, config.L2Next.JwtSecret)
if err != nil {
return nil, err
}
eNextClient, err = ethclient.Dial(config.L2Next.EthAddr)
if err != nil {
aNextClient.Close()
return nil, err
}
🤖 Prompt for AI Agents
In `@node/core/executor.go` around lines 91 - 102, When dialing the L2Next
clients, if authclient.DialContext succeeds (aNextClient is non-nil) but
ethclient.Dial (eNextClient) fails, ensure you close the opened aNextClient
before returning the error; update the block that calls authclient.DialContext
and ethclient.Dial in executor.go to call aNextClient.Close() (or the
appropriate Close/Disconnect method on authclient.Client) just prior to
returning the error from ethclient.Dial so the auth client is not leaked.

logger.Info("L2Next geth configured (upgrade switch enabled)", "engineAddr", config.L2Next.EngineAddr, "ethAddr", config.L2Next.EthAddr)
} else {
logger.Info("L2Next geth not configured (no upgrade switch)")
}

// Fetch geth config at startup (with retry to wait for geth)
gethCfg, err := types.FetchGethConfigWithRetry(config.L2.EthAddr, logger)
if err != nil {
return nil, fmt.Errorf("failed to fetch geth config: %w", err)
}
logger.Info("Geth config fetched", "switchTime", gethCfg.SwitchTime, "useZktrie", gethCfg.UseZktrie)

l2Client := types.NewRetryableClient(aClient, eClient, aNextClient, eNextClient, gethCfg.SwitchTime, logger)
index, err := getNextL1MsgIndex(l2Client)
if err != nil {
return nil, err
Expand Down Expand Up @@ -123,6 +155,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"),
}
Expand Down Expand Up @@ -283,16 +316,39 @@ func (e *Executor) DeliverBlock(txs [][]byte, metaData []byte, consensusData l2n
}

if wrappedBlock.Number <= height {
e.logger.Info("ignore it, the block was delivered", "block number", wrappedBlock.Number)
if e.devSequencer {
return nil, consensusData.ValidatorSet, nil
e.logger.Info("block already delivered by geth (via P2P sync)", "block_number", wrappedBlock.Number)
// Even if block was already delivered (e.g., synced via P2P), we still need to check
// if MPT switch should happen, otherwise sentry nodes won't switch to the correct geth.
e.l2Client.EnsureSwitched(context.Background(), wrappedBlock.Timestamp, wrappedBlock.Number)

// After switch, re-check height from the new geth client
// The block might exist in legacy geth but not in target geth after switch
newHeight, err := e.l2Client.BlockNumber(context.Background())
if err != nil {
return nil, nil, err
}
if wrappedBlock.Number > newHeight {
e.logger.Info("block not in target geth after switch, need to deliver",
"block_number", wrappedBlock.Number,
"old_height", height,
"new_height", newHeight)
// Update height and continue to deliver the block
height = newHeight
} else {
if e.devSequencer {
return nil, consensusData.ValidatorSet, nil
}
return e.getParamsAndValsAtHeight(int64(wrappedBlock.Number))
}
return e.getParamsAndValsAtHeight(int64(wrappedBlock.Number))
}

// We only accept the continuous blocks for now.
// It acts like full sync. Snap sync is not enabled until the Geth enables snapshot with zkTrie
if wrappedBlock.Number > height+1 {
e.logger.Error("!!! CRITICAL: Geth is behind - node BLOCKED !!!",
"consensus_block", wrappedBlock.Number,
"geth_height", height,
"action", "Switch to MPT-compatible geth IMMEDIATELY")
return nil, nil, types.ErrWrongBlockNumber
}

Expand Down Expand Up @@ -324,7 +380,15 @@ func (e *Executor) DeliverBlock(txs [][]byte, metaData []byte, consensusData l2n
}
err = e.l2Client.NewL2Block(context.Background(), l2Block, batchHash)
if err != nil {
e.logger.Error("failed to NewL2Block", "error", err)
e.logger.Error("========================================")
e.logger.Error("CRITICAL: Failed to deliver block to geth!")
e.logger.Error("========================================")
e.logger.Error("failed to NewL2Block",
"error", err,
"block_number", l2Block.Number,
"block_timestamp", l2Block.Timestamp)
e.logger.Error("HINT: If this occurs after MPT upgrade, your geth node may not support MPT blocks. " +
"Please ensure you are running an MPT-compatible geth node.")
return nil, nil, err
}

Expand Down
13 changes: 13 additions & 0 deletions node/derivation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
type Config struct {
L1 *types.L1Config `json:"l1"`
L2 *types.L2Config `json:"l2"`
L2Next *types.L2Config `json:"l2_next,omitempty"` // optional, for geth upgrade switch
BeaconRpc string `json:"beacon_rpc"`
RollupContractAddress common.Address `json:"rollup_contract_address"`
StartHeight uint64 `json:"start_height"`
Expand All @@ -55,6 +56,7 @@ func DefaultConfig() *Config {
LogProgressInterval: DefaultLogProgressInterval,
FetchBlockRange: DefaultFetchBlockRange,
L2: new(types.L2Config),
L2Next: nil, // optional, only for upgrade switch
}
}

Expand Down Expand Up @@ -135,6 +137,17 @@ func (c *Config) SetCliContext(ctx *cli.Context) error {
c.L2.EthAddr = l2EthAddr
c.L2.EngineAddr = l2EngineAddr
c.L2.JwtSecret = secret

// L2Next is optional - only for upgrade switch (e.g., ZK to MPT)
l2NextEthAddr := ctx.GlobalString(flags.L2NextEthAddr.Name)
l2NextEngineAddr := ctx.GlobalString(flags.L2NextEngineAddr.Name)
if l2NextEthAddr != "" && l2NextEngineAddr != "" {
c.L2Next = &types.L2Config{
EthAddr: l2NextEthAddr,
EngineAddr: l2NextEngineAddr,
JwtSecret: secret, // same secret
}
}
c.MetricsServerEnable = ctx.GlobalBool(flags.MetricsServerEnable.Name)
c.MetricsHostname = ctx.GlobalString(flags.MetricsHostname.Name)
c.MetricsPort = ctx.GlobalUint64(flags.MetricsPort.Name)
Expand Down
Loading