diff --git a/.gitignore b/.gitignore index 369a28572..5e39098e6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ contracts/mainnet.json .env # logs -*.log \ No newline at end of file +*.log + +# mpt-switch-test (local testing only) +ops/mpt-switch-test \ No newline at end of file diff --git a/Makefile b/Makefile index 494ed9049..6c14cbc7c 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ ################## 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.20260206065010-7b2a9bb930e3 TENDERMINT_TARGET_VERSION := v0.3.3 + ETHEREUM_MODULE_NAME := github.com/morph-l2/go-ethereum TENDERMINT_MODULE_NAME := github.com/morph-l2/tendermint diff --git a/bindings/go.mod b/bindings/go.mod index e5d397882..04eb5d685 100644 --- a/bindings/go.mod +++ b/bindings/go.mod @@ -4,7 +4,7 @@ go 1.24.0 replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.3 -require github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 +require github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3 require ( github.com/VictoriaMetrics/fastcache v1.12.2 // indirect diff --git a/bindings/go.sum b/bindings/go.sum index a479ed434..c717be4ff 100644 --- a/bindings/go.sum +++ b/bindings/go.sum @@ -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.20260206065010-7b2a9bb930e3 h1:xGYu0xpTuIc2uN1A4Ig1I6ZN6CkqFZXlfXmPpyTtN3I= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3/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= diff --git a/contracts/go.mod b/contracts/go.mod index c0cdc1473..e20edb8be 100644 --- a/contracts/go.mod +++ b/contracts/go.mod @@ -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.20260206065010-7b2a9bb930e3 github.com/stretchr/testify v1.10.0 ) diff --git a/contracts/go.sum b/contracts/go.sum index 319f1f2b8..906c84b44 100644 --- a/contracts/go.sum +++ b/contracts/go.sum @@ -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.20260206065010-7b2a9bb930e3 h1:xGYu0xpTuIc2uN1A4Ig1I6ZN6CkqFZXlfXmPpyTtN3I= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3/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= diff --git a/go-ethereum b/go-ethereum index 03910bc75..796834acb 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 03910bc750a2301be4c1410b9f3c4d3741df251e +Subproject commit 796834acba86e4d3622ed435091281abe86c3c99 diff --git a/go.work.sum b/go.work.sum index 4403ab863..fc650b5b7 100644 --- a/go.work.sum +++ b/go.work.sum @@ -558,8 +558,6 @@ github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phm github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e h1:bBLctRc7kr01YGvaDfgLbTwjFNW5jdp5y5rj8XXBHfY= github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY= -github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= -github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCroCIB1foO/yQ36swABL8aOVeDpgg= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw= @@ -1007,8 +1005,8 @@ github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8q github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morph-l2/go-ethereum v1.10.14-0.20251125061742-69718a9dcab9/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= -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.20260206063816-522b70a5f16f h1:e8gfduHc4AKlR0fD6J3HXveP2Gp4PMvN2UfA9CYEvEc= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206063816-522b70a5f16f/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae h1:VeRdUYdCw49yizlSbMEn2SZ+gT+3IUKx8BqxyQdz+BY= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= @@ -1245,8 +1243,6 @@ github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iL github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= github.com/ultraware/whitespace v0.0.5 h1:hh+/cpIcopyMYbZNVov9iSxvJU3OYQg78Sfaqzi/CzI= github.com/ultraware/whitespace v0.0.5/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= -github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= -github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/uudashr/gocognit v1.0.6 h1:2Cgi6MweCsdB6kpcVQp7EW4U23iBFQWfTXiWlyp842Y= @@ -1265,8 +1261,6 @@ github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtX github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6 h1:YdYsPAZ2pC6Tow/nPZOPQ96O3hm/ToAkGsPLzedXERk= github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= 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/config.go b/node/core/config.go index a11b23897..9132f732c 100644 --- a/node/core/config.go +++ b/node/core/config.go @@ -32,6 +32,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"` @@ -46,6 +47,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, @@ -126,6 +128,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 { @@ -161,6 +173,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): diff --git a/node/core/executor.go b/node/core/executor.go index 0d8895658..52d30125d 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 } @@ -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 @@ -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 + } + 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 @@ -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"), } @@ -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 } @@ -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 } diff --git a/node/derivation/config.go b/node/derivation/config.go index 439acbc42..efb1ceb31 100644 --- a/node/derivation/config.go +++ b/node/derivation/config.go @@ -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"` @@ -55,6 +56,7 @@ func DefaultConfig() *Config { LogProgressInterval: DefaultLogProgressInterval, FetchBlockRange: DefaultFetchBlockRange, L2: new(types.L2Config), + L2Next: nil, // optional, only for upgrade switch } } @@ -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) diff --git a/node/derivation/derivation.go b/node/derivation/derivation.go index 5eccd2e9c..a5b013f7c 100644 --- a/node/derivation/derivation.go +++ b/node/derivation/derivation.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "math/big" - "os" "time" "github.com/morph-l2/go-ethereum" @@ -64,6 +63,10 @@ type Derivation struct { pollInterval time.Duration logProgressInterval time.Duration stop chan struct{} + + // geth upgrade config (fetched once at startup) + switchTime uint64 + useZktrie bool } type DeployContractBackend interface { @@ -78,6 +81,7 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, if err != nil { return nil, err } + // L2 geth endpoint (required - current geth) aClient, err := authclient.DialContext(context.Background(), cfg.L2.EngineAddr, cfg.L2.JwtSecret) if err != nil { return nil, err @@ -86,6 +90,24 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, if err != nil { return nil, err } + + // L2Next endpoint (optional - for upgrade switch) + var aNextClient *authclient.Client + var eNextClient *ethclient.Client + if cfg.L2Next != nil && cfg.L2Next.EngineAddr != "" && cfg.L2Next.EthAddr != "" { + aNextClient, err = authclient.DialContext(context.Background(), cfg.L2Next.EngineAddr, cfg.L2Next.JwtSecret) + if err != nil { + return nil, err + } + eNextClient, err = ethclient.Dial(cfg.L2Next.EthAddr) + if err != nil { + return nil, err + } + logger.Info("L2Next geth configured (upgrade switch enabled)", "engineAddr", cfg.L2Next.EngineAddr, "ethAddr", cfg.L2Next.EthAddr) + } else { + logger.Info("L2Next geth not configured (no upgrade switch)") + } + msgPasser, err := bindings.NewL2ToL1MessagePasser(predeploys.L2ToL1MessagePasserAddr, eClient) if err != nil { return nil, err @@ -116,6 +138,14 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, } baseHttp := NewBasicHTTPClient(cfg.BeaconRpc, logger) l1BeaconClient := NewL1BeaconClient(baseHttp) + + // Fetch geth config once at startup for root validation skip logic (with retry) + gethCfg, err := types.FetchGethConfigWithRetry(cfg.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) + return &Derivation{ ctx: ctx, db: db, @@ -129,7 +159,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(aClient, eClient, aNextClient, eNextClient, gethCfg.SwitchTime, logger), cancel: cancel, stop: make(chan struct{}), startHeight: cfg.StartHeight, @@ -140,6 +170,8 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, metrics: metrics, l1BeaconClient: l1BeaconClient, L2ToL1MessagePasser: msgPasser, + switchTime: gethCfg.SwitchTime, + useZktrie: gethCfg.UseZktrie, }, nil } @@ -246,25 +278,47 @@ func (d *Derivation) derivationBlock(ctx context.Context) { d.logger.Error("get withdrawal root failed", "error", err) return } - if !bytes.Equal(lastHeader.Root.Bytes(), batchInfo.root.Bytes()) || !bytes.Equal(withdrawalRoot[:], batchInfo.withdrawalRoot.Bytes()) { - d.metrics.SetBatchStatus(stateException) - // TODO The challenge switch is currently on and will be turned on in the future - if d.validator != nil && d.validator.ChallengeEnable() { - if err := d.validator.ChallengeState(batchInfo.batchIndex); err != nil { - d.logger.Error("challenge state failed") - return + + rootMismatch := !bytes.Equal(lastHeader.Root.Bytes(), batchInfo.root.Bytes()) + withdrawalMismatch := !bytes.Equal(withdrawalRoot[:], batchInfo.withdrawalRoot.Bytes()) + + if rootMismatch || withdrawalMismatch { + // Check if should skip validation during upgrade transition + // Skip if: (before switch && MPT geth) or (after switch && ZK geth) + skipValidation := false + if d.switchTime > 0 { + beforeSwitch := lastHeader.Time < d.switchTime + if (beforeSwitch && !d.useZktrie) || (!beforeSwitch && d.useZktrie) { + skipValidation = true + d.logger.Info("Root validation skipped during upgrade transition", + "originStateRootHash", batchInfo.root, + "deriveStateRootHash", lastHeader.Root.Hex(), + "blockTimestamp", lastHeader.Time, + "switchTime", d.switchTime, + "useZktrie", d.useZktrie, + ) } } - d.logger.Info("root hash or withdrawal hash is not equal", - "originStateRootHash", batchInfo.root, - "deriveStateRootHash", lastHeader.Root.Hex(), - "batchWithdrawalRoot", batchInfo.withdrawalRoot.Hex(), - "deriveWithdrawalRoot", common.BytesToHash(withdrawalRoot[:]).Hex(), - ) - return - } else { - d.metrics.SetBatchStatus(stateNormal) + + if !skipValidation { + d.metrics.SetBatchStatus(stateException) + // TODO The challenge switch is currently on and will be turned on in the future + if d.validator != nil && d.validator.ChallengeEnable() { + if err := d.validator.ChallengeState(batchInfo.batchIndex); err != nil { + d.logger.Error("challenge state failed") + return + } + } + d.logger.Error("root hash or withdrawal hash is not equal", + "originStateRootHash", batchInfo.root, + "deriveStateRootHash", lastHeader.Root.Hex(), + "batchWithdrawalRoot", batchInfo.withdrawalRoot.Hex(), + "deriveWithdrawalRoot", common.BytesToHash(withdrawalRoot[:]).Hex(), + ) + return + } } + d.metrics.SetBatchStatus(stateNormal) d.metrics.SetL1SyncHeight(lg.BlockNumber) } diff --git a/node/flags/flags.go b/node/flags/flags.go index d344c62ce..2c00f4a87 100644 --- a/node/flags/flags.go +++ b/node/flags/flags.go @@ -28,6 +28,18 @@ var ( EnvVar: prefixEnvVar("L2_ENGINE_RPC"), } + L2NextEthAddr = cli.StringFlag{ + Name: "l2next.eth", + Usage: "Address of next L2 geth JSON-RPC endpoints to switch to (optional, for upgrades)", + EnvVar: prefixEnvVar("L2_NEXT_ETH_RPC"), + } + + L2NextEngineAddr = cli.StringFlag{ + Name: "l2next.engine", + Usage: "Address of next L2 geth Engine JSON-RPC endpoints to switch to (optional, for upgrades)", + EnvVar: prefixEnvVar("L2_NEXT_ENGINE_RPC"), + } + L2EngineJWTSecret = cli.StringFlag{ Name: "l2.jwt-secret", Usage: "Path to JWT secret key. Keys are 32 bytes, hex encoded in a file. A new key will be generated if left empty.", @@ -298,6 +310,12 @@ var ( Value: 26660, EnvVar: prefixEnvVar("METRICS_PORT"), } + + BlsKeyCheckForkHeight = cli.Uint64Flag{ + Name: "bls-key-check-fork-height", + Usage: "The height at which the BLS key check fork occurs", + EnvVar: prefixEnvVar("BLS_KEY_CHECK_FORK_HEIGHT"), + } ) var Flags = []cli.Flag{ @@ -308,6 +326,8 @@ var Flags = []cli.Flag{ L2EthAddr, L2EngineAddr, L2EngineJWTSecret, + L2NextEthAddr, + L2NextEngineAddr, MaxL1MessageNumPerBlock, L2CrossDomainMessengerContractAddr, L2SequencerAddr, @@ -365,4 +385,6 @@ var Flags = []cli.Flag{ MetricsServerEnable, MetricsPort, MetricsHostname, + + BlsKeyCheckForkHeight, } diff --git a/node/go.mod b/node/go.mod index 5804512a2..5d05cf206 100644 --- a/node/go.mod +++ b/node/go.mod @@ -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.20260115065009-43c9e2a6c033 + github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3 github.com/prometheus/client_golang v1.17.0 github.com/spf13/viper v1.13.0 github.com/stretchr/testify v1.10.0 diff --git a/node/go.sum b/node/go.sum index 469923eff..779785295 100644 --- a/node/go.sum +++ b/node/go.sum @@ -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.20260115065009-43c9e2a6c033 h1:p6Y/8mNezpH/UH0Tspa8iLM/GgjhILQFvdGsWDDlwdQ= -github.com/morph-l2/go-ethereum v1.10.14-0.20260115065009-43c9e2a6c033/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3 h1:xGYu0xpTuIc2uN1A4Ig1I6ZN6CkqFZXlfXmPpyTtN3I= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= github.com/morph-l2/tendermint v0.3.3 h1:zsmzVJfKp+NuCr45ZUUY2ZJjnHAVLzwJLID6GxBR4i4= github.com/morph-l2/tendermint v0.3.3/go.mod h1:TtCzp9l6Z6yDUiwv3TbqKqw8Q8RKp3fSz5+adO1/Y8w= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= diff --git a/node/types/retryable_client.go b/node/types/retryable_client.go index 0e61f88bd..d77165727 100644 --- a/node/types/retryable_client.go +++ b/node/types/retryable_client.go @@ -2,8 +2,12 @@ package types import ( "context" + "encoding/json" + "fmt" "math/big" "strings" + "sync/atomic" + "time" "github.com/cenkalti/backoff/v4" "github.com/morph-l2/go-ethereum" @@ -12,6 +16,7 @@ import ( "github.com/morph-l2/go-ethereum/eth/catalyst" "github.com/morph-l2/go-ethereum/ethclient" "github.com/morph-l2/go-ethereum/ethclient/authclient" + "github.com/morph-l2/go-ethereum/rpc" tmlog "github.com/tendermint/tendermint/libs/log" ) @@ -24,30 +29,275 @@ const ( ExecutionAborted = "execution aborted" Timeout = "timed out" DiscontinuousBlockError = "discontinuous block number" + + // Geth connection retry settings + GethRetryAttempts = 60 // max retry attempts + GethRetryInterval = 5 * time.Second // interval between retries ) +// configResponse represents the eth_config RPC response (EIP-7910) +type configResponse struct { + Current *forkConfig `json:"current"` + Next *forkConfig `json:"next"` + Last *forkConfig `json:"last"` +} + +// 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"` +} + +// morphExtension contains Morph-specific configuration fields +type morphExtension struct { + UseZktrie bool `json:"useZktrie"` + MPTForkTime *uint64 `json:"mptForkTime,omitempty"` +} + +// GethConfig holds the configuration fetched from geth via eth_config API +type GethConfig struct { + SwitchTime uint64 + UseZktrie bool +} + +// FetchGethConfigWithRetry fetches geth config with retry, waiting for geth to be ready. +func FetchGethConfigWithRetry(rpcURL string, logger tmlog.Logger) (*GethConfig, error) { + var lastErr error + for i := 0; i < GethRetryAttempts; i++ { + config, err := FetchGethConfig(rpcURL, logger) + if err == nil { + return config, nil + } + lastErr = err + logger.Info("Waiting for geth to be ready...", "attempt", i+1, "error", err) + time.Sleep(GethRetryInterval) + } + return nil, fmt.Errorf("geth not ready after %d attempts: %w", GethRetryAttempts, lastErr) +} + +// FetchGethConfig fetches the geth configuration via eth_config API +func FetchGethConfig(rpcURL string, logger tmlog.Logger) (*GethConfig, error) { + client, err := rpc.Dial(rpcURL) + if err != nil { + return nil, fmt.Errorf("failed to connect to geth: %w", err) + } + defer client.Close() + + var result json.RawMessage + if err := client.Call(&result, "eth_config"); err != nil { + return nil, fmt.Errorf("eth_config call failed: %w", err) + } + + var resp configResponse + if err := json.Unmarshal(result, &resp); err != nil { + return nil, fmt.Errorf("failed to parse eth_config response: %w", err) + } + + config := &GethConfig{} + + // Get useZktrie from current config + if resp.Current != nil && resp.Current.Morph != nil { + config.UseZktrie = resp.Current.Morph.UseZktrie + logger.Info("Fetched useZktrie from geth", "useZktrie", config.UseZktrie) + } + + // Try to get mptForkTime from current config + if resp.Current != nil && resp.Current.Morph != nil && resp.Current.Morph.MPTForkTime != nil { + config.SwitchTime = *resp.Current.Morph.MPTForkTime + logger.Info("Fetched MPT fork time from geth", "mptForkTime", config.SwitchTime, "source", "current") + return config, nil + } + + // Fallback to next config + if resp.Next != nil && resp.Next.Morph != nil && resp.Next.Morph.MPTForkTime != nil { + config.SwitchTime = *resp.Next.Morph.MPTForkTime + logger.Info("Fetched MPT fork time from geth", "mptForkTime", config.SwitchTime, "source", "next") + return config, nil + } + + // Fallback to last config + if resp.Last != nil && resp.Last.Morph != nil && resp.Last.Morph.MPTForkTime != nil { + config.SwitchTime = *resp.Last.Morph.MPTForkTime + logger.Info("Fetched MPT fork time from geth", "mptForkTime", config.SwitchTime, "source", "last") + return config, nil + } + + logger.Info("MPT fork time not configured in geth, switch disabled") + return config, nil +} + type RetryableClient struct { - authClient *authclient.Client - ethClient *ethclient.Client - b backoff.BackOff - logger tmlog.Logger + authClient *authclient.Client // current geth + ethClient *ethclient.Client // current geth + nextAuthClient *authclient.Client // next geth (for upgrade switch) + nextEthClient *ethclient.Client // next geth (for upgrade switch) + switchTime uint64 // timestamp to switch to next geth + switched atomic.Bool // whether switched to next geth + 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 { +// 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 with the given switch time. +// Will retry calling the api, if the connection is refused. +// +// If nextAuthClient or nextEthClient is nil, switch is disabled and only current client is used. +// This is useful for nodes that don't need to switch geth (most nodes). +// +// The switchTime should be fetched via FetchGethConfig before calling this function. +func NewRetryableClient(authClient *authclient.Client, ethClient *ethclient.Client, nextAuthClient *authclient.Client, nextEthClient *ethclient.Client, switchTime uint64, logger tmlog.Logger) *RetryableClient { logger = logger.With("module", "retryClient") - return &RetryableClient{ - authClient: authClient, - ethClient: ethClient, - b: backoff.NewExponentialBackOff(), - logger: logger, + + // If next client is not configured, disable switch + if nextAuthClient == nil || nextEthClient == nil { + logger.Info("L2Next client not configured, switch disabled") + return &RetryableClient{ + authClient: authClient, + ethClient: ethClient, + nextAuthClient: authClient, // fallback to current + nextEthClient: ethClient, // fallback to current + switchTime: switchTime, + b: backoff.NewExponentialBackOff(), + logger: logger, + } + } + // Check if switch time has already passed at startup + now := uint64(time.Now().Unix()) + alreadySwitched := switchTime > 0 && now >= switchTime + + if alreadySwitched { + logger.Info("Switch time already passed at startup, starting with next client", + "switchTime", switchTime, + "currentTime", now) + } else { + logger.Info("Geth switch enabled", "switchTime", switchTime) + } + + rc := &RetryableClient{ + authClient: authClient, + ethClient: ethClient, + nextAuthClient: nextAuthClient, + nextEthClient: nextEthClient, + switchTime: switchTime, + b: backoff.NewExponentialBackOff(), + logger: logger, + } + + // If switch time already passed, mark as switched immediately + if alreadySwitched { + rc.switched.Store(true) + } + + return rc +} + +func (rc *RetryableClient) aClient() *authclient.Client { + if !rc.switched.Load() { + return rc.authClient + } + return rc.nextAuthClient +} + +func (rc *RetryableClient) eClient() *ethclient.Client { + if !rc.switched.Load() { + return rc.ethClient + } + return rc.nextEthClient +} + +// EnsureSwitched checks if switch time has been reached and switches to next client if needed. +// This should be called when the block is already delivered (e.g., synced via P2P) to ensure +// the client switch happens even if NewL2Block is not called. +func (rc *RetryableClient) EnsureSwitched(ctx context.Context, timeStamp uint64, number uint64) { + rc.switchClient(ctx, timeStamp, number) +} + +func (rc *RetryableClient) switchClient(ctx context.Context, timeStamp uint64, number uint64) { + if rc.switched.Load() { + return + } + if rc.switchTime == 0 { + return + } + if timeStamp < rc.switchTime { + return + } + + rc.logger.Info("========================================") + rc.logger.Info("GETH UPGRADE: Switch time reached!") + rc.logger.Info("========================================") + rc.logger.Info("Switch time reached, switching from current client to next client", + "switch_time", rc.switchTime, + "current_time", timeStamp, + "target_block", number) + rc.logger.Info("Current status: connected to current geth, waiting for next geth to sync...") + + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + startTime := time.Now() + lastLogTime := startTime + + for { + 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 + continue + } + + if remote+1 >= number { + // Get next geth's latest block hash for debugging + targetHeader, headerErr := rc.nextEthClient.HeaderByNumber(ctx, big.NewInt(int64(remote))) + targetBlockHash := "unknown" + targetStateRoot := "unknown" + if headerErr == nil && targetHeader != nil { + targetBlockHash = targetHeader.Hash().Hex() + targetStateRoot = targetHeader.Root.Hex() + } + + rc.switched.Store(true) + rc.logger.Info("========================================") + rc.logger.Info("GETH UPGRADE: Successfully switched!") + rc.logger.Info("========================================") + rc.logger.Info("Successfully switched to next client", + "remote_block", remote, + "target_block", number, + "target_block_hash", targetBlockHash, + "target_state_root", targetStateRoot, + "wait_duration", time.Since(startTime)) + return + } + + if time.Since(lastLogTime) >= 5*time.Second { + rc.logger.Error("!!! WAITING: Node BLOCKED waiting for next geth !!!", + "next_geth_block", remote, + "target_block", number, + "blocks_behind", number-remote-1, + "wait_duration", time.Since(startTime)) + lastLogTime = time.Now() + } + + <-ticker.C } } 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) + rc.switchClient(ctx, timestamp, number.Uint64()) + resp, respErr := rc.aClient().AssembleL2Block(ctx, ×tamp, number, transactions) if respErr != nil { rc.logger.Info("failed to AssembleL2Block", "error", respErr) if retryableError(respErr) { @@ -64,8 +314,9 @@ func (rc *RetryableClient) AssembleL2Block(ctx context.Context, number *big.Int, } func (rc *RetryableClient) ValidateL2Block(ctx context.Context, executableL2Data *catalyst.ExecutableL2Data) (ret bool, err error) { + rc.switchClient(ctx, executableL2Data.Timestamp, executableL2Data.Number) if retryErr := backoff.Retry(func() error { - resp, respErr := rc.authClient.ValidateL2Block(ctx, executableL2Data) + resp, respErr := rc.aClient().ValidateL2Block(ctx, executableL2Data) if respErr != nil { rc.logger.Info("failed to ValidateL2Block", "error", respErr) if retryableError(respErr) { @@ -82,10 +333,14 @@ func (rc *RetryableClient) ValidateL2Block(ctx context.Context, executableL2Data } func (rc *RetryableClient) NewL2Block(ctx context.Context, executableL2Data *catalyst.ExecutableL2Data, batchHash *common.Hash) (err error) { + rc.switchClient(ctx, executableL2Data.Timestamp, executableL2Data.Number) + if retryErr := backoff.Retry(func() error { - respErr := rc.authClient.NewL2Block(ctx, executableL2Data, batchHash) + respErr := rc.aClient().NewL2Block(ctx, executableL2Data, batchHash) if respErr != nil { - rc.logger.Info("failed to NewL2Block", "error", respErr) + rc.logger.Error("NewL2Block failed", + "block_number", executableL2Data.Number, + "error", respErr) if retryableError(respErr) { return respErr } @@ -99,8 +354,9 @@ func (rc *RetryableClient) NewL2Block(ctx context.Context, executableL2Data *cat } func (rc *RetryableClient) NewSafeL2Block(ctx context.Context, safeL2Data *catalyst.SafeL2Data) (ret *eth.Header, err error) { + rc.switchClient(ctx, safeL2Data.Timestamp, safeL2Data.Number) if retryErr := backoff.Retry(func() error { - resp, respErr := rc.authClient.NewSafeL2Block(ctx, safeL2Data) + resp, respErr := rc.aClient().NewSafeL2Block(ctx, safeL2Data) if respErr != nil { rc.logger.Info("failed to NewSafeL2Block", "error", respErr) if retryableError(respErr) { @@ -118,7 +374,7 @@ 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) + respErr := rc.aClient().CommitBatch(ctx, batch, signatures) if respErr != nil { rc.logger.Info("failed to CommitBatch", "error", respErr) if retryableError(respErr) { @@ -135,7 +391,7 @@ 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) + respErr := rc.aClient().AppendBlsSignature(ctx, batchHash, signature) if respErr != nil { rc.logger.Info("failed to call AppendBlsSignature", "error", respErr) if retryableError(respErr) { @@ -152,7 +408,7 @@ 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) + resp, respErr := rc.eClient().BlockNumber(ctx) if respErr != nil { rc.logger.Info("failed to call BlockNumber", "error", respErr) if retryableError(respErr) { @@ -170,7 +426,7 @@ 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) + resp, respErr := rc.eClient().HeaderByNumber(ctx, blockNumber) if respErr != nil { rc.logger.Info("failed to call BlockNumber", "error", respErr) if retryableError(respErr) { @@ -188,7 +444,7 @@ 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) + resp, respErr := rc.eClient().CallContract(ctx, call, blockNumber) if respErr != nil { rc.logger.Info("failed to call eth_call", "error", respErr) if retryableError(respErr) { @@ -206,7 +462,7 @@ 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) + resp, respErr := rc.eClient().CodeAt(ctx, contract, blockNumber) if respErr != nil { rc.logger.Info("failed to call eth_getCode", "error", respErr) if retryableError(respErr) { diff --git a/ops/l2-genesis/go.mod b/ops/l2-genesis/go.mod index 54625e1be..3ef632bb4 100644 --- a/ops/l2-genesis/go.mod +++ b/ops/l2-genesis/go.mod @@ -6,7 +6,7 @@ replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3. require ( github.com/holiman/uint256 v1.2.4 - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3 github.com/stretchr/testify v1.10.0 github.com/urfave/cli v1.22.17 ) diff --git a/ops/l2-genesis/go.sum b/ops/l2-genesis/go.sum index 34798fa73..e0e757cec 100644 --- a/ops/l2-genesis/go.sum +++ b/ops/l2-genesis/go.sum @@ -141,8 +141,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.20260206065010-7b2a9bb930e3 h1:xGYu0xpTuIc2uN1A4Ig1I6ZN6CkqFZXlfXmPpyTtN3I= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3/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= diff --git a/ops/tools/go.mod b/ops/tools/go.mod index 031d413b2..73d975c04 100644 --- a/ops/tools/go.mod +++ b/ops/tools/go.mod @@ -5,7 +5,7 @@ go 1.24.0 replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.3 require ( - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3 github.com/tendermint/tendermint v0.35.9 ) diff --git a/ops/tools/go.sum b/ops/tools/go.sum index 175de8ac4..1110b553b 100644 --- a/ops/tools/go.sum +++ b/ops/tools/go.sum @@ -163,8 +163,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.20260206065010-7b2a9bb930e3 h1:xGYu0xpTuIc2uN1A4Ig1I6ZN6CkqFZXlfXmPpyTtN3I= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= github.com/morph-l2/tendermint v0.3.3 h1:zsmzVJfKp+NuCr45ZUUY2ZJjnHAVLzwJLID6GxBR4i4= github.com/morph-l2/tendermint v0.3.3/go.mod h1:TtCzp9l6Z6yDUiwv3TbqKqw8Q8RKp3fSz5+adO1/Y8w= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= diff --git a/oracle/go.mod b/oracle/go.mod index 2286651f0..c1d3fa921 100644 --- a/oracle/go.mod +++ b/oracle/go.mod @@ -7,7 +7,7 @@ replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3. require ( github.com/go-kit/kit v0.12.0 github.com/morph-l2/externalsign v0.3.1 - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3 github.com/prometheus/client_golang v1.17.0 github.com/stretchr/testify v1.10.0 github.com/tendermint/tendermint v0.35.9 diff --git a/oracle/go.sum b/oracle/go.sum index 15719693c..f2e958e44 100644 --- a/oracle/go.sum +++ b/oracle/go.sum @@ -174,8 +174,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/externalsign v0.3.1 h1:UYFDZFB0L85A4rDvuwLNBiGEi0kSmg9AZ2v8Q5O4dQo= github.com/morph-l2/externalsign v0.3.1/go.mod h1:b6NJ4GUiiG/gcSJsp3p8ExsIs4ZdphlrVALASnVoGJE= -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.20260206065010-7b2a9bb930e3 h1:xGYu0xpTuIc2uN1A4Ig1I6ZN6CkqFZXlfXmPpyTtN3I= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= github.com/morph-l2/tendermint v0.3.3 h1:zsmzVJfKp+NuCr45ZUUY2ZJjnHAVLzwJLID6GxBR4i4= github.com/morph-l2/tendermint v0.3.3/go.mod h1:TtCzp9l6Z6yDUiwv3TbqKqw8Q8RKp3fSz5+adO1/Y8w= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= diff --git a/token-price-oracle/go.mod b/token-price-oracle/go.mod index 135e77688..1f36f38ae 100644 --- a/token-price-oracle/go.mod +++ b/token-price-oracle/go.mod @@ -9,7 +9,7 @@ replace ( require ( github.com/morph-l2/externalsign v0.3.1 - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3 github.com/prometheus/client_golang v1.17.0 github.com/sirupsen/logrus v1.9.3 github.com/urfave/cli v1.22.17 diff --git a/token-price-oracle/go.sum b/token-price-oracle/go.sum index bd174e5ae..8921975fc 100644 --- a/token-price-oracle/go.sum +++ b/token-price-oracle/go.sum @@ -147,8 +147,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/externalsign v0.3.1 h1:UYFDZFB0L85A4rDvuwLNBiGEi0kSmg9AZ2v8Q5O4dQo= github.com/morph-l2/externalsign v0.3.1/go.mod h1:b6NJ4GUiiG/gcSJsp3p8ExsIs4ZdphlrVALASnVoGJE= -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.20260206065010-7b2a9bb930e3 h1:xGYu0xpTuIc2uN1A4Ig1I6ZN6CkqFZXlfXmPpyTtN3I= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3/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= diff --git a/tx-submitter/go.mod b/tx-submitter/go.mod index 6b1e3cb31..dfde530e6 100644 --- a/tx-submitter/go.mod +++ b/tx-submitter/go.mod @@ -9,7 +9,7 @@ require ( github.com/crate-crypto/go-eth-kzg v1.4.0 github.com/holiman/uint256 v1.2.4 github.com/morph-l2/externalsign v0.3.1 - github.com/morph-l2/go-ethereum v1.10.14-0.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3 github.com/prometheus/client_golang v1.17.0 github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a diff --git a/tx-submitter/go.sum b/tx-submitter/go.sum index 63d152c58..15256339c 100644 --- a/tx-submitter/go.sum +++ b/tx-submitter/go.sum @@ -163,8 +163,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/externalsign v0.3.1 h1:UYFDZFB0L85A4rDvuwLNBiGEi0kSmg9AZ2v8Q5O4dQo= github.com/morph-l2/externalsign v0.3.1/go.mod h1:b6NJ4GUiiG/gcSJsp3p8ExsIs4ZdphlrVALASnVoGJE= -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.20260206065010-7b2a9bb930e3 h1:xGYu0xpTuIc2uN1A4Ig1I6ZN6CkqFZXlfXmPpyTtN3I= +github.com/morph-l2/go-ethereum v1.10.14-0.20260206065010-7b2a9bb930e3/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= diff --git a/tx-submitter/services/rollup.go b/tx-submitter/services/rollup.go index caa5471a4..730422568 100644 --- a/tx-submitter/services/rollup.go +++ b/tx-submitter/services/rollup.go @@ -21,6 +21,7 @@ import ( "github.com/morph-l2/go-ethereum/crypto" "github.com/morph-l2/go-ethereum/crypto/kzg4844" "github.com/morph-l2/go-ethereum/eth" + "github.com/morph-l2/go-ethereum/ethclient" "github.com/morph-l2/go-ethereum/log" "github.com/morph-l2/go-ethereum/params" "github.com/morph-l2/go-ethereum/rpc" @@ -833,10 +834,26 @@ func (r *Rollup) finalize() error { log.Info("batch inside challenge window, wait") return nil } - // finalize - // get next batch nextBatchIndex := target.Uint64() + 1 + var l2Clients []iface.L2Client + zkmRpc := "http://morph-geth-a:8545" + upgradeBatchIndex := uint64(10) + + if nextBatchIndex <= upgradeBatchIndex { + l2Client, err := ethclient.DialContext(context.Background(), zkmRpc) + if err != nil { + log.Error("failed to connect to L2 provider", "url", zkmRpc) + return err + } + l2Clients = append(l2Clients, l2Client) + } else { + l2Clients = append(l2Clients, r.L2Clients...) + } + if len(l2Clients) == 0 { + return fmt.Errorf("cannot connect to any l2 rpc") + } + batch, err := GetRollupBatchByIndex(nextBatchIndex, r.L2Clients) if err != nil { log.Error("get next batch by index error",