From 06e7c74e851a35ad2c33a5de6fafa76b0a292b03 Mon Sep 17 00:00:00 2001 From: Chen Chen <34592639+envestcc@users.noreply.github.com> Date: Tue, 7 Feb 2023 02:34:46 +0800 Subject: [PATCH] [nodeinfo] broadcast node's height info into p2p network (#3744) --- chainservice/builder.go | 8 + chainservice/chainservice.go | 18 ++ config/config.go | 9 +- dispatcher/dispatcher.go | 52 +++- dispatcher/dispatcher_test.go | 10 + e2etest/nodeinfo_test.go | 132 +++++++++++ go.mod | 3 +- go.sum | 5 +- misc/scripts/mockgen.sh | 7 + nodeinfo/config.go | 22 ++ nodeinfo/manager.go | 224 ++++++++++++++++++ nodeinfo/manager_test.go | 236 +++++++++++++++++++ server/itx/server.go | 1 - test/mock/mock_dispatcher/mock_dispatcher.go | 28 +++ test/mock/mock_nodeinfo/mock_manager.go | 117 +++++++++ 15 files changed, 859 insertions(+), 13 deletions(-) create mode 100644 e2etest/nodeinfo_test.go create mode 100644 nodeinfo/config.go create mode 100644 nodeinfo/manager.go create mode 100644 nodeinfo/manager_test.go create mode 100644 test/mock/mock_nodeinfo/mock_manager.go diff --git a/chainservice/builder.go b/chainservice/builder.go index 12703c2b41..86cbc66cf8 100644 --- a/chainservice/builder.go +++ b/chainservice/builder.go @@ -31,6 +31,7 @@ import ( "github.com/iotexproject/iotex-core/consensus" rp "github.com/iotexproject/iotex-core/consensus/scheme/rolldpos" "github.com/iotexproject/iotex-core/db" + "github.com/iotexproject/iotex-core/nodeinfo" "github.com/iotexproject/iotex-core/p2p" "github.com/iotexproject/iotex-core/pkg/log" "github.com/iotexproject/iotex-core/state/factory" @@ -377,6 +378,12 @@ func (builder *Builder) createBlockchain(forSubChain, forTest bool) blockchain.B return blockchain.NewBlockchain(builder.cfg.Chain, builder.cfg.Genesis, builder.cs.blockdao, factory.NewMinter(builder.cs.factory, builder.cs.actpool), chainOpts...) } +func (builder *Builder) buildNodeInfoManager() { + dm := nodeinfo.NewInfoManager(&builder.cfg.NodeInfo, builder.cs.p2pAgent, builder.cs.chain, builder.cfg.Chain.ProducerPrivateKey()) + builder.cs.nodeInfoManager = dm + builder.cs.lifecycle.Add(dm) +} + func (builder *Builder) buildBlockSyncer() error { if builder.cs.blocksync != nil { return nil @@ -622,6 +629,7 @@ func (builder *Builder) build(forSubChain, forTest bool) (*ChainService, error) if err := builder.buildBlockSyncer(); err != nil { return nil, err } + builder.buildNodeInfoManager() cs := builder.cs builder.cs = nil diff --git a/chainservice/chainservice.go b/chainservice/chainservice.go index 4b46a2255f..fc41d3339d 100644 --- a/chainservice/chainservice.go +++ b/chainservice/chainservice.go @@ -31,6 +31,7 @@ import ( "github.com/iotexproject/iotex-core/blockindex" "github.com/iotexproject/iotex-core/blocksync" "github.com/iotexproject/iotex-core/consensus" + "github.com/iotexproject/iotex-core/nodeinfo" "github.com/iotexproject/iotex-core/p2p" "github.com/iotexproject/iotex-core/pkg/lifecycle" "github.com/iotexproject/iotex-core/pkg/log" @@ -84,6 +85,7 @@ type ChainService struct { candidateIndexer *poll.CandidateIndexer candBucketsIndexer *staking.CandidatesBucketsIndexer registry *protocol.Registry + nodeInfoManager *nodeinfo.InfoManager } // Start starts the server @@ -163,6 +165,17 @@ func (cs *ChainService) HandleConsensusMsg(msg *iotextypes.ConsensusMessage) err return cs.consensus.HandleConsensusMsg(msg) } +// HandleNodeInfo handles nodeinfo message. +func (cs *ChainService) HandleNodeInfo(ctx context.Context, peer string, msg *iotextypes.NodeInfo) error { + cs.nodeInfoManager.HandleNodeInfo(ctx, peer, msg) + return nil +} + +// HandleNodeInfoRequest handles request node info message +func (cs *ChainService) HandleNodeInfoRequest(ctx context.Context, peer peer.AddrInfo, msg *iotextypes.NodeInfoRequest) error { + return cs.nodeInfoManager.HandleNodeInfoRequest(ctx, peer) +} + // ChainID returns ChainID. func (cs *ChainService) ChainID() uint32 { return cs.chain.ChainID() } @@ -196,6 +209,11 @@ func (cs *ChainService) BlockSync() blocksync.BlockSync { return cs.blocksync } +// NodeInfoManager returns the delegate manager +func (cs *ChainService) NodeInfoManager() *nodeinfo.InfoManager { + return cs.nodeInfoManager +} + // Registry returns a pointer to the registry func (cs *ChainService) Registry() *protocol.Registry { return cs.registry } diff --git a/config/config.go b/config/config.go index 352a06d276..0ad3285ffb 100644 --- a/config/config.go +++ b/config/config.go @@ -23,6 +23,7 @@ import ( "github.com/iotexproject/iotex-core/consensus/consensusfsm" "github.com/iotexproject/iotex-core/db" "github.com/iotexproject/iotex-core/dispatcher" + "github.com/iotexproject/iotex-core/nodeinfo" "github.com/iotexproject/iotex-core/p2p" "github.com/iotexproject/iotex-core/pkg/log" ) @@ -77,9 +78,10 @@ var ( StartSubChainInterval: 10 * time.Second, SystemLogDBPath: "/var/log", }, - DB: db.DefaultConfig, - Indexer: blockindex.DefaultConfig, - Genesis: genesis.Default, + DB: db.DefaultConfig, + Indexer: blockindex.DefaultConfig, + Genesis: genesis.Default, + NodeInfo: nodeinfo.DefaultConfig, } // ErrInvalidCfg indicates the invalid config value @@ -129,6 +131,7 @@ type ( Log log.GlobalConfig `yaml:"log"` SubLogs map[string]log.GlobalConfig `yaml:"subLogs"` Genesis genesis.Genesis `yaml:"genesis"` + NodeInfo nodeinfo.Config `yaml:"nodeinfo"` } // Validate is the interface of validating the config diff --git a/dispatcher/dispatcher.go b/dispatcher/dispatcher.go index c097b57aed..832d1e2d24 100644 --- a/dispatcher/dispatcher.go +++ b/dispatcher/dispatcher.go @@ -53,6 +53,8 @@ type Subscriber interface { HandleBlock(context.Context, string, *iotextypes.Block) error HandleSyncRequest(context.Context, peer.AddrInfo, *iotexrpc.BlockSync) error HandleConsensusMsg(*iotextypes.ConsensusMessage) error + HandleNodeInfoRequest(context.Context, peer.AddrInfo, *iotextypes.NodeInfoRequest) error + HandleNodeInfo(context.Context, string, *iotextypes.NodeInfo) error } // Dispatcher is used by peers, handles incoming block and header notifications and relays announcements of new blocks. @@ -69,12 +71,14 @@ type Dispatcher interface { HandleTell(context.Context, uint32, peer.AddrInfo, proto.Message) } -var requestMtc = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "iotex_dispatch_request", - Help: "Dispatcher request counter.", - }, - []string{"method", "succeed"}, +var ( + requestMtc = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "iotex_dispatch_request", + Help: "Dispatcher request counter.", + }, + []string{"method", "succeed"}, + ) ) func init() { @@ -430,6 +434,10 @@ func (d *IotxDispatcher) HandleBroadcast(ctx context.Context, chainID uint32, pe } case *iotextypes.Block: d.dispatchBlock(ctx, chainID, peer, message.(*iotextypes.Block)) + case *iotextypes.NodeInfo: + if err := subscriber.HandleNodeInfo(ctx, peer, msg); err != nil { + log.L().Warn("Failed to handle node info message.", zap.Error(err)) + } default: msgType, _ := goproto.GetTypeFromRPCMsg(message) log.L().Warn("Unexpected msgType handled by HandleBroadcast.", zap.Any("msgType", msgType)) @@ -447,11 +455,43 @@ func (d *IotxDispatcher) HandleTell(ctx context.Context, chainID uint32, peer pe d.dispatchBlockSyncReq(ctx, chainID, peer, message) case iotexrpc.MessageType_BLOCK: d.dispatchBlock(ctx, chainID, peer.ID.Pretty(), message.(*iotextypes.Block)) + case iotexrpc.MessageType_NODE_INFO_REQUEST: + d.dispatchNodeInfoRequest(ctx, chainID, peer, message.(*iotextypes.NodeInfoRequest)) + case iotexrpc.MessageType_NODE_INFO: + d.dispatchNodeInfo(ctx, chainID, peer.ID.Pretty(), message.(*iotextypes.NodeInfo)) default: log.L().Warn("Unexpected msgType handled by HandleTell.", zap.Any("msgType", msgType)) } } +func (d *IotxDispatcher) dispatchNodeInfoRequest(ctx context.Context, chainID uint32, peer peer.AddrInfo, message *iotextypes.NodeInfoRequest) { + if atomic.LoadInt32(&d.shutdown) != 0 { + return + } + subscriber := d.subscriber(chainID) + if subscriber == nil { + log.L().Debug("no subscriber for this chain id, drop the node info", zap.Uint32("chain id", chainID)) + return + } + if err := subscriber.HandleNodeInfoRequest(ctx, peer, message); err != nil { + log.L().Warn("failed to handle request node info message", zap.Error(err)) + } +} + +func (d *IotxDispatcher) dispatchNodeInfo(ctx context.Context, chainID uint32, peerID string, message *iotextypes.NodeInfo) { + if atomic.LoadInt32(&d.shutdown) != 0 { + return + } + subscriber := d.subscriber(chainID) + if subscriber == nil { + log.L().Debug("no subscriber for this chain id, drop the node info", zap.Uint32("chain id", chainID)) + return + } + if err := subscriber.HandleNodeInfo(ctx, peerID, message); err != nil { + log.L().Warn("failed to handle node info message", zap.Error(err)) + } +} + func (d *IotxDispatcher) updateEventAudit(t iotexrpc.MessageType) { d.eventAuditLock.Lock() defer d.eventAuditLock.Unlock() diff --git a/dispatcher/dispatcher_test.go b/dispatcher/dispatcher_test.go index bc56e26e07..9c07a2c132 100644 --- a/dispatcher/dispatcher_test.go +++ b/dispatcher/dispatcher_test.go @@ -52,6 +52,8 @@ func setTestCase() []proto.Message { &iotextypes.Block{}, &iotexrpc.BlockSync{}, &testingpb.TestPayload{}, + &iotextypes.NodeInfoRequest{}, + &iotextypes.NodeInfo{}, } } @@ -98,3 +100,11 @@ func (ds *dummySubscriber) HandleSyncRequest(context.Context, peer.AddrInfo, *io func (ds *dummySubscriber) HandleAction(context.Context, *iotextypes.Action) error { return nil } func (ds *dummySubscriber) HandleConsensusMsg(*iotextypes.ConsensusMessage) error { return nil } + +func (ds *dummySubscriber) HandleNodeInfoRequest(context.Context, peer.AddrInfo, *iotextypes.NodeInfoRequest) error { + return nil +} + +func (ds *dummySubscriber) HandleNodeInfo(context.Context, string, *iotextypes.NodeInfo) error { + return nil +} diff --git a/e2etest/nodeinfo_test.go b/e2etest/nodeinfo_test.go new file mode 100644 index 0000000000..0eb4b9d95b --- /dev/null +++ b/e2etest/nodeinfo_test.go @@ -0,0 +1,132 @@ +// Copyright (c) 2023 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package e2etest + +import ( + "context" + "testing" + "time" + + "github.com/iotexproject/iotex-core/blockchain/genesis" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/server/itx" + "github.com/iotexproject/iotex-core/testutil" + "github.com/stretchr/testify/require" +) + +func newConfigForNodeInfoTest(triePath, dBPath, idxDBPath string) (config.Config, func(), error) { + cfg, err := newTestConfig() + if err != nil { + return cfg, nil, err + } + testTriePath, err := testutil.PathOfTempFile(triePath) + if err != nil { + return cfg, nil, err + } + testDBPath, err := testutil.PathOfTempFile(dBPath) + if err != nil { + return cfg, nil, err + } + indexDBPath, err := testutil.PathOfTempFile(idxDBPath) + if err != nil { + return cfg, nil, err + } + cfg.Chain.TrieDBPatchFile = "" + cfg.Chain.TrieDBPath = testTriePath + cfg.Chain.ChainDBPath = testDBPath + cfg.Chain.IndexDBPath = indexDBPath + return cfg, func() { + testutil.CleanupPath(testTriePath) + testutil.CleanupPath(testDBPath) + testutil.CleanupPath(indexDBPath) + }, nil +} + +func TestBroadcastNodeInfo(t *testing.T) { + require := require.New(t) + + cfgSender, teardown, err := newConfigForNodeInfoTest("trie.test", "db.test", "indexdb.test") + require.NoError(err) + defer teardown() + cfgSender.NodeInfo.EnableBroadcastNodeInfo = true + cfgSender.NodeInfo.BroadcastNodeInfoInterval = time.Second + cfgSender.Network.ReconnectInterval = 2 * time.Second + srvSender, err := itx.NewServer(cfgSender) + require.NoError(err) + ctxSender := genesis.WithGenesisContext(context.Background(), cfgSender.Genesis) + err = srvSender.Start(ctxSender) + require.NoError(err) + defer func() { + require.NoError(srvSender.Stop(ctxSender)) + }() + addrsSender, err := srvSender.P2PAgent().Self() + require.NoError(err) + + cfgReciever, teardown2, err := newConfigForNodeInfoTest("trie2.test", "db2.test", "indexdb2.test") + require.NoError(err) + defer teardown2() + cfgReciever.Network.BootstrapNodes = []string{validNetworkAddr(addrsSender)} + cfgReciever.Network.ReconnectInterval = 2 * time.Second + srvReciever, err := itx.NewServer(cfgReciever) + require.NoError(err) + ctxReciever := genesis.WithGenesisContext(context.Background(), cfgReciever.Genesis) + err = srvReciever.Start(ctxReciever) + require.NoError(err) + defer func() { + require.NoError(srvReciever.Stop(ctxReciever)) + }() + + // check if there is sender's info in reciever delegatemanager + require.NoError(srvSender.ChainService(cfgSender.Chain.ID).NodeInfoManager().BroadcastNodeInfo(context.Background())) + time.Sleep(1 * time.Second) + addrSender := cfgSender.Chain.ProducerAddress().String() + _, ok := srvReciever.ChainService(cfgReciever.Chain.ID).NodeInfoManager().GetNodeByAddr(addrSender) + require.True(ok) +} + +func TestUnicastNodeInfo(t *testing.T) { + require := require.New(t) + + cfgReciever, teardown2, err := newConfigForNodeInfoTest("trie2.test", "db2.test", "indexdb2.test") + require.NoError(err) + defer teardown2() + cfgReciever.Network.ReconnectInterval = 2 * time.Second + srvReciever, err := itx.NewServer(cfgReciever) + require.NoError(err) + ctxReciever := genesis.WithGenesisContext(context.Background(), cfgReciever.Genesis) + err = srvReciever.Start(ctxReciever) + require.NoError(err) + defer func() { + require.NoError(srvReciever.Stop(ctxReciever)) + }() + addrsReciever, err := srvReciever.P2PAgent().Self() + require.NoError(err) + + cfgSender, teardown, err := newConfigForNodeInfoTest("trie.test", "db.test", "indexdb.test") + require.NoError(err) + defer teardown() + cfgSender.Network.ReconnectInterval = 2 * time.Second + cfgSender.Network.BootstrapNodes = []string{validNetworkAddr(addrsReciever)} + srvSender, err := itx.NewServer(cfgSender) + require.NoError(err) + ctxSender := genesis.WithGenesisContext(context.Background(), cfgSender.Genesis) + err = srvSender.Start(ctxSender) + require.NoError(err) + defer func() { + require.NoError(srvSender.Stop(ctxSender)) + }() + + // check if there is reciever's info in sender delegatemanager + peerReciever, err := srvReciever.P2PAgent().Info() + require.NoError(err) + dmSender := srvSender.ChainService(cfgSender.Chain.ID).NodeInfoManager() + err = dmSender.RequestSingleNodeInfoAsync(context.Background(), peerReciever) + require.NoError(err) + time.Sleep(1 * time.Second) + addrReciever := cfgReciever.Chain.ProducerAddress().String() + _, ok := dmSender.GetNodeByAddr(addrReciever) + require.True(ok) +} diff --git a/go.mod b/go.mod index f50a885814..659a900b4f 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( require ( github.com/cespare/xxhash/v2 v2.1.2 + github.com/prometheus/client_model v0.2.0 github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible github.com/shirou/gopsutil/v3 v3.22.2 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.34.0 @@ -173,7 +174,6 @@ require ( github.com/pierrec/lz4 v2.0.5+incompatible // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/rjeczalik/notify v0.9.2 // indirect @@ -195,6 +195,7 @@ require ( golang.org/x/sys v0.3.0 // indirect golang.org/x/term v0.3.0 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect + golang.org/x/tools v0.2.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5a27346aef..755e909c63 100644 --- a/go.sum +++ b/go.sum @@ -1397,7 +1397,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1606,7 +1606,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190212162355-a5947ffaace3/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= diff --git a/misc/scripts/mockgen.sh b/misc/scripts/mockgen.sh index d96bc1cf73..fb3a437fb5 100755 --- a/misc/scripts/mockgen.sh +++ b/misc/scripts/mockgen.sh @@ -147,3 +147,10 @@ mockgen -destination=./test/mock/mock_web3server/mock_web3server.go \ -source=./api/web3server.go \ -package=mock_web3server \ Web3Handler + +mkdir -p ./test/mock/mock_nodeinfo +mockgen -destination=./test/mock/mock_nodeinfo/mock_manager.go \ + -source=./nodeinfo/manager.go \ + -package=mock_nodeinfo \ + transmitter chain + diff --git a/nodeinfo/config.go b/nodeinfo/config.go new file mode 100644 index 0000000000..d248079a38 --- /dev/null +++ b/nodeinfo/config.go @@ -0,0 +1,22 @@ +// Copyright (c) 2022 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package nodeinfo + +import "time" + +// Config node config +type Config struct { + EnableBroadcastNodeInfo bool `yaml:"enableBroadcastNodeInfo"` + BroadcastNodeInfoInterval time.Duration `yaml:"broadcastNodeInfoInterval"` + NodeMapSize int `yaml:"nodeMapSize"` +} + +// DefaultConfig is the default config +var DefaultConfig = Config{ + EnableBroadcastNodeInfo: false, + BroadcastNodeInfoInterval: 5 * time.Minute, + NodeMapSize: 1000, +} diff --git a/nodeinfo/manager.go b/nodeinfo/manager.go new file mode 100644 index 0000000000..61cec9b932 --- /dev/null +++ b/nodeinfo/manager.go @@ -0,0 +1,224 @@ +// Copyright (c) 2022 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package nodeinfo + +import ( + "context" + "time" + + "github.com/iotexproject/go-pkgs/cache/lru" + "github.com/iotexproject/go-pkgs/crypto" + "github.com/iotexproject/go-pkgs/hash" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/libp2p/go-libp2p-core/peer" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/iotexproject/iotex-core/pkg/log" + "github.com/iotexproject/iotex-core/pkg/routine" + "github.com/iotexproject/iotex-core/pkg/util/byteutil" + "github.com/iotexproject/iotex-core/pkg/version" +) + +type ( + transmitter interface { + BroadcastOutbound(context.Context, proto.Message) error + UnicastOutbound(context.Context, peer.AddrInfo, proto.Message) error + Info() (peer.AddrInfo, error) + } + + chain interface { + TipHeight() uint64 + } + + // Info node infomation + Info struct { + Version string + Height uint64 + Timestamp time.Time + Address string + PeerID string + } + + // InfoManager manage delegate node info + InfoManager struct { + version string + address string + nodeMap *lru.Cache + broadcastTask *routine.RecurringTask + transmitter transmitter + chain chain + privKey crypto.PrivateKey + } +) + +var _nodeInfoHeightGauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "iotex_node_info_height_gauge", + Help: "height info of node", + }, + []string{"address", "version"}, +) + +func init() { + prometheus.MustRegister(_nodeInfoHeightGauge) +} + +// NewInfoManager new info manager +func NewInfoManager(cfg *Config, t transmitter, h chain, privKey crypto.PrivateKey) *InfoManager { + dm := &InfoManager{ + nodeMap: lru.New(cfg.NodeMapSize), + transmitter: t, + chain: h, + privKey: privKey, + version: version.PackageVersion, + address: privKey.PublicKey().Address().String(), + } + + dm.broadcastTask = routine.NewRecurringTask(func() { + // delegates or nodes who are turned on will broadcast + if cfg.EnableBroadcastNodeInfo || dm.isDelegate() { + if err := dm.BroadcastNodeInfo(context.Background()); err != nil { + log.L().Error("nodeinfo manager broadcast node info failed", zap.Error(err)) + } + } else { + log.L().Debug("nodeinfo manager general node disabled node info broadcast") + } + }, cfg.BroadcastNodeInfoInterval) + return dm +} + +// Start start delegate broadcast task +func (dm *InfoManager) Start(ctx context.Context) error { + if dm.broadcastTask != nil { + return dm.broadcastTask.Start(ctx) + } + return nil +} + +// Stop stop delegate broadcast task +func (dm *InfoManager) Stop(ctx context.Context) error { + if dm.broadcastTask != nil { + return dm.broadcastTask.Stop(ctx) + } + return nil +} + +// HandleNodeInfo handle node info message +func (dm *InfoManager) HandleNodeInfo(ctx context.Context, peerID string, msg *iotextypes.NodeInfo) { + log.L().Debug("nodeinfo manager handle node info") + // recover pubkey + hash := hashNodeInfo(msg.Info) + pubKey, err := crypto.RecoverPubkey(hash[:], msg.Signature) + if err != nil { + log.L().Warn("nodeinfo manager recover pubkey failed", zap.Error(err)) + return + } + // verify signature + if addr := pubKey.Address().String(); addr != msg.Info.Address { + log.L().Warn("nodeinfo manager node info message verify failed", zap.String("expected", addr), zap.String("recieved", msg.Info.Address)) + return + } + + dm.updateNode(&Info{ + Version: msg.Info.Version, + Height: msg.Info.Height, + Timestamp: msg.Info.Timestamp.AsTime(), + Address: msg.Info.Address, + PeerID: peerID, + }) +} + +// updateNode update node info +func (dm *InfoManager) updateNode(node *Info) { + addr := node.Address + // update dm.nodeMap + dm.nodeMap.Add(addr, *node) + // update metric + _nodeInfoHeightGauge.WithLabelValues(addr, node.Version).Set(float64(node.Height)) +} + +// GetNodeByAddr get node info by address +func (dm *InfoManager) GetNodeByAddr(addr string) (Info, bool) { + info, ok := dm.nodeMap.Get(addr) + if !ok { + return Info{}, false + } + return info.(Info), true +} + +// BroadcastNodeInfo broadcast request node info message +func (dm *InfoManager) BroadcastNodeInfo(ctx context.Context) error { + log.L().Debug("nodeinfo manager broadcast node info") + req, err := dm.genNodeInfoMsg() + if err != nil { + return err + } + // broadcast request meesage + if err := dm.transmitter.BroadcastOutbound(ctx, req); err != nil { + return err + } + // manually update self node info for broadcast message to myself will be ignored + peer, err := dm.transmitter.Info() + if err != nil { + return err + } + dm.updateNode(&Info{ + Version: req.Info.Version, + Height: req.Info.Height, + Timestamp: req.Info.Timestamp.AsTime(), + Address: req.Info.Address, + PeerID: peer.ID.Pretty(), + }) + return nil +} + +// RequestSingleNodeInfoAsync unicast request node info message +func (dm *InfoManager) RequestSingleNodeInfoAsync(ctx context.Context, peer peer.AddrInfo) error { + log.L().Debug("nodeinfo manager request one node info", zap.String("peer", peer.ID.Pretty())) + return dm.transmitter.UnicastOutbound(ctx, peer, &iotextypes.NodeInfoRequest{}) +} + +// HandleNodeInfoRequest tell node info to peer +func (dm *InfoManager) HandleNodeInfoRequest(ctx context.Context, peer peer.AddrInfo) error { + log.L().Debug("nodeinfo manager tell node info", zap.Any("peer", peer.ID.Pretty())) + req, err := dm.genNodeInfoMsg() + if err != nil { + return err + } + return dm.transmitter.UnicastOutbound(ctx, peer, req) +} + +func (dm *InfoManager) genNodeInfoMsg() (*iotextypes.NodeInfo, error) { + req := &iotextypes.NodeInfo{ + Info: &iotextypes.NodeInfoCore{ + Version: dm.version, + Height: dm.chain.TipHeight(), + Timestamp: timestamppb.Now(), + Address: dm.address, + }, + } + // add sig for msg + h := hashNodeInfo(req.Info) + sig, err := dm.privKey.Sign(h[:]) + if err != nil { + return nil, errors.Wrap(err, "sign node info message failed") + } + req.Signature = sig + return req, nil +} + +func (dm *InfoManager) isDelegate() bool { + // TODO whether i am delegate + return false +} + +func hashNodeInfo(msg *iotextypes.NodeInfoCore) hash.Hash256 { + return hash.Hash256b(byteutil.Must(proto.Marshal(msg))) +} diff --git a/nodeinfo/manager_test.go b/nodeinfo/manager_test.go new file mode 100644 index 0000000000..103b15a79e --- /dev/null +++ b/nodeinfo/manager_test.go @@ -0,0 +1,236 @@ +// Copyright (c) 2022 IoTeX Foundation +// This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability +// or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. +// This source code is governed by Apache License 2.0 that can be found in the LICENSE file. + +package nodeinfo + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/iotexproject/go-pkgs/crypto" + "github.com/iotexproject/iotex-core/test/mock/mock_nodeinfo" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/libp2p/go-libp2p-core/peer" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestNewDelegateManager(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + privK, err := crypto.GenerateKey() + require.NoError(err) + + t.Run("disable_broadcast", func(t *testing.T) { + cfg := Config{false, 100 * time.Millisecond, 1000} + dm := NewInfoManager(&cfg, tMock, hMock, privK) + require.NotNil(dm.nodeMap) + require.Equal(tMock, dm.transmitter) + require.Equal(hMock, dm.chain) + require.Equal(privK, dm.privKey) + require.Equal(true, dm.broadcastTask != nil) + tMock.EXPECT().BroadcastOutbound(gomock.Any(), gomock.Any()).Times(0) + err := dm.Start(context.Background()) + require.NoError(err) + defer dm.Stop(context.Background()) + time.Sleep(time.Second) + }) + + t.Run("enable_broadcast", func(t *testing.T) { + cfg := Config{true, 100 * time.Millisecond, 1000} + dm := NewInfoManager(&cfg, tMock, hMock, privK) + require.NotNil(dm.nodeMap) + require.Equal(tMock, dm.transmitter) + require.Equal(hMock, dm.chain) + require.Equal(privK, dm.privKey) + require.Equal(true, dm.broadcastTask != nil) + tMock.EXPECT().Info().Return(peer.AddrInfo{}, nil).MinTimes(1) + hMock.EXPECT().TipHeight().Return(uint64(10)).MinTimes(1) + tMock.EXPECT().BroadcastOutbound(gomock.Any(), gomock.Any()).Return(nil).MinTimes(1) + err := dm.Start(context.Background()) + require.NoError(err) + defer dm.Stop(context.Background()) + time.Sleep(time.Second) + }) +} + +func TestDelegateManager_HandleNodeInfo(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + + require := require.New(t) + privKey, err := crypto.GenerateKey() + require.NoError(err) + + t.Run("verify_pass", func(t *testing.T) { + msg := &iotextypes.NodeInfo{ + Info: &iotextypes.NodeInfoCore{ + Version: "v1.8.0", + Height: 200, + Timestamp: timestamppb.Now(), + Address: privKey.PublicKey().Address().String(), + }, + } + hash := hashNodeInfo(msg.Info) + msg.Signature, _ = privKey.Sign(hash[:]) + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey) + dm.HandleNodeInfo(context.Background(), "abc", msg) + addr := msg.Info.Address + nodeGot, ok := dm.nodeMap.Get(addr) + require.True(ok) + nodeInfo := nodeGot.(Info) + require.Equal(msg.Info.Height, nodeInfo.Height) + require.Equal(msg.Info.Version, nodeInfo.Version) + require.Equal(msg.Info.Timestamp.AsTime().String(), nodeInfo.Timestamp.String()) + require.Equal("abc", nodeInfo.PeerID) + m := dto.Metric{} + _nodeInfoHeightGauge.WithLabelValues(addr, msg.Info.Version).Write(&m) + require.Equal(msg.Info.Height, uint64(m.Gauge.GetValue())) + }) + + t.Run("verify_fail", func(t *testing.T) { + privKey2, _ := crypto.GenerateKey() + msg := &iotextypes.NodeInfo{ + Info: &iotextypes.NodeInfoCore{ + Version: "v1.8.0", + Height: 200, + Timestamp: timestamppb.Now(), + Address: privKey2.PublicKey().Address().String(), + }, + Signature: []byte("xxxx"), + } + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey) + dm.HandleNodeInfo(context.Background(), "abc", msg) + addr := msg.Info.Address + _, ok := dm.nodeMap.Get(addr) + require.False(ok) + m := dto.Metric{} + _nodeInfoHeightGauge.WithLabelValues(addr, msg.Info.Version).Write(&m) + require.Equal(uint64(0), uint64(m.Gauge.GetValue())) + }) +} + +func TestDelegateManager_BroadcastNodeInfo(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + privKey, err := crypto.GenerateKey() + require.NoError(err) + + t.Run("update_self", func(t *testing.T) { + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey) + height := uint64(200) + peerID, err := peer.IDFromString("12D3KooWF2fns5ZWKbPfx2U1wQDdxoTK2D6HC3ortbSAQYR4BQp4") + require.NoError(err) + hMock.EXPECT().TipHeight().Return(height).Times(1) + tMock.EXPECT().Info().Return(peer.AddrInfo{ID: peerID}, nil).Times(1) + tMock.EXPECT().BroadcastOutbound(gomock.Any(), gomock.Any()).Return(nil).Times(1) + err = dm.BroadcastNodeInfo(context.Background()) + require.NoError(err) + addr := privKey.PublicKey().Address().String() + nodeGot, ok := dm.nodeMap.Get(addr) + require.True(ok) + nodeInfo := nodeGot.(Info) + require.Equal(height, nodeInfo.Height) + require.Equal(dm.version, nodeInfo.Version) + require.Equal(addr, nodeInfo.Address) + require.Equal(peerID.Pretty(), nodeInfo.PeerID) + }) +} + +func TestDelegateManager_HandleNodeInfoRequest(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + privKey, err := crypto.GenerateKey() + require.NoError(err) + + t.Run("unicast", func(t *testing.T) { + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey) + height := uint64(200) + var sig []byte + message := &iotextypes.NodeInfo{} + hMock.EXPECT().TipHeight().Return(height).Times(1) + tMock.EXPECT().UnicastOutbound(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, peerInfo peer.AddrInfo, msg proto.Message) error { + *message = *msg.(*iotextypes.NodeInfo) + hash := hashNodeInfo(message.Info) + sig, _ = dm.privKey.Sign(hash[:]) + return nil + }).Times(1) + err := dm.HandleNodeInfoRequest(context.Background(), peer.AddrInfo{}) + require.NoError(err) + require.Equal(message.Info.Height, height) + require.Equal(message.Info.Version, dm.version) + require.Equal(message.Info.Address, dm.address) + require.Equal(message.Signature, sig) + }) +} + +func TestDelegateManager_RequestSingleNodeInfoAsync(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + privKey, err := crypto.GenerateKey() + require.NoError(err) + + t.Run("request_single", func(t *testing.T) { + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey) + var paramPeer peer.AddrInfo + var paramMsg iotextypes.NodeInfoRequest + peerID, err := peer.IDFromString("12D3KooWF2fns5ZWKbPfx2U1wQDdxoTK2D6HC3ortbSAQYR4BQp4") + require.NoError(err) + targetPeer := peer.AddrInfo{ID: peerID} + tMock.EXPECT().UnicastOutbound(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(_ context.Context, p peer.AddrInfo, msg proto.Message) { + paramPeer = p + paramMsg = *msg.(*iotextypes.NodeInfoRequest) + }).Times(1) + dm.RequestSingleNodeInfoAsync(context.Background(), targetPeer) + require.Equal(targetPeer, paramPeer) + require.Equal(iotextypes.NodeInfoRequest{}, paramMsg) + }) +} + +func TestDelegateManager_GetNodeByAddr(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + privKey, err := crypto.GenerateKey() + require.NoError(err) + + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey) + dm.updateNode(&Info{Address: "1"}) + dm.updateNode(&Info{Address: "2"}) + + t.Run("exist", func(t *testing.T) { + info, ok := dm.GetNodeByAddr("1") + require.True(ok) + require.Equal(Info{Address: "1"}, info) + info, ok = dm.GetNodeByAddr("2") + require.True(ok) + require.Equal(Info{Address: "2"}, info) + }) + t.Run("not_exist", func(t *testing.T) { + _, ok := dm.GetNodeByAddr("3") + require.False(ok) + }) + +} diff --git a/server/itx/server.go b/server/itx/server.go index f96207cb51..811d784e21 100644 --- a/server/itx/server.go +++ b/server/itx/server.go @@ -125,7 +125,6 @@ func (s *Server) Start(ctx context.Context) error { if err := s.dispatcher.Start(cctx); err != nil { return errors.Wrap(err, "error when starting dispatcher") } - return nil } diff --git a/test/mock/mock_dispatcher/mock_dispatcher.go b/test/mock/mock_dispatcher/mock_dispatcher.go index 33db450d83..d1377ac3ed 100644 --- a/test/mock/mock_dispatcher/mock_dispatcher.go +++ b/test/mock/mock_dispatcher/mock_dispatcher.go @@ -81,6 +81,34 @@ func (mr *MockSubscriberMockRecorder) HandleConsensusMsg(arg0 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleConsensusMsg", reflect.TypeOf((*MockSubscriber)(nil).HandleConsensusMsg), arg0) } +// HandleNodeInfo mocks base method. +func (m *MockSubscriber) HandleNodeInfo(arg0 context.Context, arg1 string, arg2 *iotextypes.NodeInfo) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleNodeInfo", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// HandleNodeInfo indicates an expected call of HandleNodeInfo. +func (mr *MockSubscriberMockRecorder) HandleNodeInfo(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleNodeInfo", reflect.TypeOf((*MockSubscriber)(nil).HandleNodeInfo), arg0, arg1, arg2) +} + +// HandleNodeInfoRequest mocks base method. +func (m *MockSubscriber) HandleNodeInfoRequest(arg0 context.Context, arg1 peer.AddrInfo, arg2 *iotextypes.NodeInfoRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HandleNodeInfoRequest", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// HandleNodeInfoRequest indicates an expected call of HandleNodeInfoRequest. +func (mr *MockSubscriberMockRecorder) HandleNodeInfoRequest(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleNodeInfoRequest", reflect.TypeOf((*MockSubscriber)(nil).HandleNodeInfoRequest), arg0, arg1, arg2) +} + // HandleSyncRequest mocks base method. func (m *MockSubscriber) HandleSyncRequest(arg0 context.Context, arg1 peer.AddrInfo, arg2 *iotexrpc.BlockSync) error { m.ctrl.T.Helper() diff --git a/test/mock/mock_nodeinfo/mock_manager.go b/test/mock/mock_nodeinfo/mock_manager.go new file mode 100644 index 0000000000..71d26a5232 --- /dev/null +++ b/test/mock/mock_nodeinfo/mock_manager.go @@ -0,0 +1,117 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./nodeinfo/manager.go + +// Package mock_nodeinfo is a generated GoMock package. +package mock_nodeinfo + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + peer "github.com/libp2p/go-libp2p-core/peer" + proto "google.golang.org/protobuf/proto" +) + +// Mocktransmitter is a mock of transmitter interface. +type Mocktransmitter struct { + ctrl *gomock.Controller + recorder *MocktransmitterMockRecorder +} + +// MocktransmitterMockRecorder is the mock recorder for Mocktransmitter. +type MocktransmitterMockRecorder struct { + mock *Mocktransmitter +} + +// NewMocktransmitter creates a new mock instance. +func NewMocktransmitter(ctrl *gomock.Controller) *Mocktransmitter { + mock := &Mocktransmitter{ctrl: ctrl} + mock.recorder = &MocktransmitterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mocktransmitter) EXPECT() *MocktransmitterMockRecorder { + return m.recorder +} + +// BroadcastOutbound mocks base method. +func (m *Mocktransmitter) BroadcastOutbound(arg0 context.Context, arg1 proto.Message) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastOutbound", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// BroadcastOutbound indicates an expected call of BroadcastOutbound. +func (mr *MocktransmitterMockRecorder) BroadcastOutbound(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastOutbound", reflect.TypeOf((*Mocktransmitter)(nil).BroadcastOutbound), arg0, arg1) +} + +// Info mocks base method. +func (m *Mocktransmitter) Info() (peer.AddrInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Info") + ret0, _ := ret[0].(peer.AddrInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Info indicates an expected call of Info. +func (mr *MocktransmitterMockRecorder) Info() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*Mocktransmitter)(nil).Info)) +} + +// UnicastOutbound mocks base method. +func (m *Mocktransmitter) UnicastOutbound(arg0 context.Context, arg1 peer.AddrInfo, arg2 proto.Message) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnicastOutbound", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnicastOutbound indicates an expected call of UnicastOutbound. +func (mr *MocktransmitterMockRecorder) UnicastOutbound(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnicastOutbound", reflect.TypeOf((*Mocktransmitter)(nil).UnicastOutbound), arg0, arg1, arg2) +} + +// Mockchain is a mock of chain interface. +type Mockchain struct { + ctrl *gomock.Controller + recorder *MockchainMockRecorder +} + +// MockchainMockRecorder is the mock recorder for Mockchain. +type MockchainMockRecorder struct { + mock *Mockchain +} + +// NewMockchain creates a new mock instance. +func NewMockchain(ctrl *gomock.Controller) *Mockchain { + mock := &Mockchain{ctrl: ctrl} + mock.recorder = &MockchainMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mockchain) EXPECT() *MockchainMockRecorder { + return m.recorder +} + +// TipHeight mocks base method. +func (m *Mockchain) TipHeight() uint64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TipHeight") + ret0, _ := ret[0].(uint64) + return ret0 +} + +// TipHeight indicates an expected call of TipHeight. +func (mr *MockchainMockRecorder) TipHeight() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TipHeight", reflect.TypeOf((*Mockchain)(nil).TipHeight)) +}