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 6e55077de..11b3c35d7 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/bindings/go.mod b/bindings/go.mod index cb76dba78..1dd5535d8 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.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 diff --git a/bindings/go.sum b/bindings/go.sum index a479ed434..4c2a44fcf 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.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= diff --git a/contracts/go.mod b/contracts/go.mod index 05b2b2912..e48a7c63b 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.20260113015804-82683159dfd0 github.com/stretchr/testify v1.10.0 ) diff --git a/contracts/go.sum b/contracts/go.sum index 319f1f2b8..c58d26687 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.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= diff --git a/node/core/config.go b/node/core/config.go index cd3e80aae..1c85c045b 100644 --- a/node/core/config.go +++ b/node/core/config.go @@ -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"` @@ -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, @@ -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 { diff --git a/node/core/executor.go b/node/core/executor.go index 0d8895658..2757600f3 100644 --- a/node/core/executor.go +++ b/node/core/executor.go @@ -71,6 +71,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 +81,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 + gethCfg, err := types.FetchGethConfig(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 @@ -283,16 +308,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 +372,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..2592b2e45 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 + gethCfg, err := types.FetchGethConfig(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.Error("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.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 + } } + d.metrics.SetBatchStatus(stateNormal) d.metrics.SetL1SyncHeight(lg.BlockNumber) } diff --git a/node/flags/flags.go b/node/flags/flags.go index 3bb690e5c..f692391c0 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.", @@ -300,6 +312,8 @@ var Flags = []cli.Flag{ L2EthAddr, L2EngineAddr, L2EngineJWTSecret, + L2NextEthAddr, + L2NextEngineAddr, MaxL1MessageNumPerBlock, L2CrossDomainMessengerContractAddr, L2SequencerAddr, diff --git a/node/go.mod b/node/go.mod index e8f9a3353..f237142a9 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.20251219060125-03910bc750a2 + github.com/morph-l2/go-ethereum v1.10.14-0.20260113015804-82683159dfd0 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 d11447ab7..7de2c2e1d 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.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/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= diff --git a/node/types/retryable_client.go b/node/types/retryable_client.go index 3d3ad949d..53d94f35e 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" ) @@ -26,28 +31,246 @@ const ( DiscontinuousBlockError = "discontinuous block number" ) +// 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 +} + +// 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 { +// 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 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 +287,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 +306,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 +327,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 +347,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 +364,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 +381,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 +399,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 +417,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 +435,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/oracle/go.mod b/oracle/go.mod index 2c115e81a..a8f8274b4 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.20260113015804-82683159dfd0 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 e4d6c750c..d8de0e727 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.20260113015804-82683159dfd0 h1:tu77ClhPcySgkweTINJBoLkIdpKKjrDF+4JPMOBCBLk= +github.com/morph-l2/go-ethereum v1.10.14-0.20260113015804-82683159dfd0/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= diff --git a/token-price-oracle/go.mod b/token-price-oracle/go.mod index 135e77688..ac502ac44 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.20260113015804-82683159dfd0 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..15cb477e4 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.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= diff --git a/tx-submitter/go.mod b/tx-submitter/go.mod index 546e3b215..547e3e379 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.20260113015804-82683159dfd0 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 e98ef3ac1..8adf903e5 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.20260113015804-82683159dfd0 h1:tu77ClhPcySgkweTINJBoLkIdpKKjrDF+4JPMOBCBLk= +github.com/morph-l2/go-ethereum v1.10.14-0.20260113015804-82683159dfd0/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=