diff --git a/action/protocol/context.go b/action/protocol/context.go index 725874c3ce..18b33e902f 100644 --- a/action/protocol/context.go +++ b/action/protocol/context.go @@ -110,6 +110,7 @@ type ( CorrectGasRefund bool FixRewardErroCheckPosition bool EnableWeb3Rewarding bool + EnableNodeInfo bool } // FeatureWithHeightCtx provides feature check functions. @@ -246,6 +247,7 @@ func WithFeatureCtx(ctx context.Context) context.Context { CorrectGasRefund: g.IsOkhotsk(height), FixRewardErroCheckPosition: g.IsOkhotsk(height), EnableWeb3Rewarding: g.IsToBeEnabled(height), + EnableNodeInfo: g.IsToBeEnabled(height), }, ) } diff --git a/chainservice/builder.go b/chainservice/builder.go index 86cbc66cf8..a5afbe1c3f 100644 --- a/chainservice/builder.go +++ b/chainservice/builder.go @@ -34,6 +34,7 @@ import ( "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" "github.com/iotexproject/iotex-core/state/factory" "github.com/iotexproject/iotex-election/committee" "github.com/pkg/errors" @@ -378,10 +379,18 @@ 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()) +func (builder *Builder) buildNodeInfoManager() error { + cs := builder.cs + stk := staking.FindProtocol(cs.Registry()) + if stk == nil { + return errors.New("cannot find staking protocol") + } + dm := nodeinfo.NewInfoManager(&builder.cfg.NodeInfo, cs.p2pAgent, cs.chain, builder.cfg.Chain.ProducerPrivateKey(), func(ctx context.Context) (state.CandidateList, error) { + return stk.ActiveCandidates(ctx, cs.factory, 0) + }) builder.cs.nodeInfoManager = dm builder.cs.lifecycle.Add(dm) + return nil } func (builder *Builder) buildBlockSyncer() error { @@ -629,7 +638,9 @@ func (builder *Builder) build(forSubChain, forTest bool) (*ChainService, error) if err := builder.buildBlockSyncer(); err != nil { return nil, err } - builder.buildNodeInfoManager() + if err := builder.buildNodeInfoManager(); err != nil { + return nil, err + } cs := builder.cs builder.cs = nil diff --git a/e2etest/nodeinfo_test.go b/e2etest/nodeinfo_test.go index 0eb4b9d95b..7d14207545 100644 --- a/e2etest/nodeinfo_test.go +++ b/e2etest/nodeinfo_test.go @@ -22,6 +22,7 @@ func newConfigForNodeInfoTest(triePath, dBPath, idxDBPath string) (config.Config if err != nil { return cfg, nil, err } + cfg.Genesis.ToBeEnabledBlockHeight = 0 testTriePath, err := testutil.PathOfTempFile(triePath) if err != nil { return cfg, nil, err diff --git a/go.mod b/go.mod index 659a900b4f..a2f38e4ae0 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( 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 + golang.org/x/exp v0.0.0-20230206171751-46f607a40771 golang.org/x/text v0.5.0 ) @@ -195,7 +196,6 @@ 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 755e909c63..84e9215d2e 100644 --- a/go.sum +++ b/go.sum @@ -1375,6 +1375,8 @@ golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm0 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1607,7 +1609,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f 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.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/nodeinfo/manager.go b/nodeinfo/manager.go index 61cec9b932..5a96690822 100644 --- a/nodeinfo/manager.go +++ b/nodeinfo/manager.go @@ -17,13 +17,18 @@ import ( "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" + "golang.org/x/exp/slices" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/iotexproject/iotex-core/action/protocol" + "github.com/iotexproject/iotex-core/blockchain/genesis" + "github.com/iotexproject/iotex-core/pkg/lifecycle" "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" + "github.com/iotexproject/iotex-core/state" ) type ( @@ -35,8 +40,11 @@ type ( chain interface { TipHeight() uint64 + Genesis() genesis.Genesis } + delegatesGetFunc func(context.Context) (state.CandidateList, error) + // Info node infomation Info struct { Version string @@ -48,13 +56,15 @@ type ( // InfoManager manage delegate node info InfoManager struct { + lifecycle.Lifecycle version string address string + delegateCache bool nodeMap *lru.Cache - broadcastTask *routine.RecurringTask transmitter transmitter chain chain privKey crypto.PrivateKey + getDelegates delegatesGetFunc } ) @@ -71,17 +81,29 @@ func init() { } // NewInfoManager new info manager -func NewInfoManager(cfg *Config, t transmitter, h chain, privKey crypto.PrivateKey) *InfoManager { +func NewInfoManager(cfg *Config, t transmitter, ch chain, privKey crypto.PrivateKey, getDelegatesHandler delegatesGetFunc) *InfoManager { dm := &InfoManager{ - nodeMap: lru.New(cfg.NodeMapSize), - transmitter: t, - chain: h, - privKey: privKey, - version: version.PackageVersion, - address: privKey.PublicKey().Address().String(), + nodeMap: lru.New(cfg.NodeMapSize), + transmitter: t, + chain: ch, + privKey: privKey, + version: version.PackageVersion, + address: privKey.PublicKey().Address().String(), + getDelegates: getDelegatesHandler, } + // init recurring tasks + broadcastTask := routine.NewRecurringTask(func() { + ctx := protocol.WithFeatureCtx( + protocol.WithBlockCtx( + genesis.WithGenesisContext(context.Background(), dm.chain.Genesis()), + protocol.BlockCtx{BlockHeight: dm.chain.TipHeight()}, + ), + ) + if !protocol.MustGetFeatureCtx(ctx).EnableNodeInfo { + log.L().Debug("nodeinfo manager feature is disabled") + return + } - 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 { @@ -91,23 +113,18 @@ func NewInfoManager(cfg *Config, t transmitter, h chain, privKey crypto.PrivateK log.L().Debug("nodeinfo manager general node disabled node info broadcast") } }, cfg.BroadcastNodeInfoInterval) + dm.Add(broadcastTask) 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 + return dm.OnStart(ctx) } // Stop stop delegate broadcast task func (dm *InfoManager) Stop(ctx context.Context) error { - if dm.broadcastTask != nil { - return dm.broadcastTask.Stop(ctx) - } - return nil + return dm.OnStop(ctx) } // HandleNodeInfo handle node info message @@ -215,8 +232,24 @@ func (dm *InfoManager) genNodeInfoMsg() (*iotextypes.NodeInfo, error) { } func (dm *InfoManager) isDelegate() bool { - // TODO whether i am delegate - return false + if !dm.delegateCache { + if err := dm.updateDelegateCache(); err != nil { + log.L().Error("nodeinfo manager update delegate cache failed", zap.Error(err)) + } + } + return dm.delegateCache +} + +func (dm *InfoManager) updateDelegateCache() error { + candList, err := dm.getDelegates(context.Background()) + if err != nil { + return err + } + log.L().Debug("nodeinfo manager active candidates", zap.Any("candidates", candList)) + dm.delegateCache = slices.ContainsFunc(candList, func(e *state.Candidate) bool { + return dm.address == e.Address + }) + return nil } func hashNodeInfo(msg *iotextypes.NodeInfoCore) hash.Hash256 { diff --git a/nodeinfo/manager_test.go b/nodeinfo/manager_test.go index 103b15a79e..05cddeee85 100644 --- a/nodeinfo/manager_test.go +++ b/nodeinfo/manager_test.go @@ -12,6 +12,8 @@ import ( "github.com/golang/mock/gomock" "github.com/iotexproject/go-pkgs/crypto" + "github.com/iotexproject/iotex-core/blockchain/genesis" + "github.com/iotexproject/iotex-core/state" "github.com/iotexproject/iotex-core/test/mock/mock_nodeinfo" "github.com/iotexproject/iotex-proto/golang/iotextypes" "github.com/libp2p/go-libp2p-core/peer" @@ -21,24 +23,33 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +func getEmptyCandidates(context.Context) (state.CandidateList, error) { + return state.CandidateList{}, nil +} + 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) { + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) cfg := Config{false, 100 * time.Millisecond, 1000} - dm := NewInfoManager(&cfg, tMock, hMock, privK) + dm := NewInfoManager(&cfg, tMock, hMock, privK, getEmptyCandidates) 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) + hMock.EXPECT().TipHeight().Return(uint64(2)).MinTimes(1) + hMock.EXPECT().Genesis().DoAndReturn(func() genesis.Genesis { + g := genesis.TestDefault() + g.ToBeEnabledBlockHeight = 1 + return g + }).MinTimes(1) err := dm.Start(context.Background()) require.NoError(err) defer dm.Stop(context.Background()) @@ -46,15 +57,49 @@ func TestNewDelegateManager(t *testing.T) { }) t.Run("enable_broadcast", func(t *testing.T) { + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) cfg := Config{true, 100 * time.Millisecond, 1000} - dm := NewInfoManager(&cfg, tMock, hMock, privK) + dm := NewInfoManager(&cfg, tMock, hMock, privK, getEmptyCandidates) 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) + hMock.EXPECT().Genesis().DoAndReturn(func() genesis.Genesis { + g := genesis.TestDefault() + g.ToBeEnabledBlockHeight = 1 + return g + }).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) + }) + t.Run("delegate_broadcast", func(t *testing.T) { + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + cfg := Config{false, 100 * time.Millisecond, 1000} + dm := NewInfoManager(&cfg, tMock, hMock, privK, func(ctx context.Context) (state.CandidateList, error) { + return state.CandidateList{ + &state.Candidate{ + Address: privK.PublicKey().Address().String(), + }, + }, nil + }) + require.NotNil(dm.nodeMap) + require.Equal(tMock, dm.transmitter) + require.Equal(hMock, dm.chain) + require.Equal(privK, dm.privKey) + tMock.EXPECT().Info().Return(peer.AddrInfo{}, nil).MinTimes(1) + hMock.EXPECT().TipHeight().Return(uint64(10)).MinTimes(1) + hMock.EXPECT().Genesis().DoAndReturn(func() genesis.Genesis { + g := genesis.TestDefault() + g.ToBeEnabledBlockHeight = 1 + return g + }).MinTimes(1) tMock.EXPECT().BroadcastOutbound(gomock.Any(), gomock.Any()).Return(nil).MinTimes(1) err := dm.Start(context.Background()) require.NoError(err) @@ -66,14 +111,14 @@ func TestNewDelegateManager(t *testing.T) { 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) { + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) msg := &iotextypes.NodeInfo{ Info: &iotextypes.NodeInfoCore{ Version: "v1.8.0", @@ -84,7 +129,7 @@ func TestDelegateManager_HandleNodeInfo(t *testing.T) { } hash := hashNodeInfo(msg.Info) msg.Signature, _ = privKey.Sign(hash[:]) - dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey) + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey, getEmptyCandidates) dm.HandleNodeInfo(context.Background(), "abc", msg) addr := msg.Info.Address nodeGot, ok := dm.nodeMap.Get(addr) @@ -100,6 +145,8 @@ func TestDelegateManager_HandleNodeInfo(t *testing.T) { }) t.Run("verify_fail", func(t *testing.T) { + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) privKey2, _ := crypto.GenerateKey() msg := &iotextypes.NodeInfo{ Info: &iotextypes.NodeInfoCore{ @@ -110,7 +157,7 @@ func TestDelegateManager_HandleNodeInfo(t *testing.T) { }, Signature: []byte("xxxx"), } - dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey) + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey, getEmptyCandidates) dm.HandleNodeInfo(context.Background(), "abc", msg) addr := msg.Info.Address _, ok := dm.nodeMap.Get(addr) @@ -125,13 +172,13 @@ 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) + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey, getEmptyCandidates) height := uint64(200) peerID, err := peer.IDFromString("12D3KooWF2fns5ZWKbPfx2U1wQDdxoTK2D6HC3ortbSAQYR4BQp4") require.NoError(err) @@ -155,13 +202,13 @@ 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) + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey, getEmptyCandidates) height := uint64(200) var sig []byte message := &iotextypes.NodeInfo{} @@ -185,13 +232,13 @@ 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) + hMock := mock_nodeinfo.NewMockchain(ctrl) + tMock := mock_nodeinfo.NewMocktransmitter(ctrl) + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey, getEmptyCandidates) var paramPeer peer.AddrInfo var paramMsg iotextypes.NodeInfoRequest peerID, err := peer.IDFromString("12D3KooWF2fns5ZWKbPfx2U1wQDdxoTK2D6HC3ortbSAQYR4BQp4") @@ -216,7 +263,7 @@ func TestDelegateManager_GetNodeByAddr(t *testing.T) { privKey, err := crypto.GenerateKey() require.NoError(err) - dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey) + dm := NewInfoManager(&DefaultConfig, tMock, hMock, privKey, getEmptyCandidates) dm.updateNode(&Info{Address: "1"}) dm.updateNode(&Info{Address: "2"}) diff --git a/test/mock/mock_nodeinfo/mock_manager.go b/test/mock/mock_nodeinfo/mock_manager.go index 71d26a5232..fc46dd8221 100644 --- a/test/mock/mock_nodeinfo/mock_manager.go +++ b/test/mock/mock_nodeinfo/mock_manager.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + genesis "github.com/iotexproject/iotex-core/blockchain/genesis" peer "github.com/libp2p/go-libp2p-core/peer" proto "google.golang.org/protobuf/proto" ) @@ -102,6 +103,20 @@ func (m *Mockchain) EXPECT() *MockchainMockRecorder { return m.recorder } +// Genesis mocks base method. +func (m *Mockchain) Genesis() genesis.Genesis { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Genesis") + ret0, _ := ret[0].(genesis.Genesis) + return ret0 +} + +// Genesis indicates an expected call of Genesis. +func (mr *MockchainMockRecorder) Genesis() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Genesis", reflect.TypeOf((*Mockchain)(nil).Genesis)) +} + // TipHeight mocks base method. func (m *Mockchain) TipHeight() uint64 { m.ctrl.T.Helper()