Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2cdfb07
update l2 RetryableClient
Dec 11, 2025
7c11f8c
fmt
Dec 11, 2025
98925c5
clean
Dec 11, 2025
c34bee9
fix mpt time
Dec 11, 2025
e39a59a
fix
Dec 15, 2025
105c335
Merge branch 'main' into mpt-switch
curryxbo Dec 15, 2025
a3d76f2
clean
Dec 17, 2025
8ae80bd
fix and test
Dec 22, 2025
d5f2d8d
clean
Dec 22, 2025
fa2307f
update
Jan 9, 2026
c38ea30
refactor
Jan 12, 2026
8b2a833
chore: remove mpt-switch-test from git tracking
Jan 12, 2026
c9f508d
chore: revert go-ethereum submodule to previous version
Jan 12, 2026
0968304
fix
Jan 12, 2026
f4d7b0c
rollback skip bls verify
Jan 13, 2026
32512d0
fix
Jan 13, 2026
b5e977c
clean
Jan 13, 2026
5f6ad17
clean
Jan 13, 2026
8631ca8
Merge branch 'main' into mpt-switch
Jan 13, 2026
5dea362
update geth version
Jan 13, 2026
772df41
Fix NewRetryableClient (#856)
curryxbo Jan 15, 2026
75181a7
Fix swtich time (#862)
curryxbo Jan 28, 2026
6120b0a
add mpt upgrade code for batch finalize
Kukoomomo Jan 29, 2026
89218d0
fix log level
Jan 29, 2026
0a63c6b
fix log level
Jan 29, 2026
a438b20
Force batch points around MPT fork to isolate the first post-fork blo…
FletcherMan Jan 30, 2026
8abcf7c
update submitter mpt config
Kukoomomo Jan 30, 2026
d6dd008
Merge branch 'mpt-switch' of github.com:morph-l2/morph into mpt-switch
Kukoomomo Jan 30, 2026
abcc805
update submitter mpt config
Kukoomomo Jan 30, 2026
57cf1e4
add bls key fork height flag (#863)
FletcherMan Feb 3, 2026
0f74ddb
Add retry for fetch eth_config (#865)
curryxbo Feb 4, 2026
0ec7bda
Merge branch 'main' into mpt-switch
curryxbo Feb 6, 2026
fd31a72
update geth version
Feb 6, 2026
fae4ad8
clean unused code
Kukoomomo Feb 9, 2026
ffa5e6d
Merge branch 'mpt-switch' of github.com:morph-l2/morph into mpt-switch
Kukoomomo Feb 9, 2026
f11e92a
Bump version and fix lint (#881)
curryxbo Feb 11, 2026
f34a133
make submodules
curryxbo Feb 11, 2026
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
12 changes: 11 additions & 1 deletion node/core/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
type NewSyncerFunc func() (*sync.Syncer, error)

type Executor struct {
l2LegacyClient *types.RetryableClient
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
l2Client *types.RetryableClient
bc BlockConverter
nextL1MsgIndex uint64
Expand Down Expand Up @@ -70,6 +71,15 @@ 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")
// TODO
laClient, err := authclient.DialContext(context.Background(), config.L2.EngineAddr, config.L2.JwtSecret)
if err != nil {
return nil, err
}
leClient, err := ethclient.Dial(config.L2.EthAddr)
if err != nil {
return nil, err
}
aClient, err := authclient.DialContext(context.Background(), config.L2.EngineAddr, config.L2.JwtSecret)
if err != nil {
return nil, err
Expand All @@ -79,7 +89,7 @@ func NewExecutor(newSyncFunc NewSyncerFunc, config *Config, tmPubKey crypto.PubK
return nil, err
}

l2Client := types.NewRetryableClient(aClient, eClient, config.Logger)
l2Client := types.NewRetryableClient(laClient, leClient, aClient, eClient, config.Logger)
index, err := getNextL1MsgIndex(l2Client)
if err != nil {
return nil, err
Expand Down
11 changes: 10 additions & 1 deletion node/derivation/derivation.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer,
if err != nil {
return nil, err
}
// TODO
laClient, err := authclient.DialContext(context.Background(), cfg.L2.EngineAddr, cfg.L2.JwtSecret)
if err != nil {
return nil, err
}
leClient, err := ethclient.Dial(cfg.L2.EthAddr)
if err != nil {
return nil, err
}
aClient, err := authclient.DialContext(context.Background(), cfg.L2.EngineAddr, cfg.L2.JwtSecret)
if err != nil {
return nil, err
Expand Down Expand Up @@ -129,7 +138,7 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer,
logger: logger,
RollupContractAddress: cfg.RollupContractAddress,
confirmations: cfg.L1.Confirmations,
l2Client: types.NewRetryableClient(aClient, eClient, tmlog.NewTMLogger(tmlog.NewSyncWriter(os.Stdout))),
l2Client: types.NewRetryableClient(laClient, leClient, aClient, eClient, tmlog.NewTMLogger(tmlog.NewSyncWriter(os.Stdout))),
cancel: cancel,
stop: make(chan struct{}),
startHeight: cfg.StartHeight,
Expand Down
2 changes: 1 addition & 1 deletion node/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/hashicorp/golang-lru v1.0.2
github.com/holiman/uint256 v1.2.4
github.com/klauspost/compress v1.17.9
github.com/morph-l2/go-ethereum v1.10.14-0.20251119080508-d085f8c79a53
github.com/morph-l2/go-ethereum v1.10.14-0.20251211075654-796834acba86
github.com/prometheus/client_golang v1.17.0
github.com/spf13/viper v1.13.0
github.com/stretchr/testify v1.10.0
Expand Down
4 changes: 2 additions & 2 deletions node/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,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.20251119080508-d085f8c79a53 h1:8+qaUTn1/eyS8er4RkibhHMFC/L4IgqIXLtORakBDkI=
github.com/morph-l2/go-ethereum v1.10.14-0.20251119080508-d085f8c79a53/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M=
github.com/morph-l2/go-ethereum v1.10.14-0.20251211075654-796834acba86 h1:4BgRnW5lZcgtVvK/WuDTNAfi5F5/VEb7FbDEvCksPHk=
github.com/morph-l2/go-ethereum v1.10.14-0.20251211075654-796834acba86/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M=
github.com/morph-l2/tendermint v0.3.2 h1:Gu6Uj2G6c3YP2NAKFi7A46JZaOCdD4zfZDKCjt0pDm8=
github.com/morph-l2/tendermint v0.3.2/go.mod h1:TtCzp9l6Z6yDUiwv3TbqKqw8Q8RKp3fSz5+adO1/Y8w=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
Expand Down
70 changes: 48 additions & 22 deletions node/types/retryable_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package types

import (
"context"
"math/big"
"strings"

"github.com/cenkalti/backoff/v4"
"github.com/morph-l2/go-ethereum"
"github.com/morph-l2/go-ethereum/common"
Expand All @@ -13,6 +10,9 @@ import (
"github.com/morph-l2/go-ethereum/ethclient"
"github.com/morph-l2/go-ethereum/ethclient/authclient"
tmlog "github.com/tendermint/tendermint/libs/log"
"math/big"
"strings"
"time"
)

const (
Expand All @@ -27,27 +27,47 @@ const (
)

type RetryableClient struct {
authClient *authclient.Client
ethClient *ethclient.Client
b backoff.BackOff
logger tmlog.Logger
legacyAuthClient *authclient.Client
legacyEthClient *ethclient.Client
authClient *authclient.Client
ethClient *ethclient.Client
mptTime uint64 // TODO rename
b backoff.BackOff
logger tmlog.Logger
}

// NewRetryableClient make the client retryable
// Will retry calling the api, if the connection is refused
func NewRetryableClient(authClient *authclient.Client, ethClient *ethclient.Client, logger tmlog.Logger) *RetryableClient {
func NewRetryableClient(legacyAuthClient *authclient.Client, legacyEthClient *ethclient.Client, authClient *authclient.Client, ethClient *ethclient.Client, logger tmlog.Logger) *RetryableClient {
logger = logger.With("module", "retryClient")
return &RetryableClient{
authClient: authClient,
ethClient: ethClient,
b: backoff.NewExponentialBackOff(),
logger: logger,
legacyAuthClient: legacyAuthClient,
legacyEthClient: legacyEthClient,
authClient: authClient,
ethClient: ethClient,
b: backoff.NewExponentialBackOff(),
logger: logger,
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

func (c *RetryableClient) aClient(timeStamp uint64) *authclient.Client {
if c.mptTime >= timeStamp {
return c.legacyAuthClient
}
return c.authClient
}

func (c *RetryableClient) eClient(timeStamp uint64) *ethclient.Client {
if c.mptTime >= timeStamp {
return c.legacyEthClient
}
Comment on lines +250 to 293
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Blocking loop lacks timeout and context cancellation, risking indefinite hang.

The polling loop that waits for the next geth to sync has no exit conditions other than successful synchronization:

  1. No timeout: If the next geth is down or never catches up, this blocks forever.
  2. No context cancellation: The ctx parameter is passed but ctx.Done() is never checked. A cancelled context won't stop the loop.

This could cause the entire node to hang during an upgrade if the next geth is unavailable.

Proposed fix
 	for {
+		select {
+		case <-ctx.Done():
+			rc.logger.Error("Context cancelled while waiting for next geth to sync",
+				"error", ctx.Err(),
+				"wait_duration", time.Since(startTime))
+			return
+		default:
+		}
+
 		remote, err := rc.nextEthClient.BlockNumber(ctx)
 		if err != nil {
 			rc.logger.Error("Failed to get next geth block number",
 				"error", err,
 				"hint", "Please ensure next geth is running and accessible")
-			<-ticker.C
+			select {
+			case <-ctx.Done():
+				return
+			case <-ticker.C:
+			}
 			continue
 		}

Consider also adding a maximum wait timeout (e.g., configurable or a sensible default like 30 minutes) to prevent indefinite blocking in production.

🤖 Prompt for AI Agents
In @node/types/retryable_client.go around lines 239 - 282, The polling loop that
waits for next geth sync (using rc.nextEthClient.BlockNumber and HeaderByNumber)
lacks context cancellation and a maximum timeout, risking indefinite hang;
update the loop to select on three cases: ctx.Done() to abort and return an
error, a timeout channel (configurable default e.g., 30 minutes) to abort with a
clear log/error if exceeded, and ticker.C for the existing polling path; ensure
you propagate the cancel reason (ctx.Err() or timeout) in the log and return,
and keep the existing success path that sets rc.switched.Store(true) and logs
timing (use startTime) while removing the unconditional blocking read from
ticker.C.

return c.ethClient
}
Comment on lines +224 to 294
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Infinite blocking loop can halt the node permanently.

The switchClient method enters an unbounded loop (lines 239-282) waiting for the next geth to sync. If the next geth is misconfigured, unreachable, or permanently behind, this will block the caller forever with no way to recover.

Additionally:

  • Multiple concurrent callers can enter the waiting loop simultaneously before switched.Store(true) at line 259.
  • Line 273 uses logger.Error for a status message that's more of a warning.

Consider adding:

  1. A maximum timeout or retry limit with graceful degradation
  2. A mutex or sync.Once to ensure only one goroutine performs the switch logic
🛠️ Suggested approach with timeout
 func (rc *RetryableClient) switchClient(ctx context.Context, timeStamp uint64, number uint64) {
 	if rc.switched.Load() {
 		return
 	}
 	if timeStamp < rc.switchTime {
 		return
 	}

+	// Add a reasonable timeout for the switch operation
+	switchCtx, cancel := context.WithTimeout(ctx, 30*time.Minute)
+	defer cancel()

 	// ... existing logging ...

 	ticker := time.NewTicker(500 * time.Millisecond)
 	defer ticker.Stop()

 	for {
+		select {
+		case <-switchCtx.Done():
+			rc.logger.Error("Switch timeout exceeded, failing switch operation")
+			return // or panic, depending on desired behavior
+		default:
+		}
+
 		remote, err := rc.nextEthClient.BlockNumber(ctx)
 		// ... rest of loop ...
 	}
 }
🤖 Prompt for AI Agents
In @node/types/retryable_client.go around lines 216 - 283, The switchClient
method can block forever while polling rc.nextEthClient, so add a bounded
retry/timeout and single-writer guarantee: introduce a context-aware timeout or
maxRetries and bail out with a clear error/log if the next node doesn't reach
target within that limit (use ctx with deadline or a local retry counter around
the loop that calls rc.nextEthClient.BlockNumber and HeaderByNumber), ensure
only one goroutine runs the switch by protecting the critical section with a
sync.Mutex or sync.Once (guard the polling loop and the rc.switched.Store(true)
path using the chosen sync primitive) and change the periodic status log call
(currently using rc.logger.Error for waiting state) to rc.logger.Warn or Info;
keep all other logging and the existing remote/block checks in switchClient.


func (rc *RetryableClient) AssembleL2Block(ctx context.Context, number *big.Int, transactions eth.Transactions) (ret *catalyst.ExecutableL2Data, err error) {
timestamp := uint64(time.Now().Unix())
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.authClient.AssembleL2Block(ctx, number, transactions)
resp, respErr := rc.aClient(timestamp).AssembleL2Block(ctx, &timestamp, number, transactions)
if respErr != nil {
rc.logger.Info("failed to AssembleL2Block", "error", respErr)
if retryableError(respErr) {
Expand All @@ -65,7 +85,7 @@ func (rc *RetryableClient) AssembleL2Block(ctx context.Context, number *big.Int,

func (rc *RetryableClient) ValidateL2Block(ctx context.Context, executableL2Data *catalyst.ExecutableL2Data) (ret bool, err error) {
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.authClient.ValidateL2Block(ctx, executableL2Data)
resp, respErr := rc.aClient(executableL2Data.Timestamp).ValidateL2Block(ctx, executableL2Data)
if respErr != nil {
rc.logger.Info("failed to ValidateL2Block", "error", respErr)
if retryableError(respErr) {
Expand All @@ -83,7 +103,7 @@ func (rc *RetryableClient) ValidateL2Block(ctx context.Context, executableL2Data

func (rc *RetryableClient) NewL2Block(ctx context.Context, executableL2Data *catalyst.ExecutableL2Data, batchHash *common.Hash) (err error) {
if retryErr := backoff.Retry(func() error {
respErr := rc.authClient.NewL2Block(ctx, executableL2Data, batchHash)
respErr := rc.aClient(executableL2Data.Timestamp).NewL2Block(ctx, executableL2Data, batchHash)
if respErr != nil {
rc.logger.Info("failed to NewL2Block", "error", respErr)
if retryableError(respErr) {
Expand All @@ -100,7 +120,7 @@ func (rc *RetryableClient) NewL2Block(ctx context.Context, executableL2Data *cat

func (rc *RetryableClient) NewSafeL2Block(ctx context.Context, safeL2Data *catalyst.SafeL2Data) (ret *eth.Header, err error) {
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.authClient.NewSafeL2Block(ctx, safeL2Data)
resp, respErr := rc.aClient(safeL2Data.Timestamp).NewSafeL2Block(ctx, safeL2Data)
if respErr != nil {
rc.logger.Info("failed to NewSafeL2Block", "error", respErr)
if retryableError(respErr) {
Expand All @@ -118,7 +138,8 @@ func (rc *RetryableClient) NewSafeL2Block(ctx context.Context, safeL2Data *catal

func (rc *RetryableClient) CommitBatch(ctx context.Context, batch *eth.RollupBatch, signatures []eth.BatchSignature) (err error) {
if retryErr := backoff.Retry(func() error {
respErr := rc.authClient.CommitBatch(ctx, batch, signatures)
// TODO timestamp
respErr := rc.aClient(0).CommitBatch(ctx, batch, signatures)
if respErr != nil {
rc.logger.Info("failed to CommitBatch", "error", respErr)
if retryableError(respErr) {
Expand All @@ -135,7 +156,8 @@ func (rc *RetryableClient) CommitBatch(ctx context.Context, batch *eth.RollupBat

func (rc *RetryableClient) AppendBlsSignature(ctx context.Context, batchHash common.Hash, signature eth.BatchSignature) (err error) {
if retryErr := backoff.Retry(func() error {
respErr := rc.authClient.AppendBlsSignature(ctx, batchHash, signature)
// TODO timestamp
respErr := rc.aClient(0).AppendBlsSignature(ctx, batchHash, signature)
if respErr != nil {
rc.logger.Info("failed to call AppendBlsSignature", "error", respErr)
if retryableError(respErr) {
Expand All @@ -152,7 +174,8 @@ func (rc *RetryableClient) AppendBlsSignature(ctx context.Context, batchHash com

func (rc *RetryableClient) BlockNumber(ctx context.Context) (ret uint64, err error) {
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.ethClient.BlockNumber(ctx)
// TODO timestamp
resp, respErr := rc.eClient(0).BlockNumber(ctx)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if respErr != nil {
rc.logger.Info("failed to call BlockNumber", "error", respErr)
if retryableError(respErr) {
Expand All @@ -170,7 +193,8 @@ func (rc *RetryableClient) BlockNumber(ctx context.Context) (ret uint64, err err

func (rc *RetryableClient) HeaderByNumber(ctx context.Context, blockNumber *big.Int) (ret *eth.Header, err error) {
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.ethClient.HeaderByNumber(ctx, blockNumber)
// TODO timestamp
resp, respErr := rc.eClient(0).HeaderByNumber(ctx, blockNumber)
if respErr != nil {
rc.logger.Info("failed to call BlockNumber", "error", respErr)
Comment thread
curryxbo marked this conversation as resolved.
if retryableError(respErr) {
Expand All @@ -188,7 +212,8 @@ func (rc *RetryableClient) HeaderByNumber(ctx context.Context, blockNumber *big.

func (rc *RetryableClient) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) (ret []byte, err error) {
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.ethClient.CallContract(ctx, call, blockNumber)
// TODO timestamp
resp, respErr := rc.eClient(0).CallContract(ctx, call, blockNumber)
if respErr != nil {
rc.logger.Info("failed to call eth_call", "error", respErr)
if retryableError(respErr) {
Expand All @@ -206,7 +231,8 @@ func (rc *RetryableClient) CallContract(ctx context.Context, call ethereum.CallM

func (rc *RetryableClient) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) (ret []byte, err error) {
if retryErr := backoff.Retry(func() error {
resp, respErr := rc.ethClient.CodeAt(ctx, contract, blockNumber)
// TODO timestamp
resp, respErr := rc.eClient(0).CodeAt(ctx, contract, blockNumber)
if respErr != nil {
rc.logger.Info("failed to call eth_getCode", "error", respErr)
if retryableError(respErr) {
Expand Down
Loading