diff --git a/Makefile b/Makefile index 4b2472608..b39d54f26 100644 --- a/Makefile +++ b/Makefile @@ -297,7 +297,7 @@ protogen_local: go_protoc-go-inject-tag ## Generate go structures for all of the $(PROTOC_SHARED) -I=./consensus/types/proto --go_out=./consensus/types ./consensus/types/proto/*.proto # P2P - $(PROTOC_SHARED) -I=./p2p/raintree/types/proto --go_out=./p2p/types ./p2p/raintree/types/proto/*.proto + $(PROTOC_SHARED) -I=./p2p/types/proto --go_out=./p2p/types ./p2p/types/proto/*.proto # echo "View generated proto files by running: make protogen_show" diff --git a/consensus/e2e_tests/utils_test.go b/consensus/e2e_tests/utils_test.go index 41340abc6..2f533b21b 100644 --- a/consensus/e2e_tests/utils_test.go +++ b/consensus/e2e_tests/utils_test.go @@ -3,6 +3,9 @@ package e2e_tests import ( "context" "fmt" + "github.com/pokt-network/pocket/internal/testutil/p2p" + "github.com/pokt-network/pocket/internal/testutil/persistence" + telemetry_testutil "github.com/pokt-network/pocket/internal/testutil/telemetry" "os" "reflect" "sort" @@ -13,7 +16,6 @@ import ( "github.com/golang/mock/gomock" "github.com/pokt-network/pocket/consensus" typesCons "github.com/pokt-network/pocket/consensus/types" - persistenceMocks "github.com/pokt-network/pocket/persistence/types/mocks" "github.com/pokt-network/pocket/runtime" "github.com/pokt-network/pocket/runtime/configs" "github.com/pokt-network/pocket/runtime/defaults" @@ -101,7 +103,7 @@ func CreateTestConsensusPocketNode( bus modules.Bus, eventsChannel modules.EventsChannel, ) *shared.Node { - persistenceMock := basePersistenceMock(t, eventsChannel, bus) + persistenceMock := persistence_testutil.PersistenceMockWithBlockStore(t, eventsChannel, bus) bus.RegisterModule(persistenceMock) consensusMod, err := consensus.Create(bus) @@ -115,9 +117,9 @@ func CreateTestConsensusPocketNode( runtimeMgr := (bus).GetRuntimeMgr() // TODO(olshansky): At the moment we are using the same base mocks for all the tests, // but note that they will need to be customized on a per test basis. - p2pMock := baseP2PMock(t, eventsChannel) + p2pMock := p2p_testutil.BaseP2PMock(t, eventsChannel) utilityMock := baseUtilityMock(t, eventsChannel, runtimeMgr.GetGenesis(), consensusModule) - telemetryMock := baseTelemetryMock(t, eventsChannel) + telemetryMock := telemetry_testutil.MinimalTelemetryMock(t) loggerMock := baseLoggerMock(t, eventsChannel) rpcMock := baseRpcMock(t, eventsChannel) @@ -548,21 +550,6 @@ func baseReplicaUtilityUnitOfWorkMock(t *testing.T, genesisState *genesis.Genesi return utilityReplicaUnitOfWorkMock } -func baseTelemetryMock(t *testing.T, _ modules.EventsChannel) *mockModules.MockTelemetryModule { - ctrl := gomock.NewController(t) - telemetryMock := mockModules.NewMockTelemetryModule(ctrl) - timeSeriesAgentMock := baseTelemetryTimeSeriesAgentMock(t) - eventMetricsAgentMock := baseTelemetryEventMetricsAgentMock(t) - - telemetryMock.EXPECT().Start().Return(nil).AnyTimes() - telemetryMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() - telemetryMock.EXPECT().GetTimeSeriesAgent().Return(timeSeriesAgentMock).AnyTimes() - telemetryMock.EXPECT().GetEventMetricsAgent().Return(eventMetricsAgentMock).AnyTimes() - telemetryMock.EXPECT().GetModuleName().Return(modules.TelemetryModuleName).AnyTimes() - - return telemetryMock -} - func baseRpcMock(t *testing.T, _ modules.EventsChannel) *mockModules.MockRPCModule { ctrl := gomock.NewController(t) rpcMock := mockModules.NewMockRPCModule(ctrl) diff --git a/go.mod b/go.mod index e64de0142..7116b1cdb 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/dgraph-io/badger/v3 v3.2103.2 github.com/foxcpp/go-mockdns v1.0.0 github.com/getkin/kin-openapi v0.107.0 + github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/vault/api v1.9.0 github.com/jackc/pgconn v1.13.0 github.com/jordanorelli/lexnum v0.0.0-20141216151731-460eeb125754 @@ -112,7 +113,6 @@ require ( github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.6.6 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect diff --git a/internal/testutil/bus.go b/internal/testutil/bus.go new file mode 100644 index 000000000..1f83597a3 --- /dev/null +++ b/internal/testutil/bus.go @@ -0,0 +1,96 @@ +package testutil + +import ( + "github.com/golang/mock/gomock" + "github.com/pokt-network/pocket/p2p/providers/current_height_provider" + "github.com/pokt-network/pocket/p2p/providers/peerstore_provider" + "github.com/pokt-network/pocket/runtime" + "github.com/pokt-network/pocket/shared/messaging" + "github.com/regen-network/gocuke" + + "github.com/pokt-network/pocket/shared/modules" + "github.com/pokt-network/pocket/shared/modules/mocks" +) + +type BusEventHandler func(*messaging.PocketEnvelope) +type BusEventHandlerFactory func(t gocuke.TestingT, busMock *mock_modules.MockBus) BusEventHandler + +// MinimalBusMock returns a bus mock with a module registry and minimal +// expectations registered to maximize re-usability. +func MinimalBusMock( + t gocuke.TestingT, + runtimeMgr modules.RuntimeMgr, +) *mock_modules.MockBus { + t.Helper() + + ctrl := gomock.NewController(t) + busMock := mock_modules.NewMockBus(ctrl) + busMock.EXPECT().GetRuntimeMgr().Return(runtimeMgr).AnyTimes() + busMock.EXPECT().RegisterModule(gomock.Any()).DoAndReturn(func(m modules.Module) { + m.SetBus(busMock) + }).AnyTimes() + + mockModulesRegistry := mock_modules.NewMockModulesRegistry(ctrl) + + // TODO_THIS_COMMIT: refactor - this doesn't belong here + mockModulesRegistry.EXPECT().GetModule(peerstore_provider.ModuleName).Return(nil, runtime.ErrModuleNotRegistered(peerstore_provider.ModuleName)).AnyTimes() + mockModulesRegistry.EXPECT().GetModule(current_height_provider.ModuleName).Return(nil, runtime.ErrModuleNotRegistered(current_height_provider.ModuleName)).AnyTimes() + + busMock.EXPECT().GetModulesRegistry().Return(mockModulesRegistry).AnyTimes() + return busMock +} + +// BaseBusMock returns a base bus mock which will accept any event, +// passing it to the provided handler function, any number of times. +func BaseBusMock( + t gocuke.TestingT, + runtimeMgr modules.RuntimeMgr, +) *mock_modules.MockBus { + t.Helper() + + return WithoutBusEventHandler(t, MinimalBusMock(t, runtimeMgr)) +} + +// BusMockWithEventHandler returns a base bus mock which will accept any event, +// any number of times, calling the `handler` returned from `handlerFactory` +// with the event as an argument. +func BusMockWithEventHandler( + t gocuke.TestingT, + runtimeMgr modules.RuntimeMgr, + handlerFactory BusEventHandlerFactory, +) *mock_modules.MockBus { + t.Helper() + + busMock := MinimalBusMock(t, runtimeMgr) + return WithBusEventHandler(t, busMock, handlerFactory) +} + +// WithBusEventHandler adds an expectation to a bus mock such that it will accept +// any event, any number of times, calling the `handler` returned from `handlerFactory` +// with the event as an argument. +func WithBusEventHandler( + t gocuke.TestingT, + busMock *mock_modules.MockBus, + handlerFactory BusEventHandlerFactory, +) *mock_modules.MockBus { + t.Helper() + + if handlerFactory != nil { + handler := handlerFactory(t, busMock) + busMock.EXPECT().PublishEventToBus(gomock.Any()).Do(handler).AnyTimes() + } + + return busMock +} + +// WithoutBusEventHandler adds an expectation to a bus mock such that it will accept +// any event, any number of times. +func WithoutBusEventHandler( + t gocuke.TestingT, + busMock *mock_modules.MockBus, +) *mock_modules.MockBus { + t.Helper() + + busMock.EXPECT().PublishEventToBus(gomock.Any()).AnyTimes() + return busMock +} diff --git a/internal/testutil/bus/bus.go b/internal/testutil/bus/bus.go new file mode 100644 index 000000000..f83e27735 --- /dev/null +++ b/internal/testutil/bus/bus.go @@ -0,0 +1,30 @@ +package bus_testutil + +import ( + "github.com/regen-network/gocuke" + + "github.com/pokt-network/pocket/internal/testutil" + "github.com/pokt-network/pocket/internal/testutil/runtime" + "github.com/pokt-network/pocket/runtime/genesis" + "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/pocket/shared/modules/mocks" +) + +func NewBus( + t gocuke.TestingT, + privKey crypto.PrivateKey, + serviceURL string, + genesisState *genesis.GenesisState, + busEventHandlerFactory testutil.BusEventHandlerFactory, +) *mock_modules.MockBus { + t.Helper() + + runtimeMgrMock := runtime_testutil.BaseRuntimeManagerMock( + t, privKey, + serviceURL, + genesisState, + ) + busMock := testutil.BusMockWithEventHandler(t, runtimeMgrMock, busEventHandlerFactory) + busMock.EXPECT().GetRuntimeMgr().Return(runtimeMgrMock).AnyTimes() + return busMock +} diff --git a/internal/testutil/composition.go b/internal/testutil/composition.go new file mode 100644 index 000000000..5e4d66b0f --- /dev/null +++ b/internal/testutil/composition.go @@ -0,0 +1,30 @@ +package testutil + +// PipeTwoToOne threads two values of any type (T and U) through a pipeline of +// functions and returns the result of any type (U). Each function in the pipeline +// takes two arguments of type T and U, and returns a value of type U. +// +// Applies each function in the pipeline to the current value of U and the +// constant value of T, effectively "threading" the initial U value through the +// pipeline of functions. +// +// Does *not* mutate the original U value. Instead, it operates on a reference +// to U, ensuring that value types (non-pointer types) are not mutated. +// +// Returns the final value of U after it has been threaded through all the functions in the pipeline. +// +// Usage: +// +// result := PipeTwo(initialT, initialU, func1, func2, func3) +// +// In this example, initialT and initialU are the initial values of T and U, and func1, func2, and func3 +// are functions that take two arguments of type T and U and return a value of type U. +func PipeTwoToOne[T, U any](t T, u U, pipeline ...func(T, U) U) U { + // NB: don't mutate potential value type `u` (i.e. non-pointer) + uRef := u + for _, fn := range pipeline { + uRef = fn(t, uRef) + } + + return u +} diff --git a/internal/testutil/consensus/mocks.go b/internal/testutil/consensus/mocks.go new file mode 100644 index 000000000..51bdc3338 --- /dev/null +++ b/internal/testutil/consensus/mocks.go @@ -0,0 +1,24 @@ +package consensus_testutil + +import ( + "github.com/golang/mock/gomock" + "github.com/regen-network/gocuke" + + "github.com/pokt-network/pocket/shared/modules" + "github.com/pokt-network/pocket/shared/modules/mocks" +) + +// Consensus mock - only needed for validatorMap access +func BaseConsensusMock(t gocuke.TestingT, busMock *mock_modules.MockBus) *mock_modules.MockConsensusModule { + ctrl := gomock.NewController(t) + consensusMock := mock_modules.NewMockConsensusModule(ctrl) + consensusMock.EXPECT().CurrentHeight().Return(uint64(1)).AnyTimes() + + consensusMock.EXPECT().GetBus().Return(busMock).AnyTimes() + consensusMock.EXPECT().SetBus(busMock).AnyTimes() + consensusMock.EXPECT().GetModuleName().Return(modules.ConsensusModuleName).AnyTimes() + busMock.EXPECT().GetConsensusModule().Return(consensusMock).AnyTimes() + //busMock.RegisterModule(consensusMock) + + return consensusMock +} diff --git a/internal/testutil/constructors/constructors.go b/internal/testutil/constructors/constructors.go new file mode 100644 index 000000000..2d6e7900f --- /dev/null +++ b/internal/testutil/constructors/constructors.go @@ -0,0 +1,156 @@ +package constructors + +import ( + "github.com/foxcpp/go-mockdns" + libp2pHost "github.com/libp2p/go-libp2p/core/host" + libp2pNetwork "github.com/libp2p/go-libp2p/core/network" + mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" + "github.com/pokt-network/pocket/internal/testutil/bus" + consensus_testutil "github.com/pokt-network/pocket/internal/testutil/consensus" + persistence_testutil "github.com/pokt-network/pocket/internal/testutil/persistence" + telemetry_testutil "github.com/pokt-network/pocket/internal/testutil/telemetry" + "github.com/regen-network/gocuke" + "github.com/stretchr/testify/require" + "net" + "strconv" + + "github.com/pokt-network/pocket/internal/testutil" + p2p_testutil "github.com/pokt-network/pocket/internal/testutil/p2p" + "github.com/pokt-network/pocket/p2p" + "github.com/pokt-network/pocket/runtime/genesis" + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/pocket/shared/modules" + mock_modules "github.com/pokt-network/pocket/shared/modules/mocks" +) + +type serviceURLStr = string + +// NewP2PMocknetModules returns a map of peer IDs to P2PModules using libp2p mocknet hosts. +func NewBusesMocknetAndP2PModules( + t gocuke.TestingT, + count int, + dnsSrv *mockdns.Server, + genesisState *genesis.GenesisState, + busEventHandlerFactory testutil.BusEventHandlerFactory, + notifiee libp2pNetwork.Notifiee, +) ( + buses map[serviceURLStr]*mock_modules.MockBus, + libp2pNetworkMock mocknet.Mocknet, + p2pModules map[serviceURLStr]modules.P2PModule, +) { + libp2pNetworkMock = p2p_testutil.NewLibp2pNetworkMock(t) + serviceURLKeyMap := testutil.SequentialServiceURLPrivKeyMap(t, count) + + buses, p2pModules = NewBusesAndP2PModules( + t, busEventHandlerFactory, + dnsSrv, + genesisState, + libp2pNetworkMock, + serviceURLKeyMap, + notifiee, + ) + err := libp2pNetworkMock.LinkAll() + require.NoError(t, err) + + return buses, libp2pNetworkMock, p2pModules +} + +// TODO_THIS_COMMIT: rename / move, if possible +func NewP2PModule( + t gocuke.TestingT, + busMock *mock_modules.MockBus, + dnsSrv *mockdns.Server, + libp2pNetworkMock mocknet.Mocknet, + notifiee libp2pNetwork.Notifiee, + // TODO_THIS_COMMIT: consider *p2p.P2PModule instead +) modules.P2PModule { + genesisState := busMock.GetRuntimeMgr().GetGenesis() + + _ = consensus_testutil.BaseConsensusMock(t, busMock) + _ = persistence_testutil.BasePersistenceMock(t, busMock, genesisState) + + // -- option 1 + _ = telemetry_testutil.BaseTelemetryMock(t, busMock) + + // -- option 2 + //_ = telemetry_testutil.WithTimeSeriesAgent( + // t, telemetry_testutil.MinimalTelemetryMock(t, busMock), + //) + + p2pCfg := busMock.GetRuntimeMgr().GetConfig().P2P + serviceURL := net.JoinHostPort(p2pCfg.Hostname, strconv.Itoa(int(p2pCfg.Port))) + privKey, err := cryptoPocket.NewPrivateKey(p2pCfg.PrivateKey) + require.NoError(t, err) + + // MUST register DNS before instantiating P2PModule + testutil.AddServiceURLZone(t, dnsSrv, serviceURL) + + host := testutil.NewMocknetHost(t, libp2pNetworkMock, privKey, notifiee) + return NewP2PModuleWithHost(t, busMock, host) +} + +// TODO_THIS_TEST: need this? +func NewBusesAndP2PModules( + t gocuke.TestingT, + busEventHandlerFactory testutil.BusEventHandlerFactory, + dnsSrv *mockdns.Server, + genesisState *genesis.GenesisState, + libp2pNetworkMock mocknet.Mocknet, + serviceURLKeyMap map[serviceURLStr]cryptoPocket.PrivateKey, + notifiee libp2pNetwork.Notifiee, +) ( + busMocks map[serviceURLStr]*mock_modules.MockBus, + // TODO_THIS_COMMIT: consider *p2p.P2PModule instead + p2pModules map[serviceURLStr]modules.P2PModule, +) { + busMocks = make(map[serviceURLStr]*mock_modules.MockBus) + p2pModules = make(map[serviceURLStr]modules.P2PModule) + + for serviceURL, privKey := range serviceURLKeyMap { + busMock := bus_testutil.NewBus( + t, privKey, + serviceURL, + genesisState, + busEventHandlerFactory, + ) + busMocks[serviceURL] = busMock + + p2pModules[serviceURL] = NewP2PModule( + t, busMock, + dnsSrv, + libp2pNetworkMock, + notifiee, + ) + } + return busMocks, p2pModules +} + +// TODO_THIS_TEST: need this? +// TODO_THIS_COMMIT: consider following create factory convention (?) +func NewBusesAndP2PModuleWithHost( + t gocuke.TestingT, + privKey cryptoPocket.PrivateKey, + serviceURL string, + host libp2pHost.Host, + genesisState *genesis.GenesisState, + busEventHandlerFactory testutil.BusEventHandlerFactory, +) (*mock_modules.MockBus, modules.P2PModule) { + t.Helper() + + busMock := bus_testutil.NewBus(t, privKey, serviceURL, genesisState, busEventHandlerFactory) + return busMock, NewP2PModuleWithHost(t, busMock, host) +} + +// TODO_THIS_COMMIT: rename; consider returning *p2p.P2PModule instead +func NewP2PModuleWithHost( + t gocuke.TestingT, + busMock *mock_modules.MockBus, + host libp2pHost.Host, +) modules.P2PModule { + t.Helper() + + mod, err := p2p.Create(busMock, p2p.WithHostOption(host)) + require.NoError(t, err) + + return mod.(modules.P2PModule) +} diff --git a/internal/testutil/generics/composition.go b/internal/testutil/generics/composition.go new file mode 100644 index 000000000..efff575d8 --- /dev/null +++ b/internal/testutil/generics/composition.go @@ -0,0 +1,30 @@ +package generics_testutil + +// PipeTwoToOne threads two values of any type (T and U) through a pipeline of +// functions and returns the result of any type (U). Each function in the pipeline +// takes two arguments of type T and U, and returns a value of type U. +// +// Applies each function in the pipeline to the current value of U and the +// constant value of T, effectively "threading" the initial U value through the +// pipeline of functions. +// +// Does *not* mutate the original U value. Instead, it operates on a reference +// to U, ensuring that value types (non-pointer types) are not mutated. +// +// Returns the final value of U after it has been threaded through all the functions in the pipeline. +// +// Usage: +// +// result := PipeTwo(initialT, initialU, func1, func2, func3) +// +// In this example, initialT and initialU are the initial values of T and U, and func1, func2, and func3 +// are functions that take two arguments of type T and U and return a value of type U. +func PipeTwoToOne[T, U any](t T, u U, pipeline ...func(T, U) U) U { + // NB: don't mutate potential value type `u` (i.e. non-pointer) + uRef := u + for _, fn := range pipeline { + uRef = fn(t, uRef) + } + + return u +} diff --git a/internal/testutil/map.go b/internal/testutil/generics/map.go similarity index 87% rename from internal/testutil/map.go rename to internal/testutil/generics/map.go index 55ef6617f..32639577c 100644 --- a/internal/testutil/map.go +++ b/internal/testutil/generics/map.go @@ -1,4 +1,4 @@ -package testutil +package generics_testutil func GetKeys[K comparable, V any](keyMap map[K]V) []K { var ( diff --git a/internal/testutil/generics/proxy.go b/internal/testutil/generics/proxy.go new file mode 100644 index 000000000..8dd837735 --- /dev/null +++ b/internal/testutil/generics/proxy.go @@ -0,0 +1,3 @@ +package generics_testutil + +type ProxyFactory[T any] func(target T) (proxy T) diff --git a/internal/testutil/identity.go b/internal/testutil/identity.go new file mode 100644 index 000000000..110b2e6a7 --- /dev/null +++ b/internal/testutil/identity.go @@ -0,0 +1 @@ +package testutil diff --git a/internal/testutil/keys.go b/internal/testutil/keys.go new file mode 100644 index 000000000..a541f4205 --- /dev/null +++ b/internal/testutil/keys.go @@ -0,0 +1,47 @@ +package testutil + +import ( + "bufio" + "os" + "path/filepath" + "regexp" + "runtime" + + "github.com/regen-network/gocuke" + "github.com/stretchr/testify/require" + + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" +) + +var ( + privKeyManifestKeyRegex = regexp.MustCompile(`\s+"\d+":\s+(\w+)\s+`) +) + +func LoadLocalnetPrivateKeys(t gocuke.TestingT, keyCount int) (privKeys []cryptoPocket.PrivateKey) { + _, filename, _, _ := runtime.Caller(0) + pkgDir := filepath.Dir(filename) + relativePathToKeys := filepath.Join(pkgDir, "..", "..", "build", "localnet", "manifests", "private-keys.yaml") + + privKeyManifest, err := os.Open(relativePathToKeys) + require.NoError(t, err) + + privKeys = make([]cryptoPocket.PrivateKey, 0, keyCount) + + // scan through file & extract private keys + scanner := bufio.NewScanner(privKeyManifest) + scanner.Split(bufio.ScanLines) + + for i, done := 0, false; i < keyCount && !done; { + done = !scanner.Scan() + line := scanner.Text() + matches := privKeyManifestKeyRegex.FindStringSubmatch(line) + if len(matches) > 0 { + privKey, err := cryptoPocket.NewPrivateKey(matches[1]) + require.NoError(t, err) + + privKeys = append(privKeys, privKey) + i++ + } + } + return privKeys +} diff --git a/internal/testutil/keys_test.go b/internal/testutil/keys_test.go new file mode 100644 index 000000000..fc423fb5e --- /dev/null +++ b/internal/testutil/keys_test.go @@ -0,0 +1,23 @@ +package testutil_test + +import ( + "testing" + + "github.com/pokt-network/pocket/internal/testutil" + "github.com/stretchr/testify/require" +) + +func TestLoadLocalnetPrivateKeys(t *testing.T) { + keyCount := 1000 + privKeys := testutil.LoadLocalnetPrivateKeys(t, keyCount) + + require.Lenf(t, privKeys, keyCount, "expected %d private keys; got %d", keyCount, len(privKeys)) + + // ensure each key is unique + seen := make(map[string]struct{}) + for _, privKey := range privKeys { + seen[privKey.String()] = struct{}{} + } + + require.Lenf(t, seen, keyCount, "expected %d unique private keys; got %d", keyCount, len(seen)) +} diff --git a/internal/testutil/mockdns.go b/internal/testutil/mockdns.go index 009a0e0fa..2adc4ab74 100644 --- a/internal/testutil/mockdns.go +++ b/internal/testutil/mockdns.go @@ -3,43 +3,59 @@ package testutil import ( "fmt" "net" - "net/url" - "testing" "github.com/foxcpp/go-mockdns" + "github.com/regen-network/gocuke" "github.com/stretchr/testify/require" ) -func PrepareDNSMockFromServiceURLs(t *testing.T, serviceURLs []string) (done func()) { - zones := make(map[string]mockdns.Zone) - for i, u := range serviceURLs { - // Perpend `scheme://` as serviceURLs are currently scheme-less. - // Required for parsing to produce useful results. - // (see: https://pkg.go.dev/net/url@go1.20.2#URL) - serviceURL, err := url.Parse(fmt.Sprintf("scheme://%s", u)) - require.NoError(t, err) +func DNSMockFromServiceURLs(t gocuke.TestingT, serviceURLs []string) *mockdns.Server { + t.Helper() + + srv := MinimalDNSMock(t) + for _, serviceURL := range serviceURLs { + AddServiceURLZone(t, srv, serviceURL) + } + return srv +} - ipStr := fmt.Sprintf("10.0.0.%d", i+1) +func AddServiceURLZone(t gocuke.TestingT, srv *mockdns.Server, serviceURL string) { + t.Helper() - if i >= 254 { - panic(fmt.Sprintf("would generate invalid IPv4 address: %s", ipStr)) - } + // TODO_THIS_COMMIT: move & de-dup + hostname, _, err := net.SplitHostPort(serviceURL) + require.NoError(t, err) - zones[fmt.Sprintf("%s.", serviceURL.Hostname())] = mockdns.Zone{ - A: []string{ipStr}, - } + zone := mockdns.Zone{ + A: []string{"10.0.0.1"}, } - return PrepareDNSMock(zones) + err = srv.AddZone(fmt.Sprintf("%s.", hostname), zone) + require.NoError(t, err) +} + +func MinimalDNSMock(t gocuke.TestingT) *mockdns.Server { + t.Helper() + + return BaseDNSMock(t, nil) } -func PrepareDNSMock(zones map[string]mockdns.Zone) (done func()) { +func BaseDNSMock(t gocuke.TestingT, zones map[string]mockdns.Zone) *mockdns.Server { + t.Helper() + + if zones == nil { + zones = make(map[string]mockdns.Zone) + } + srv, _ := mockdns.NewServerWithLogger(zones, noopLogger{}, false) srv.PatchNet(net.DefaultResolver) - return func() { - _ = srv.Close() + t.Cleanup(func() { + err := srv.Close() + require.NoError(t, err) mockdns.UnpatchNet(net.DefaultResolver) - } + }) + + return srv } // NB: default logging behavior is too noisy. diff --git a/internal/testutil/module.go b/internal/testutil/module.go new file mode 100644 index 000000000..a59710b02 --- /dev/null +++ b/internal/testutil/module.go @@ -0,0 +1,41 @@ +package testutil + +import ( + "github.com/foxcpp/go-mockdns" + mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" + "github.com/pokt-network/pocket/runtime/genesis" + "github.com/pokt-network/pocket/shared/modules" + "github.com/pokt-network/pocket/shared/modules/base_modules" +) + +// TODO_THIS_COMMIT: is this helpful? +const TestModuleName = "testModule" + +var ( + _ modules.Module = &TestModule{} + _ modules.ModuleFactoryWithOptions = &TestModule{} +) + +type TestModule struct { + base_modules.IntegratableModule + base_modules.InterruptableModule + + DNS *mockdns.Server + Genesis *genesis.GenesisState + Libp2pNetworkMock mocknet.Mocknet +} + +func (m *TestModule) GetModuleName() string { + return TestModuleName +} + +func (m *TestModule) Create( + bus modules.Bus, + opts ...modules.ModuleOption, +) (modules.Module, error) { + panic("implement me") +} + +func (m *TestModule) GetDNS() *mockdns.Server { + return m.DNS +} diff --git a/internal/testutil/network.go b/internal/testutil/network.go new file mode 100644 index 000000000..a86d5a866 --- /dev/null +++ b/internal/testutil/network.go @@ -0,0 +1,105 @@ +package testutil + +import ( + "fmt" + + crypto2 "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" + libp2pNetwork "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/p2p/net/mock" + "github.com/multiformats/go-multiaddr" + "github.com/regen-network/gocuke" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/pocket/runtime/defaults" + "github.com/pokt-network/pocket/shared/crypto" +) + +const ServiceURLFormat = "node%d.consensus:42069" + +func NewMocknetHost( + t gocuke.TestingT, + libp2pNetworkMock mocknet.Mocknet, + privKey crypto.PrivateKey, + notifiee libp2pNetwork.Notifiee, +) host.Host { + t.Helper() + + // TODO_THIS_COMMIT: move to const + addrMock, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/10.0.0.1/tcp/%d", defaults.DefaultP2PPort)) + require.NoError(t, err) + + libp2pPrivKey, err := crypto2.UnmarshalEd25519PrivateKey(privKey.Bytes()) + require.NoError(t, err) + + host, err := libp2pNetworkMock.AddPeer(libp2pPrivKey, addrMock) + require.NoError(t, err) + + if notifiee != nil { + host.Network().Notify(notifiee) + } + + return host +} + +func SequentialServiceURLPrivKeyMap(t gocuke.TestingT, count int) map[string]crypto.PrivateKey { + t.Helper() + + // CONSIDERATION: using an iterator/generator would prevent unintentional + // ID collisions + privKeys := LoadLocalnetPrivateKeys(t, count) + // CONSIDERATION: using an iterator/generator would prevent unintentional + // serviceURL collisions + serviceURLs := SequentialServiceURLs(t, count) + + require.GreaterOrEqualf(t, len(privKeys), len(serviceURLs), "not enough private keys for service URLs") + + serviceURLKeysMap := make(map[string]crypto.PrivateKey, len(serviceURLs)) + + for i, serviceURL := range serviceURLs { + serviceURLKeysMap[serviceURL] = privKeys[i] + } + return serviceURLKeysMap +} + +// CONSIDERATION: serviceURLs are only unique within their respective slice; +// consider building an iterator/generator instead. +func SequentialServiceURLs(t gocuke.TestingT, count int) (serviceURLs []string) { + t.Helper() + + for i := 0; i < count; i++ { + serviceURLs = append(serviceURLs, NewServiceURL(i+1)) + } + return serviceURLs +} + +// TECHDEBT: rename `validatorId()` to `serviceURL()` +func NewServiceURL(i int) string { + return fmt.Sprintf(ServiceURLFormat, i) +} + +// TODO_THIS_COMMIT: move +func NewDebugNotifee(t gocuke.TestingT) libp2pNetwork.Notifiee { + t.Helper() + + return &libp2pNetwork.NotifyBundle{ + ConnectedF: func(_ libp2pNetwork.Network, conn libp2pNetwork.Conn) { + t.Logf("connected: local: %s; remote: %s", + conn.LocalPeer().String(), + conn.RemotePeer().String(), + ) + }, + DisconnectedF: func(_ libp2pNetwork.Network, conn libp2pNetwork.Conn) { + t.Logf("disconnected: local: %s; remote: %s", + conn.LocalPeer().String(), + conn.RemotePeer().String(), + ) + }, + ListenF: func(_ libp2pNetwork.Network, addr multiaddr.Multiaddr) { + t.Logf("listening: %s", addr.String()) + }, + ListenCloseF: func(_ libp2pNetwork.Network, addr multiaddr.Multiaddr) { + t.Logf("closed: %s", addr.String()) + }, + } +} diff --git a/internal/testutil/p2p/constants.go b/internal/testutil/p2p/constants.go new file mode 100644 index 000000000..8cc1f0bc9 --- /dev/null +++ b/internal/testutil/p2p/constants.go @@ -0,0 +1,15 @@ +package p2p_testutil + +import ( + "fmt" + + "github.com/pokt-network/pocket/runtime/defaults" +) + +var ( + // IP4ServiceURL is a string representing a valid IPv4 based ServiceURL using the loopback interface. + IP4ServiceURL = fmt.Sprintf("127.0.0.1:%d", defaults.DefaultP2PPort) + // IP6ServiceURL is a string representing a valid IPv6 based ServiceURL. + // (see: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2) + IP6ServiceURL = fmt.Sprintf("[2a00:1450:4005:802::2004]:%d", defaults.DefaultP2PPort) +) diff --git a/internal/testutil/p2p/mocknet.go b/internal/testutil/p2p/mocknet.go new file mode 100644 index 000000000..e97234510 --- /dev/null +++ b/internal/testutil/p2p/mocknet.go @@ -0,0 +1,45 @@ +package p2p_testutil + +import ( + "testing" + + libp2pCrypto "github.com/libp2p/go-libp2p/core/crypto" + libp2pHost "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/p2p/net/mock" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/pocket/p2p/types" + "github.com/pokt-network/pocket/p2p/utils" + "github.com/pokt-network/pocket/shared/crypto" +) + +func NewTestPeer(t *testing.T) (*types.NetworkPeer, libp2pHost.Host) { + t.Helper() + + selfPrivKey, err := crypto.GeneratePrivateKey() + require.NoError(t, err) + + selfAddr := selfPrivKey.Address() + selfPeer := &types.NetworkPeer{ + PublicKey: selfPrivKey.PublicKey(), + Address: selfAddr, + ServiceURL: IP4ServiceURL, + } + return selfPeer, NewLibp2pMockNetHost(t, selfPrivKey, selfPeer) +} + +func NewLibp2pMockNetHost(t *testing.T, privKey crypto.PrivateKey, peer *types.NetworkPeer) libp2pHost.Host { + t.Helper() + + libp2pPrivKey, err := libp2pCrypto.UnmarshalEd25519PrivateKey(privKey.Bytes()) + require.NoError(t, err) + + libp2pMultiAddr, err := utils.Libp2pMultiaddrFromServiceURL(peer.ServiceURL) + require.NoError(t, err) + + libp2pMockNet := mocknet.New() + host, err := libp2pMockNet.AddPeer(libp2pPrivKey, libp2pMultiAddr) + require.NoError(t, err) + + return host +} diff --git a/internal/testutil/p2p/mocks.go b/internal/testutil/p2p/mocks.go new file mode 100644 index 000000000..61c437a34 --- /dev/null +++ b/internal/testutil/p2p/mocks.go @@ -0,0 +1,41 @@ +package p2p_testutil + +import ( + "testing" + + "github.com/golang/mock/gomock" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/pocket/shared/messaging" + "github.com/pokt-network/pocket/shared/modules" + "github.com/pokt-network/pocket/shared/modules/mocks" +) + +// Creates a p2p module mock with mock implementations of some basic functionality +func BaseP2PMock(t *testing.T, eventsChannel modules.EventsChannel) *mock_modules.MockP2PModule { + ctrl := gomock.NewController(t) + p2pMock := mock_modules.NewMockP2PModule(ctrl) + + p2pMock.EXPECT().Start().Return(nil).AnyTimes() + p2pMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + p2pMock.EXPECT(). + Broadcast(gomock.Any()). + Do(func(msg *anypb.Any) { + e := &messaging.PocketEnvelope{Content: msg} + eventsChannel <- e + }). + AnyTimes() + // CONSIDERATION: Adding a check to not to send message to itself + p2pMock.EXPECT(). + Send(gomock.Any(), gomock.Any()). + Do(func(addr crypto.Address, msg *anypb.Any) { + e := &messaging.PocketEnvelope{Content: msg} + eventsChannel <- e + }). + AnyTimes() + p2pMock.EXPECT().GetModuleName().Return(modules.P2PModuleName).AnyTimes() + p2pMock.EXPECT().HandleEvent(gomock.Any()).Return(nil).AnyTimes() + + return p2pMock +} diff --git a/internal/testutil/p2p/network.go b/internal/testutil/p2p/network.go new file mode 100644 index 000000000..2fd47f150 --- /dev/null +++ b/internal/testutil/p2p/network.go @@ -0,0 +1,140 @@ +package p2p_testutil + +import ( + "testing" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + libp2pPeer "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/net/mock" + "github.com/regen-network/gocuke" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/pokt-network/pocket/internal/testutil" + "github.com/pokt-network/pocket/p2p/types" + "github.com/pokt-network/pocket/p2p/utils" + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/pocket/shared/messaging" +) + +// TODO: remove if not needed +func NewMocknetWithNPeers(t gocuke.TestingT, peerCount int) (mocknet.Mocknet, []string) { + t.Helper() + + // load pre-generated validator keypairs + libp2pNetworkMock := mocknet.New() + privKeys := testutil.LoadLocalnetPrivateKeys(t, peerCount) + serviceURLs := testutil.SequentialServiceURLs(t, peerCount) + _ = SetupMockNetPeers(t, libp2pNetworkMock, privKeys, serviceURLs) + + return libp2pNetworkMock, serviceURLs +} + +func NewLibp2pNetworkMock(t gocuke.TestingT) mocknet.Mocknet { + t.Helper() + + libp2pNetworkMock := mocknet.New() + // destroy mocknet on test cleanup + t.Cleanup(func() { + err := libp2pNetworkMock.Close() + require.NoError(t, err) + }) + + return libp2pNetworkMock +} + +func SetupMockNetPeers( + t gocuke.TestingT, + netMock mocknet.Mocknet, + privKeys []cryptoPocket.PrivateKey, + serviceURLs []string, +) (peerIDs []peer.ID) { + t.Helper() + + // TODO_THIS_COMMIT: return these + // + // MUST add mockdns before any libp2p host comes online. Otherwise, it will + // error while attempting to resolve its own hostname. + _ = testutil.DNSMockFromServiceURLs(t, serviceURLs) + + // Add a libp2p peers/hosts to the `MockNet` with the keypairs corresponding + // to the genesis validators' keypairs + for i, peerInfo := range PeersFromPrivKeysAndServiceURLs(t, privKeys, serviceURLs) { + libp2pPrivKey, err := crypto.UnmarshalEd25519PrivateKey(privKeys[i].Bytes()) + require.NoError(t, err) + + // TODO_THIS_COMMIT: add mock DNS zone per peer instead of all at once + _, err = netMock.AddPeer(libp2pPrivKey, peerInfo.Addrs[0]) + require.NoError(t, err) + + peerIDs = append(peerIDs, peerInfo.ID) + } + + // Link all peers such that any may dial/connect to any other. + err := netMock.LinkAll() + require.NoError(t, err) + + return peerIDs +} + +func PeersFromPrivKeysAndServiceURLs( + t gocuke.TestingT, + privKeys []cryptoPocket.PrivateKey, + serviceURLs []string, +) (peersInfo []libp2pPeer.AddrInfo) { + t.Helper() + + serviceURLCount, privKeyCount := len(serviceURLs), len(privKeys) + maxCount := serviceURLCount + if privKeyCount < serviceURLCount { + maxCount = privKeyCount + } + + for i, privKey := range privKeys[:maxCount] { + peerInfo := peerFromPrivKeyAndServiceURL(t, privKey, testutil.NewServiceURL(i+1)) + peersInfo = append(peersInfo, peerInfo) + } + return peersInfo +} + +func peerFromPrivKeyAndServiceURL( + t gocuke.TestingT, + privKey cryptoPocket.PrivateKey, + serviceURL string, +) libp2pPeer.AddrInfo { + t.Helper() + + peerInfo, err := utils.Libp2pAddrInfoFromPeer(&types.NetworkPeer{ + PublicKey: privKey.PublicKey(), + Address: privKey.Address(), + ServiceURL: serviceURL, + }) + require.NoError(t, err) + + return peerInfo +} + +func NewTestPoktEnvelopeBz(t *testing.T, msg string) []byte { + debugMsg := NewDebugStringMessage(t, msg) + + poktEnvelope, err := messaging.PackMessage(debugMsg) + require.NoError(t, err) + + poktEnvelopeBz, err := proto.Marshal(poktEnvelope) + require.NoError(t, err) + + return poktEnvelopeBz +} + +func NewDebugStringMessage(t gocuke.TestingT, msg string) *messaging.DebugMessage { + debugStringMsg, err := anypb.New(&messaging.DebugStringMessage{Value: msg}) + require.NoError(t, err) + + return &messaging.DebugMessage{ + Action: messaging.DebugMessageAction_DEBUG_ACTION_UNKNOWN, + Type: messaging.DebugMessageRoutingType_DEBUG_MESSAGE_TYPE_BROADCAST, + Message: debugStringMsg, + } +} diff --git a/internal/testutil/persistence/mocks.go b/internal/testutil/persistence/mocks.go new file mode 100644 index 000000000..2e18385b6 --- /dev/null +++ b/internal/testutil/persistence/mocks.go @@ -0,0 +1,85 @@ +package persistence_testutil + +import ( + "fmt" + "github.com/golang/mock/gomock" + "github.com/regen-network/gocuke" + + persistence_mocks "github.com/pokt-network/pocket/persistence/types/mocks" + "github.com/pokt-network/pocket/runtime/genesis" + "github.com/pokt-network/pocket/shared/codec" + "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" + "github.com/pokt-network/pocket/shared/modules/mocks" + "github.com/pokt-network/pocket/shared/utils" +) + +// Persistence mock - only needed for validatorMap access +func BasePersistenceMock(t gocuke.TestingT, busMock *mock_modules.MockBus, genesisState *genesis.GenesisState) *mock_modules.MockPersistenceModule { + ctrl := gomock.NewController(t) + + persistenceModuleMock := mock_modules.NewMockPersistenceModule(ctrl) + readCtxMock := mock_modules.NewMockPersistenceReadContext(ctrl) + + readCtxMock.EXPECT().GetAllValidators(gomock.Any()).Return(genesisState.GetValidators(), nil).AnyTimes() + persistenceModuleMock.EXPECT().NewReadContext(gomock.Any()).Return(readCtxMock, nil).AnyTimes() + readCtxMock.EXPECT().Release().AnyTimes() + + persistenceModuleMock.EXPECT().GetBus().Return(busMock).AnyTimes() + persistenceModuleMock.EXPECT().SetBus(busMock).AnyTimes() + persistenceModuleMock.EXPECT().GetModuleName().Return(modules.PersistenceModuleName).AnyTimes() + busMock.EXPECT().GetPersistenceModule().Return(persistenceModuleMock).AnyTimes() + //busMock.RegisterModule(persistenceModuleMock) + + return persistenceModuleMock +} + +// Creates a persistence module mock with mock implementations of some basic functionality +func PersistenceMockWithBlockStore(t gocuke.TestingT, _ modules.EventsChannel, bus modules.Bus) *mock_modules.MockPersistenceModule { + ctrl := gomock.NewController(t) + persistenceMock := mock_modules.NewMockPersistenceModule(ctrl) + persistenceReadContextMock := mock_modules.NewMockPersistenceReadContext(ctrl) + + persistenceMock.EXPECT().GetModuleName().Return(modules.PersistenceModuleName).AnyTimes() + persistenceMock.EXPECT().Start().Return(nil).AnyTimes() + persistenceMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + persistenceMock.EXPECT().NewReadContext(gomock.Any()).Return(persistenceReadContextMock, nil).AnyTimes() + + persistenceMock.EXPECT().ReleaseWriteContext().Return(nil).AnyTimes() + + blockStoreMock := persistence_mocks.NewMockBlockStore(ctrl) + + blockStoreMock.EXPECT().Get(gomock.Any()).DoAndReturn(func(height []byte) ([]byte, error) { + heightInt := utils.HeightFromBytes(height) + if bus.GetConsensusModule().CurrentHeight() < heightInt { + return nil, fmt.Errorf("requested height is higher than current height of the node's consensus module") + } + blockWithHeight := &types.Block{ + BlockHeader: &types.BlockHeader{ + Height: utils.HeightFromBytes(height), + }, + } + return codec.GetCodec().Marshal(blockWithHeight) + }).AnyTimes() + + persistenceMock.EXPECT().GetBlockStore().Return(blockStoreMock).AnyTimes() + + persistenceReadContextMock.EXPECT().GetMaximumBlockHeight().DoAndReturn(func() (uint64, error) { + height := bus.GetConsensusModule().CurrentHeight() + return height, nil + }).AnyTimes() + + persistenceReadContextMock.EXPECT().GetMinimumBlockHeight().DoAndReturn(func() (uint64, error) { + // mock minimum block height in persistence module to 1 if current height is equal or more than 1, else return 0 as the minimum height + if bus.GetConsensusModule().CurrentHeight() >= 1 { + return 1, nil + } + return 0, nil + }).AnyTimes() + + persistenceReadContextMock.EXPECT().GetAllValidators(gomock.Any()).Return(bus.GetRuntimeMgr().GetGenesis().Validators, nil).AnyTimes() + persistenceReadContextMock.EXPECT().GetBlockHash(gomock.Any()).Return("", nil).AnyTimes() + persistenceReadContextMock.EXPECT().Release().AnyTimes() + + return persistenceMock +} diff --git a/internal/testutil/runtime/genesis.go b/internal/testutil/runtime/genesis.go new file mode 100644 index 000000000..47da6caed --- /dev/null +++ b/internal/testutil/runtime/genesis.go @@ -0,0 +1,68 @@ +package runtime_testutil + +import ( + "github.com/pokt-network/pocket/internal/testutil" + "github.com/pokt-network/pocket/runtime/genesis" + "github.com/pokt-network/pocket/runtime/test_artifacts" + "github.com/pokt-network/pocket/shared/core/types" + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" + "github.com/regen-network/gocuke" +) + +func BaseGenesisStateMock(t gocuke.TestingT, valKeys []cryptoPocket.PublicKey, serviceURLs []string) *genesis.GenesisState { + t.Helper() + + genesisState := new(genesis.GenesisState) + validators := make([]*types.Actor, len(valKeys)) + for i, valKey := range valKeys { + addr := valKey.Address().String() + mockActor := &types.Actor{ + ActorType: types.ActorType_ACTOR_TYPE_VAL, + Address: addr, + PublicKey: valKey.String(), + ServiceUrl: serviceURLs[i], + StakedAmount: test_artifacts.DefaultStakeAmountString, + PausedHeight: int64(0), + UnstakingHeight: int64(0), + Output: addr, + } + validators[i] = mockActor + } + genesisState.Validators = validators + + return genesisState +} + +func BaseGenesisStateMockFromServiceURLKeyMap(t gocuke.TestingT, serviceURLKeyMap map[string]cryptoPocket.PrivateKey) *genesis.GenesisState { + t.Helper() + + var validators []*types.Actor + genesisState := new(genesis.GenesisState) + for serviceURL, privKey := range serviceURLKeyMap { + addr := privKey.Address().String() + mockValidator := &types.Actor{ + ActorType: types.ActorType_ACTOR_TYPE_VAL, + Address: addr, + PublicKey: privKey.PublicKey().String(), + ServiceUrl: serviceURL, + StakedAmount: test_artifacts.DefaultStakeAmountString, + PausedHeight: int64(0), + UnstakingHeight: int64(0), + Output: addr, + } + validators = append(validators, mockValidator) + } + genesisState.Validators = validators + + return genesisState +} + +func GenesisWithSequentialServiceURLs(t gocuke.TestingT, valKeys []cryptoPocket.PublicKey) *genesis.GenesisState { + t.Helper() + + serviceURLs := make([]string, len(valKeys)) + for i := range valKeys { + serviceURLs[i] = testutil.NewServiceURL(i + 1) + } + return BaseGenesisStateMock(t, valKeys, serviceURLs) +} diff --git a/internal/testutil/runtime/mocks.go b/internal/testutil/runtime/mocks.go new file mode 100644 index 000000000..cf6549a08 --- /dev/null +++ b/internal/testutil/runtime/mocks.go @@ -0,0 +1,51 @@ +package runtime_testutil + +import ( + "net" + "strconv" + + "github.com/golang/mock/gomock" + "github.com/regen-network/gocuke" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/pocket/runtime/configs" + "github.com/pokt-network/pocket/runtime/configs/types" + "github.com/pokt-network/pocket/runtime/defaults" + "github.com/pokt-network/pocket/runtime/genesis" + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/pocket/shared/modules" + mock_modules "github.com/pokt-network/pocket/shared/modules/mocks" +) + +func BaseRuntimeManagerMock( + t gocuke.TestingT, + privKey cryptoPocket.PrivateKey, + serviceURL string, + genesisState *genesis.GenesisState, +) modules.RuntimeMgr { + ctrl := gomock.NewController(t) + runtimeMgrMock := mock_modules.NewMockRuntimeMgr(ctrl) + + hostname, portStr, err := net.SplitHostPort(serviceURL) + require.NoError(t, err) + + port, err := strconv.Atoi(portStr) + require.NoError(t, err) + + cfg := &configs.Config{ + RootDirectory: "", + // TODO: need this? + //PrivateKey: privKey.String(), + P2P: &configs.P2PConfig{ + Hostname: hostname, + PrivateKey: privKey.String(), + Port: uint32(port), + ConnectionType: types.ConnectionType_EmptyConnection, + MaxNonces: defaults.DefaultP2PMaxNonces, + }, + } + + runtimeMgrMock.EXPECT().GetConfig().Return(cfg).AnyTimes() + runtimeMgrMock.EXPECT().GetGenesis().Return(genesisState).AnyTimes() + return runtimeMgrMock +} diff --git a/internal/testutil/telemetry/event_metrics_agent.go b/internal/testutil/telemetry/event_metrics_agent.go new file mode 100644 index 000000000..5de2987c0 --- /dev/null +++ b/internal/testutil/telemetry/event_metrics_agent.go @@ -0,0 +1,110 @@ +package telemetry_testutil + +import ( + "github.com/golang/mock/gomock" + "github.com/regen-network/gocuke" + "sync" + + "github.com/pokt-network/pocket/shared/modules/mocks" + "github.com/pokt-network/pocket/telemetry" +) + +func WithP2PIntegrationEvents( + t gocuke.TestingT, + eventMetricsAgentMock *mock_modules.MockEventMetricsAgent, +) *mock_modules.MockEventMetricsAgent { + t.Helper() + + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + + // TODO_THIS_COMMIT: remove v -- may represent failure condition w/ reused nonces.. + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + // END TODO + + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + return eventMetricsAgentMock +} + +// TODO_THIS_COMMIT: refactor... +// Events metric mock - Needed to help with proper counts for number of expected network writes +func PrepareEventMetricsAgentMock(t gocuke.TestingT, valId string, wg *sync.WaitGroup, expectedNumNetworkWrites int) *mock_modules.MockEventMetricsAgent { + ctrl := gomock.NewController(t) + eventMetricsAgentMock := mock_modules.NewMockEventMetricsAgent(ctrl) + + // TODO_THIS_COMMIT: remove + logEvent := func(n, e string, l ...any) { + //t.Logf("n: %s, e: %s, l: %v\n", n, e, l) + } + + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(logEvent).AnyTimes() + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Eq(telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL), gomock.Any()).Do(func(n, e any, l ...any) { + t.Logf("[valId: %s] Write", valId) + wg.Done() + }).Do(logEvent).Times(expectedNumNetworkWrites) + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Not(telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL), gomock.Any()).Do(logEvent).AnyTimes() + + return eventMetricsAgentMock +} + +func WhyEventMetricsAgentMock( + t gocuke.TestingT, + eventMetricsAgentMock *mock_modules.MockEventMetricsAgent, + valId string, + wg *sync.WaitGroup, + //handler func(namespace, eventName string, labels ...any), + expectedNumNetworkWrites int, +) *mock_modules.MockEventMetricsAgent { + // TODO_THIS_COMMIT: remove + logEvent := func(n, e string, l ...any) { + //t.Logf("n: %s, e: %s, l: %v\n", n, e, l) + } + + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(logEvent).AnyTimes() + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Eq(telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL), gomock.Any()).Do(func(n, e any, l ...any) { + t.Logf("[valId: %s] Write", valId) + wg.Done() + }).Do(logEvent).Times(expectedNumNetworkWrites) + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Not(telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL), gomock.Any()).Do(logEvent).AnyTimes() + + return eventMetricsAgentMock +} + +func EventMetricsAgentMockWithHandler( + t gocuke.TestingT, + label string, + // TODO_THIS_COMMIT: consider refactoring as a type + handler func(namespace, eventName string, labels ...any), + times int, +) *mock_modules.MockEventMetricsAgent { + t.Helper() + + ctrl := gomock.NewController(t) + eventMetricsAgentMock := mock_modules.NewMockEventMetricsAgent(ctrl) + + return WithEventMetricsHandler(t, eventMetricsAgentMock, label, handler, times) +} + +func WithEventMetricsHandler( + t gocuke.TestingT, + eventMetricsAgentMock *mock_modules.MockEventMetricsAgent, + label string, + handler func(namespace, eventName string, labels ...any), + times int, +) *mock_modules.MockEventMetricsAgent { + t.Helper() + + //eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Do(logEvent).AnyTimes() + //eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Eq(telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL), gomock.Any()).Do(func(n, e any, l ...any) { + // t.Logf("[valId: %s] Write", valId) + // wg.Done() + //}).Do(logEvent).Times(expectedNumNetworkWrites) + //eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Not(telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL), gomock.Any()).Do(logEvent).AnyTimes() + + // TODO_THIS_COMMIT: scrutinize these & their order + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Eq(label), gomock.Any()).Do(handler).Times(times) + // TODO_THIS_COMMIT: is this really needed? + eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Not(label), gomock.Any()).AnyTimes() + + return eventMetricsAgentMock +} diff --git a/internal/testutil/telemetry/mocks.go b/internal/testutil/telemetry/mocks.go new file mode 100644 index 000000000..0f88302bf --- /dev/null +++ b/internal/testutil/telemetry/mocks.go @@ -0,0 +1,79 @@ +package telemetry_testutil + +import ( + "github.com/golang/mock/gomock" + "github.com/pokt-network/pocket/internal/testutil/generics" + "github.com/regen-network/gocuke" + + "github.com/pokt-network/pocket/shared/modules" + "github.com/pokt-network/pocket/shared/modules/mocks" +) + +func MinimalTelemetryMock( + t gocuke.TestingT, + busMock *mock_modules.MockBus, +) *mock_modules.MockTelemetryModule { + t.Helper() + + ctrl := gomock.NewController(t) + telemetryMock := mock_modules.NewMockTelemetryModule(ctrl) + + busMock.EXPECT().GetTelemetryModule().Return(telemetryMock).AnyTimes() + + return telemetryMock +} + +func BehavesLikeBaseTelemetryMock( + t gocuke.TestingT, + telemetryMock *mock_modules.MockTelemetryModule, +) *mock_modules.MockTelemetryModule { + t.Helper() + + telemetryMock.EXPECT().Start().Return(nil).AnyTimes() + telemetryMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + telemetryMock.EXPECT().GetModuleName().Return(modules.TelemetryModuleName).AnyTimes() + + return telemetryMock +} + +func BaseTelemetryMock( + t gocuke.TestingT, + busMock *mock_modules.MockBus, +) *mock_modules.MockTelemetryModule { + t.Helper() + + return generics_testutil.PipeTwoToOne[ + gocuke.TestingT, + *mock_modules.MockTelemetryModule, + ]( + t, MinimalTelemetryMock(t, busMock), + BehavesLikeBaseTelemetryMock, + WithEventMetricsAgent, + WithTimeSeriesAgent, + ) +} + +func WithTimeSeriesAgent( + t gocuke.TestingT, + telemetryMock *mock_modules.MockTelemetryModule, +) *mock_modules.MockTelemetryModule { + t.Helper() + + timeSeriesAgentMock := BaseTimeSeriesAgentMock(t) + + telemetryMock.EXPECT().GetTimeSeriesAgent().Return(timeSeriesAgentMock).AnyTimes() + return telemetryMock +} + +func WithEventMetricsAgent( + t gocuke.TestingT, + telemetryMock *mock_modules.MockTelemetryModule, +) *mock_modules.MockTelemetryModule { + t.Helper() + + ctrl := gomock.NewController(t) + eventMetricsAgentMock := mock_modules.NewMockEventMetricsAgent(ctrl) + + telemetryMock.EXPECT().GetEventMetricsAgent().Return(eventMetricsAgentMock).AnyTimes() + return telemetryMock +} diff --git a/internal/testutil/telemetry/time_series_agent.go b/internal/testutil/telemetry/time_series_agent.go new file mode 100644 index 000000000..9d63dbd03 --- /dev/null +++ b/internal/testutil/telemetry/time_series_agent.go @@ -0,0 +1,31 @@ +package telemetry_testutil + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/regen-network/gocuke" + + "github.com/pokt-network/pocket/shared/modules/mocks" +) + +func BaseTimeSeriesAgentMock(t gocuke.TestingT) *mock_modules.MockTimeSeriesAgent { + t.Helper() + + ctrl := gomock.NewController(t) + timeSeriesAgentMock := mock_modules.NewMockTimeSeriesAgent(ctrl) + timeSeriesAgentMock.EXPECT().CounterRegister(gomock.Any(), gomock.Any()).AnyTimes() + timeSeriesAgentMock.EXPECT().CounterIncrement(gomock.Any()).AnyTimes() + return timeSeriesAgentMock +} + +// Noop mock - no specific business logic to tend to in the timeseries agent mock +func NoopTelemetryTimeSeriesAgentMock(t *testing.T) *mock_modules.MockTimeSeriesAgent { + ctrl := gomock.NewController(t) + timeseriesAgentMock := mock_modules.NewMockTimeSeriesAgent(ctrl) + + timeseriesAgentMock.EXPECT().CounterRegister(gomock.Any(), gomock.Any()).AnyTimes() + timeseriesAgentMock.EXPECT().CounterIncrement(gomock.Any()).AnyTimes() + + return timeseriesAgentMock +} diff --git a/p2p/background/kad_discovery_baseline_test.go b/p2p/background/kad_discovery_baseline_test.go index fd352456c..7ee351d8a 100644 --- a/p2p/background/kad_discovery_baseline_test.go +++ b/p2p/background/kad_discovery_baseline_test.go @@ -54,7 +54,7 @@ func TestLibp2pKademliaPeerDiscovery(t *testing.T) { expectedPeerIDs = append(expectedPeerIDs, host4.ID()) // TECHDEBT: consider using `host.ConnManager().Notifee()` to avoid sleeping here - time.Sleep(time.Millisecond * 500) + time.Sleep(dhtUpdateSleepDuration) // new host discovers existing hosts... host4DiscoveredHost2Addrs := host4.Peerstore().Addrs(host2.ID()) diff --git a/p2p/background/router.go b/p2p/background/router.go index 199190b21..0579e1d0b 100644 --- a/p2p/background/router.go +++ b/p2p/background/router.go @@ -9,9 +9,12 @@ import ( dht "github.com/libp2p/go-libp2p-kad-dht" pubsub "github.com/libp2p/go-libp2p-pubsub" libp2pHost "github.com/libp2p/go-libp2p/core/host" + "google.golang.org/protobuf/proto" + "github.com/pokt-network/pocket/logger" "github.com/pokt-network/pocket/p2p/config" "github.com/pokt-network/pocket/p2p/protocol" + "github.com/pokt-network/pocket/p2p/providers" typesP2P "github.com/pokt-network/pocket/p2p/types" "github.com/pokt-network/pocket/p2p/utils" cryptoPocket "github.com/pokt-network/pocket/shared/crypto" @@ -29,6 +32,8 @@ type backgroundRouter struct { base_modules.IntegratableModule logger *modules.Logger + // handler is the function to call when a message is received. + handler typesP2P.RouterHandler // host represents a libp2p network node, it encapsulates a libp2p peerstore // & connection manager. `libp2p.New` configures and starts listening // according to options. @@ -47,7 +52,7 @@ type backgroundRouter struct { kadDHT *dht.IpfsDHT // TECHDEBT: `pstore` will likely be removed in future refactoring / simplification // of the `Router` interface. - // pstore is the background router's peerstore. + // pstore is the background router's peerstore. Assigned in `backgroundRouter#setupPeerstore()`. pstore typesP2P.Peerstore } @@ -60,20 +65,10 @@ func NewBackgroundRouter(bus modules.Bus, cfg *config.BackgroundConfig) (typesP2 networkLogger := logger.Global.CreateLoggerForModule("backgroundRouter") networkLogger.Info().Msg("Initializing background router") - // seed initial peerstore with current on-chain peer info (i.e. staked actors) - pstore, err := cfg.PeerstoreProvider.GetStakedPeerstoreAtHeight( - cfg.CurrentHeightProvider.CurrentHeight(), - ) - if err != nil { + if err := cfg.IsValid(); err != nil { return nil, err } - // CONSIDERATION: If switching to `NewRandomSub`, there will be a max size - gossipSub, err := pubsub.NewGossipSub(ctx, cfg.Host) - if err != nil { - return nil, fmt.Errorf("creating gossip pubsub: %w", err) - } - dhtMode := dht.ModeAutoServer // NB: don't act as a bootstrap node in peer discovery in client debug mode if isClientDebugMode(bus) { @@ -85,6 +80,31 @@ func NewBackgroundRouter(bus modules.Bus, cfg *config.BackgroundConfig) (typesP2 return nil, fmt.Errorf("creating DHT: %w", err) } + //if err := kadDHT.Bootstrap(ctx); err != nil { + // return nil, fmt.Errorf("bootstrapping DHT: %w", err) + //} + + // CONSIDERATION: If switching to `NewRandomSub`, there will be a max size + // TECHDEBT: integrate with go-libp2p-pubsub tracing + truncID := cfg.Host.ID().String()[:20] + jsonTracer, err := pubsub.NewJSONTracer(fmt.Sprintf("./pubsub-trace_%s.json", truncID)) + if err != nil { + return nil, fmt.Errorf("creating json tracer: %w", err) + } + //pbTracer, err := pubsub.NewPBTracer("./pubsub-trace.pb") + //if err != nil { + // return nil, fmt.Errorf("creating json tracer: %w", err) + //} + + tracerOpt := pubsub.WithEventTracer(jsonTracer) + //tracerOpt := pubsub.WithEventTracer(pbTracer) + gossipSub, err := pubsub.NewGossipSub(ctx, cfg.Host, tracerOpt) //pubsub.WithFloodPublish(false), + //pubsub.WithMaxMessageSize(256), + + if err != nil { + return nil, fmt.Errorf("creating gossip pubsub: %w", err) + } + topic, err := gossipSub.Join(protocol.BackgroundTopicStr) if err != nil { return nil, fmt.Errorf("joining background topic: %w", err) @@ -95,7 +115,10 @@ func NewBackgroundRouter(bus modules.Bus, cfg *config.BackgroundConfig) (typesP2 // > output buffer. The default length is 32 but it can be configured to avoid // > dropping messages if the consumer is not reading fast enough. // (see: https://pkg.go.dev/github.com/libp2p/go-libp2p-pubsub#WithBufferSize) - subscription, err := topic.Subscribe() + subscription, err := topic.Subscribe( + //pubsub.WithBufferSize(10), + //pubsub.With + ) if err != nil { return nil, fmt.Errorf("subscribing to background topic: %w", err) } @@ -107,16 +130,35 @@ func NewBackgroundRouter(bus modules.Bus, cfg *config.BackgroundConfig) (typesP2 topic: topic, subscription: subscription, logger: networkLogger, - pstore: pstore, + handler: cfg.Handler, + } + + if err := rtr.setupPeerstore( + cfg.PeerstoreProvider, + cfg.CurrentHeightProvider, + ); err != nil { + return nil, err } + go rtr.readSubscription(ctx) + return rtr, nil } // Broadcast implements the respective `typesP2P.Router` interface method. func (rtr *backgroundRouter) Broadcast(data []byte) error { + // CONSIDERATION: validate as PocketEnvelopeBz here (?) + // TODO_THIS_COMMIT: wrap in BackgroundMessage + backgroundMsg := &typesP2P.BackgroundMessage{ + Data: data, + } + backgroundMsgBz, err := proto.Marshal(backgroundMsg) + if err != nil { + return err + } + // TECHDEBT(#595): add ctx to interface methods and propagate down. - return rtr.topic.Publish(context.TODO(), data) + return rtr.topic.Publish(context.TODO(), backgroundMsgBz) } // Send implements the respective `typesP2P.Router` interface method. @@ -132,11 +174,6 @@ func (rtr *backgroundRouter) Send(data []byte, address cryptoPocket.Address) err return nil } -// HandleNetworkData implements the respective `typesP2P.Router` interface method. -func (rtr *backgroundRouter) HandleNetworkData(data []byte) ([]byte, error) { - return data, nil // intentional passthrough -} - // GetPeerstore implements the respective `typesP2P.Router` interface method. func (rtr *backgroundRouter) GetPeerstore() typesP2P.Peerstore { return rtr.pstore @@ -166,6 +203,92 @@ func (rtr *backgroundRouter) RemovePeer(peer typesP2P.Peer) error { return rtr.pstore.RemovePeer(peer.GetAddress()) } +func (rtr *backgroundRouter) Close() error { + // TODO_THIS_COMMIT: why is this causing problems? + //rtr.subscription.Cancel() + + //return multierror.Append( + // rtr.topic.Close(), + // rtr.kadDHT.Close(), + //) + return nil +} + +func (rtr *backgroundRouter) setupPeerstore( + pstoreProvider providers.PeerstoreProvider, + currentHeightProvider providers.CurrentHeightProvider, +) (err error) { + // seed initial peerstore with current on-chain peer info (i.e. staked actors) + rtr.pstore, err = pstoreProvider.GetStakedPeerstoreAtHeight( + currentHeightProvider.CurrentHeight(), + ) + if err != nil { + return err + } + + // CONSIDERATION: add `GetPeers` method to `PeerstoreProvider` interface + // to avoid this loop. + for _, peer := range rtr.pstore.GetPeerList() { + if err := utils.AddPeerToLibp2pHost(rtr.host, peer); err != nil { + return err + } + + // TODO: refactor: #bootstrap() + libp2pPeer, err := utils.Libp2pAddrInfoFromPeer(peer) + if err != nil { + return fmt.Errorf( + "converting peer info, pokt address: %s: %w", + peer.GetAddress(), + err, + ) + } + + // don't attempt to connect to self + if rtr.host.ID() == libp2pPeer.ID { + return nil + } + + // TECHDEBT(#595): add ctx to interface methods and propagate down. + if err := rtr.host.Connect(context.TODO(), libp2pPeer); err != nil { + return fmt.Errorf("connecting to peer: %w", err) + } + } + return nil +} + +func (rtr *backgroundRouter) readSubscription(ctx context.Context) { + // TODO_THIS_COMMIT: look into "topic validaton" + // (see: https://github.com/libp2p/specs/tree/master/pubsub#topic-validation) + for { + msg, err := rtr.subscription.Next(ctx) + if ctx.Err() != nil { + fmt.Printf("error: %s\n", ctx.Err()) + return + } + + if err != nil { + rtr.logger.Error().Err(err). + Msg("error reading from background topic subscription") + continue + } + + // TECHDEBT/DISCUSS: telemetry + if err := rtr.handleBackgroundMsg(msg.Data); err != nil { + rtr.logger.Error().Err(err).Msg("error handling background message") + continue + } + } +} + +func (rtr *backgroundRouter) handleBackgroundMsg(backgroundMsgBz []byte) error { + var backgroundMsg typesP2P.BackgroundMessage + if err := proto.Unmarshal(backgroundMsgBz, &backgroundMsg); err != nil { + return err + } + + return rtr.handler(backgroundMsg.Data) +} + // isClientDebugMode returns the value of `ClientDebugMode` in the base config func isClientDebugMode(bus modules.Bus) bool { return bus.GetRuntimeMgr().GetConfig().ClientDebugMode diff --git a/p2p/background/router_test.go b/p2p/background/router_test.go index a1a0fe40b..6b3219d12 100644 --- a/p2p/background/router_test.go +++ b/p2p/background/router_test.go @@ -14,7 +14,11 @@ import ( libp2pPeer "github.com/libp2p/go-libp2p/core/peer" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/multiformats/go-multiaddr" - "github.com/pokt-network/pocket/internal/testutil" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + "github.com/pokt-network/pocket/internal/testutil/generics" + "github.com/pokt-network/pocket/internal/testutil/p2p" "github.com/pokt-network/pocket/p2p/config" typesP2P "github.com/pokt-network/pocket/p2p/types" mock_types "github.com/pokt-network/pocket/p2p/types/mocks" @@ -22,16 +26,28 @@ import ( "github.com/pokt-network/pocket/runtime/configs" "github.com/pokt-network/pocket/runtime/defaults" cryptoPocket "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/pocket/shared/messaging" mockModules "github.com/pokt-network/pocket/shared/modules/mocks" - "github.com/stretchr/testify/require" ) // https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 -const testIP6ServiceURL = "[2a00:1450:4005:802::2004]:8080" +const ( + testIP6ServiceURL = "[2a00:1450:4005:802::2004]:8080" + numPeers = 4 + testMsg = "test messsage" + testTimeoutDuration = time.Second * 2 +) // TECHDEBT(#609): move & de-dup. var testLocalServiceURL = fmt.Sprintf("127.0.0.1:%d", defaults.DefaultP2PPort) +func TestBackgroundRouter_InvalidConfig(t *testing.T) { + t.Skip("pending") + //busMock := bus_testutil.NewBus(t) + // + //router, err := NewBackgroundRouter() +} + func TestBackgroundRouter_AddPeer(t *testing.T) { testRouter := newTestRouter(t, nil) libp2pPStore := testRouter.host.Peerstore() @@ -115,19 +131,15 @@ func TestBackgroundRouter_RemovePeer(t *testing.T) { } func TestBackgroundRouter_Broadcast(t *testing.T) { - const ( - numPeers = 4 - testMsg = "test messsage" - testTimeoutDuration = time.Second * 5 - ) - var ( ctx = context.Background() // mutex preventing concurrent writes to `seenMessages` - seenMessagesMutext sync.Mutex + seenMessagesMutex sync.Mutex // map used as a set to collect IDs of peers which have received a message seenMessages = make(map[string]struct{}) bootstrapWaitgroup = sync.WaitGroup{} + bootstrapPeerIDCh = make(chan string) + bootstrapPeerIDs = make(map[string]struct{}) broadcastWaitgroup = sync.WaitGroup{} broadcastDone = make(chan struct{}, 1) testTimeout = time.After(testTimeoutDuration) @@ -148,9 +160,31 @@ func TestBackgroundRouter_Broadcast(t *testing.T) { testHosts = append(testHosts, host) expectedPeerIDs[i] = host.ID().String() rtr := newRouterWithSelfPeerAndHost(t, selfPeer, host) - go readSubscription(t, ctx, &broadcastWaitgroup, rtr, &seenMessagesMutext, seenMessages) + rtr.HandlerProxy(t, func(origHandler typesP2P.RouterHandler) typesP2P.RouterHandler { + return func(data []byte) error { + seenMessagesMutex.Lock() + broadcastWaitgroup.Done() + seenMessages[rtr.host.ID().String()] = struct{}{} + seenMessagesMutex.Unlock() + + return origHandler(data) + } + }) } + // concurrently update the set of bootstrapped peer IDs as they connect + go func() { + for { + peerIDStr := <-bootstrapPeerIDCh + if _, ok := bootstrapPeerIDs[peerIDStr]; ok { + // already connected to this peer during bootstrapping + continue + } + bootstrapPeerIDs[peerIDStr] = struct{}{} + bootstrapWaitgroup.Done() + } + }() + // bootstrap off of arbitrary testHost privKey, selfPeer := newTestPeer(t) @@ -166,9 +200,14 @@ func TestBackgroundRouter_Broadcast(t *testing.T) { // setup notifee/notify BEFORE bootstrapping notifee := &libp2pNetwork.NotifyBundle{ - ConnectedF: func(_ libp2pNetwork.Network, _ libp2pNetwork.Conn) { + ConnectedF: func(_ libp2pNetwork.Network, conn libp2pNetwork.Conn) { t.Logf("connected!") - bootstrapWaitgroup.Done() + t.Logf("local PeerID %s; remote PeerID: %s", + conn.LocalPeer().String(), + conn.RemotePeer().String(), + ) + bootstrapPeerIDCh <- conn.RemotePeer().String() + //bootstrapWaitgroup.Done() }, } testRouter.host.Network().Notify(notifee) @@ -189,7 +228,8 @@ func TestBackgroundRouter_Broadcast(t *testing.T) { // broadcast message t.Log("broadcasting...") - err := testRouter.Broadcast([]byte(testMsg)) + testPoktEnvelopeBz := p2p_testutil.NewTestPoktEnvelopeBz(t, testMsg) + err = testRouter.Broadcast(testPoktEnvelopeBz) require.NoError(t, err) // wait for broadcast to be received by all peers @@ -200,6 +240,7 @@ func TestBackgroundRouter_Broadcast(t *testing.T) { // waitgroup broadcastDone or timeout select { case <-testTimeout: + seenMessagesMutex.Lock() t.Fatalf( "timed out waiting for all expected messages: got %d; wanted %d", len(seenMessages), @@ -208,7 +249,10 @@ func TestBackgroundRouter_Broadcast(t *testing.T) { case <-broadcastDone: } - actualPeerIDs = testutil.GetKeys[string](seenMessages) + seenMessagesMutex.Lock() + defer seenMessagesMutex.Unlock() + + actualPeerIDs = generics_testutil.GetKeys[string](seenMessages) require.ElementsMatchf(t, expectedPeerIDs, actualPeerIDs, "peerIDs don't match") } @@ -284,11 +328,33 @@ func newRouterWithSelfPeerAndHost(t *testing.T, selfPeer typesP2P.Peer, host lib err := pstore.AddPeer(selfPeer) require.NoError(t, err) + handler := func(poktEnvelopeBz []byte) error { + poktEnvelope := &messaging.PocketEnvelope{} + err := proto.Unmarshal(poktEnvelopeBz, poktEnvelope) + require.NoError(t, err) + + require.NotEmpty(t, poktEnvelope.Nonce) + require.NotEmpty(t, poktEnvelope.Content) + + debugMsg := &messaging.DebugMessage{} + err = poktEnvelope.Content.UnmarshalTo(debugMsg) + require.NoError(t, err) + + debugStringMsg := &messaging.DebugStringMessage{} + err = debugMsg.Message.UnmarshalTo(debugStringMsg) + require.NoError(t, err) + + require.Equal(t, testMsg, debugStringMsg.Value, "debug string messages don't match") + + return nil + } + router, err := NewBackgroundRouter(busMock, &config.BackgroundConfig{ Addr: selfPeer.GetAddress(), PeerstoreProvider: pstoreProviderMock, CurrentHeightProvider: consensusMock, Host: host, + Handler: handler, }) require.NoError(t, err) @@ -345,31 +411,3 @@ func newTestHost(t *testing.T, mockNet mocknet.Mocknet, privKey cryptoPocket.Pri // construct mock host return newMockNetHostFromPeer(t, mockNet, privKey, peer) } - -func readSubscription( - t *testing.T, - ctx context.Context, - broadcastWaitGroup *sync.WaitGroup, - rtr *backgroundRouter, - mu *sync.Mutex, - seenMsgs map[string]struct{}, -) { - t.Helper() - - for { - if err := ctx.Err(); err != nil { - if err != context.Canceled || err != context.DeadlineExceeded { - require.NoError(t, err) - } - return - } - - _, err := rtr.subscription.Next(ctx) - require.NoError(t, err) - - mu.Lock() - broadcastWaitGroup.Done() - seenMsgs[rtr.host.ID().String()] = struct{}{} - mu.Unlock() - } -} diff --git a/p2p/background/testutil.go b/p2p/background/testutil.go new file mode 100644 index 000000000..c6de4fc61 --- /dev/null +++ b/p2p/background/testutil.go @@ -0,0 +1,26 @@ +//go:build test + +package background + +import ( + "github.com/pokt-network/pocket/internal/testutil/generics" + typesP2P "github.com/pokt-network/pocket/p2p/types" + "github.com/regen-network/gocuke" +) + +// BackgroundRouter exports `backgroundRouter` for testing purposes. +type BackgroundRouter = backgroundRouter + +// TOOD_THIS_COMMIT: move & dedup +type routerHandlerProxyFactory = generics_testutil.ProxyFactory[typesP2P.RouterHandler] + +func (rtr *backgroundRouter) HandlerProxy( + t gocuke.TestingT, + handlerProxyFactory routerHandlerProxyFactory, +) { + t.Helper() + + // pass original handler to proxy factory & replace it with the proxy + rtr.handler = handlerProxyFactory(rtr.handler) +} + diff --git a/p2p/bootstrap.go b/p2p/bootstrap.go index f9f76a049..00a89de2f 100644 --- a/p2p/bootstrap.go +++ b/p2p/bootstrap.go @@ -76,14 +76,14 @@ func (m *p2pModule) bootstrap() error { for _, peer := range pstore.GetPeerList() { m.logger.Debug().Str("address", peer.GetAddress().String()).Msg("Adding peer to router") - if err := m.router.AddPeer(peer); err != nil { + if err := m.stakedActorRouter.AddPeer(peer); err != nil { m.logger.Error().Err(err). Str("pokt_address", peer.GetAddress().String()). Msg("adding peer") } } - if m.router.GetPeerstore().Size() == 0 { + if m.stakedActorRouter.GetPeerstore().Size() == 0 { return fmt.Errorf("bootstrap failed") } return nil diff --git a/p2p/config/config.go b/p2p/config/config.go index e64fe8519..2ff62e966 100644 --- a/p2p/config/config.go +++ b/p2p/config/config.go @@ -20,6 +20,7 @@ type baseConfig struct { Addr crypto.Address CurrentHeightProvider providers.CurrentHeightProvider PeerstoreProvider providers.PeerstoreProvider + Handler func(data []byte) error } // BackgroundConfig implements `RouterConfig` for use with `BackgroundRouter`. @@ -57,6 +58,10 @@ func (cfg *baseConfig) IsValid() (err error) { if cfg.PeerstoreProvider == nil { err = multierr.Append(err, fmt.Errorf("peerstore provider not configured")) } + + if cfg.Handler == nil { + err = multierr.Append(err, fmt.Errorf("handler not configured")) + } return err } @@ -67,6 +72,7 @@ func (cfg *BackgroundConfig) IsValid() (err error) { Addr: cfg.Addr, CurrentHeightProvider: cfg.CurrentHeightProvider, PeerstoreProvider: cfg.PeerstoreProvider, + Handler: cfg.Handler, } return multierr.Append(err, baseCfg.IsValid()) } @@ -78,6 +84,7 @@ func (cfg *RainTreeConfig) IsValid() (err error) { Addr: cfg.Addr, CurrentHeightProvider: cfg.CurrentHeightProvider, PeerstoreProvider: cfg.PeerstoreProvider, + Handler: cfg.Handler, } return multierr.Append(err, baseCfg.IsValid()) } diff --git a/p2p/event_handler.go b/p2p/event_handler.go index 48e1a7d73..8a2b30473 100644 --- a/p2p/event_handler.go +++ b/p2p/event_handler.go @@ -23,7 +23,7 @@ func (m *p2pModule) HandleEvent(event *anypb.Any) error { return fmt.Errorf("failed to cast event to ConsensusNewHeightEvent") } - oldPeerList := m.router.GetPeerstore().GetPeerList() + oldPeerList := m.stakedActorRouter.GetPeerstore().GetPeerList() updatedPeerstore, err := m.pstoreProvider.GetStakedPeerstoreAtHeight(consensusNewHeightEvent.Height) if err != nil { return err @@ -31,12 +31,12 @@ func (m *p2pModule) HandleEvent(event *anypb.Any) error { added, removed := oldPeerList.Delta(updatedPeerstore.GetPeerList()) for _, add := range added { - if err := m.router.AddPeer(add); err != nil { + if err := m.stakedActorRouter.AddPeer(add); err != nil { return err } } for _, rm := range removed { - if err := m.router.RemovePeer(rm); err != nil { + if err := m.stakedActorRouter.RemovePeer(rm); err != nil { return err } } @@ -50,7 +50,7 @@ func (m *p2pModule) HandleEvent(event *anypb.Any) error { m.logger.Debug().Fields(messaging.TransitionEventToMap(stateMachineTransitionEvent)).Msg("Received state machine transition event") if stateMachineTransitionEvent.NewState == string(coreTypes.StateMachineState_P2P_Bootstrapping) { - if m.router.GetPeerstore().Size() == 0 { + if m.stakedActorRouter.GetPeerstore().Size() == 0 { m.logger.Warn().Msg("No peers in addrbook, bootstrapping") if err := m.bootstrap(); err != nil { diff --git a/p2p/integration/background_gossip.feature b/p2p/integration/background_gossip.feature new file mode 100644 index 000000000..1799fbf23 --- /dev/null +++ b/p2p/integration/background_gossip.feature @@ -0,0 +1,54 @@ +Feature: Background Gossip Broadcast + + Scenario Outline: Complete broadcast + Given a fully connected network of peers + When a node broadcasts a test message via its background router + Then minus one number of nodes should receive the test message + + Examples: + | size | + | 3 | +# | 4 | +# | 5 | +# | 6 | +# | 7 | +# | 8 | +# | 9 | +# | 10 | +# | 11 | +# TODO_THIS_COMMIT: figure out why these are failing +# | 12 | +# | 13 | +# | 18 | +# | 27 | +# | 100 | +## | 1024 | + +# Scenario Outline: Partial broadcast +# Given a faulty network of peers +# And number of faulty peers +# When a node broadcasts a test message via its background router +# Then number of nodes should receive the test message +# +# Examples: +# | size | faulty | received | +# | 4 | 2 | 2 | +# | 6 | 3 | 3 | +# | 12 | 8 | 4 | +## | 100 | 75 | 25 | +## | 1024 | 1000 | 24 | +# +# Scenario Outline: Broadcast during churn +# Given a fully connected network of peers +# When number of nodes join the network +# And number of nodes leave the network +# And a node broadcasts a test message via its background router +# Then number of nodes should receive the test message +# +# Examples: +# | size | leave | join | received | +# | 4 | 2 | 2 | 4 | +# | 4 | 3 | 4 | 5 | +# | 12 | 6 | 6 | 12 | +## | 100 | 50 | 55 | 105 | +## | 1024 | 1000 | 1200 | 1224 | diff --git a/p2p/integration/background_gossip_test.go b/p2p/integration/background_gossip_test.go new file mode 100644 index 000000000..6ee5944bc --- /dev/null +++ b/p2p/integration/background_gossip_test.go @@ -0,0 +1,467 @@ +//go:build integration && test + +package integration + +import ( + "fmt" + "github.com/foxcpp/go-mockdns" + libp2pNetwork "github.com/libp2p/go-libp2p/core/network" + libp2pPeer "github.com/libp2p/go-libp2p/core/peer" + typesP2P "github.com/pokt-network/pocket/p2p/types" + "sync" + "testing" + "time" + + libp2pMocknet "github.com/libp2p/go-libp2p/p2p/net/mock" + "github.com/regen-network/gocuke" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/pokt-network/pocket/internal/testutil" + "github.com/pokt-network/pocket/internal/testutil/constructors" + "github.com/pokt-network/pocket/internal/testutil/generics" + p2p_testutil "github.com/pokt-network/pocket/internal/testutil/p2p" + runtime_testutil "github.com/pokt-network/pocket/internal/testutil/runtime" + telemetry_testutil "github.com/pokt-network/pocket/internal/testutil/telemetry" + "github.com/pokt-network/pocket/p2p" + "github.com/pokt-network/pocket/runtime/defaults" + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/pocket/shared/messaging" + "github.com/pokt-network/pocket/shared/modules" + mock_modules "github.com/pokt-network/pocket/shared/modules/mocks" +) + +type PeerIDSet map[libp2pPeer.ID]struct{} + +const ( + backgroundGossipFeaturePath = "background_gossip.feature" + //broadcastTimeoutDuration = time.Millisecond * 250 + broadcastTimeoutDuration = time.Second * 1 + // TODO_THIS_COMMIT: move + bootstrapTimeoutMinSeconds = 3 +) + +func TestMinimal(t *testing.T) { + t.Parallel() + + // a new step definition suite is constructed for every scenario + gocuke.NewRunner(t, new(backgroundGossipSuite)).Path(backgroundGossipFeaturePath).Run() +} + +type peerConnectionEvent struct { + localID libp2pPeer.ID + remoteID libp2pPeer.ID +} + +type backgroundGossipSuite struct { + // special arguments like TestingT are injected automatically into exported fields + gocuke.TestingT + dnsSrv *mockdns.Server + + timeoutDuration time.Duration + // TODO_THIS_COMMIT: rename + mu sync.Mutex + receivedServiceURLCh chan string + // receivedServiceURLMap is used as a map to track which messages have been + // received by which nodes. + receivedServiceURLMap map[string]struct{} + bootstrapMutex sync.Mutex + + // bootstrapPeerIDChMap is a mapping between the peerID string of each node to + // a channel that will be used to signal the peer ID strings of each node it + // has discovered. + //bootstrapPeerIDChMap map[libp2pPeer.ID]chan libp2pPeer.ID + + bootstrapPeerIDCh chan peerConnectionEvent + + // bootstrapPeerIDsMap is a mapping between the peerID string of each node to a + // set of peerID strings that node has discovered. This set is represented as + // a map with the peerID string as the key and an empty struct as the value. + bootstrapPeerIDsMap map[libp2pPeer.ID]PeerIDSet + + bootstrapNetworkWaitGroup sync.WaitGroup + // TODO_THIS_COMMIT: use this? + receivedCount int + receivedWaitGroup sync.WaitGroup + p2pModules map[string]modules.P2PModule + busMocks map[string]*mock_modules.MockBus + libp2pNetworkMock libp2pMocknet.Mocknet + sender *p2p.P2PModule +} + +func (s *backgroundGossipSuite) Before(_ gocuke.Scenario) { + s.mu.Lock() + defer s.mu.Unlock() + + s.dnsSrv = testutil.MinimalDNSMock(s) + //s.bootstrapPeerIDChMap = make(map[libp2pPeer.ID]chan libp2pPeer.ID) + s.bootstrapPeerIDCh = make(chan peerConnectionEvent) + s.receivedServiceURLCh = make(chan string) + s.receivedServiceURLMap = make(map[string]struct{}) +} + +func (s *backgroundGossipSuite) After(_ gocuke.Scenario) { + close(s.receivedServiceURLCh) +} + +func (s *backgroundGossipSuite) AFaultyNetworkOfPeers(a int64) { + panic("PENDING") +} + +func (s *backgroundGossipSuite) NumberOfFaultyPeers(a int64) { + panic("PENDING") +} + +func (s *backgroundGossipSuite) NumberOfNodesJoinTheNetwork(a int64) { + panic("PENDING") +} + +func (s *backgroundGossipSuite) NumberOfNodesLeaveTheNetwork(a int64) { + panic("PENDING") +} + +func (s *backgroundGossipSuite) AFullyConnectedNetworkOfPeers(count int64) { + peerCount := int(count) + + // TODO_THIS_COMMOT: comment, explain + //s.bootstrapNetworkWaitGroup.Add(peerCount) + //s.bootstrapNetworkWaitGroup.Add(peerCount * (peerCount - 1) / 2) + s.bootstrapNetworkWaitGroup.Add(peerCount * (peerCount - 1)) + s.mu.Lock() + defer s.mu.Unlock() + + serviceURLKeyMap := testutil.SequentialServiceURLPrivKeyMap(s, peerCount) + genesisState := runtime_testutil.BaseGenesisStateMockFromServiceURLKeyMap(s, serviceURLKeyMap) + + // TODO_THIS_COMMIT: refactor + go func() { + for serviceURL := range s.receivedServiceURLCh { + // use a channel instead of a map; send a struct like w/ trackBootstrapProgress() + // THIS IS PER BUS (i.e. run in each node)!! + _, ok := s.receivedServiceURLMap[serviceURL] + require.Falsef(s, ok, "received message from duplicate serviceURL: %s", serviceURL) + + s.receivedCount++ + s.receivedServiceURLMap[serviceURL] = struct{}{} + s.receivedWaitGroup.Done() + } + }() + + //s.Cleanup(func() { + // close(s.receivedServiceURLCh) + //}) + + busEventHandlerFactory := func(t gocuke.TestingT, busMock *mock_modules.MockBus) testutil.BusEventHandler { + // event handler is called when a p2p module receives a network message + return func(data *messaging.PocketEnvelope) { + s.mu.Lock() + defer s.mu.Unlock() + + //defer func() { + // if r := recover(); r != nil { + // t.Logf("seenCount: %d; receivedServiceURLMap: %v", len(s.receivedServiceURLMap), s.receivedServiceURLMap) + // //panic(r) + // t.Fatalf("panic: %v", r) + // } + //}() + + p2pCfg := busMock.GetRuntimeMgr().GetConfig().P2P + serviceURL := fmt.Sprintf("%s:%d", p2pCfg.Hostname, defaults.DefaultP2PPort) + + peerPrivKey, err := cryptoPocket.NewPrivateKey(p2pCfg.PrivateKey) + require.NoError(t, err) + + senderAddr, err := s.sender.GetAddress() + require.NoError(t, err) + + if senderAddr.Equals(peerPrivKey.Address()) { + t.Logf("ignoring message from self: %s", serviceURL) + return + } + t.Logf("received message from %s", serviceURL) + + s.receivedServiceURLCh <- serviceURL + } + } + // -- + + // TODO_THIS_COMMIT: refactor + //debugNotifiee := testutil.NewDebugNotifee(s) + notifiee := &libp2pNetwork.NotifyBundle{ + //DisconnectedF: func(network libp2pNetwork.Network, conn libp2pNetwork.Conn) { + // s.Logf("disconnected: %s", conn.RemotePeer()) + //}, + //DisconnectedF: debugNotifiee.Disconnected, + ConnectedF: func(net libp2pNetwork.Network, conn libp2pNetwork.Conn) { + //s.Logf("connected: %s", conn.RemotePeer()) + //s.Logf("pstore size: %d", len(p2pModule.GetHost().Peerstore().Peers())) + + s.bootstrapPeerIDCh <- peerConnectionEvent{ + localID: conn.LocalPeer(), + remoteID: conn.RemotePeer(), + } + //s.bootstrapPeerIDChMap[p2pModule.GetHost().ID()] <- conn.RemotePeer() + //s.Logf("bootstrap peer ID sent on channel") + //if len(p2pModule.GetHost().Peerstore().Peers()) == peerCount { + // countz++ + // s.Logf("count: %d", countz) + // //s.bootstrapNetworkWaitGroup.Done() + // //s.bootstrapNetworkWaitGroup.Done() + // //s.bootstrapNetworkWaitGroup.Done() + //} + //debugNotifiee.Connected(net, conn) + }, + //ListenF: debugNotifiee.Listen, + //ListenCloseF: debugNotifiee.ListenClose, + } + // -- + + // setup mock network + s.busMocks, s.libp2pNetworkMock, s.p2pModules = constructors.NewBusesMocknetAndP2PModules( + s, peerCount, + s.dnsSrv, + genesisState, + busEventHandlerFactory, + notifiee, + ) + + // add expectations for P2P events to telemetry module's event metrics agent + for _, busMock := range s.busMocks { + // TODO_THIS_COMMIT: ?? + eventMetricsAgentMock := busMock. + GetTelemetryModule(). + GetEventMetricsAgent().(*mock_modules.MockEventMetricsAgent) + + // TODO_THIS_COMMIT: ?? + telemetry_testutil.WithP2PIntegrationEvents( + s, eventMetricsAgentMock, + ) + } + + // TODO_THIS_COMMIT: bus event handler based receivedWaitGroup.Done()! + + // concurrently update `s.bootstrapPeerIDsMap` by receiving from the + // corresponding channel from `s.bootstrapPeerIDChMap` that `notifee` + // is sending on. + go s.trackBootstrapProgress(peerCount - 1) + + // start P2P modules of all peers + //handleCount := 0 + for _, module := range s.p2pModules { + //countz := 0 + p2pModule := module.(*p2p.P2PModule) + + // TODO_THIS_COMMIT: refactor + //debugNotifiee := testutil.NewDebugNotifee(s) + //notifee := &libp2pNetwork.NotifyBundle{ + // //DisconnectedF: func(network libp2pNetwork.Network, conn libp2pNetwork.Conn) { + // // s.Logf("disconnected: %s", conn.RemotePeer()) + // //}, + // DisconnectedF: debugNotifiee.Disconnected, + // ConnectedF: func(net libp2pNetwork.Network, conn libp2pNetwork.Conn) { + // //s.Logf("connected: %s", conn.RemotePeer()) + // //s.Logf("pstore size: %d", len(p2pModule.GetHost().Peerstore().Peers())) + // s.bootstrapMutex.Lock() + // defer s.bootstrapMutex.Unlock() + // + // s.bootstrapPeerIDCh <- peerConnectionEvent{ + // localID: conn.LocalPeer(), + // remoteID: conn.RemotePeer(), + // } + // //s.bootstrapPeerIDChMap[p2pModule.GetHost().ID()] <- conn.RemotePeer() + // //s.Logf("bootstrap peer ID sent on channel") + // //if len(p2pModule.GetHost().Peerstore().Peers()) == peerCount { + // // countz++ + // // s.Logf("count: %d", countz) + // // //s.bootstrapNetworkWaitGroup.Done() + // // //s.bootstrapNetworkWaitGroup.Done() + // // //s.bootstrapNetworkWaitGroup.Done() + // //} + // debugNotifiee.Connected(net, conn) + // }, + // ListenF: debugNotifiee.Listen, + // ListenCloseF: debugNotifiee.ListenClose, + //} + ////p2pModule.GetHost().Network().Notify(debugNotifiee) + //p2pModule.GetHost().Network().Notify(notifee) + // -- + + err := p2pModule.Start() + require.NoError(s, err) + + // TODO_THIS_COMMIT: fix + //s.Cleanup(func() { + // err := p2pModule.Stop() + // require.NoError(s, err) + //}) + + handlerProxyFactory := func( + origHandler typesP2P.RouterHandler, + ) (proxyHandler typesP2P.RouterHandler) { + return func(data []byte) error { + //s.receivedWaitGroup.Done() + return origHandler(data) + } + } + + // TODO_THIS_COMMIT: look into go-libp2p-pubsub tracing + // (see: https://github.com/libp2p/go-libp2p-pubsub#tracing) + noopHandlerProxyFactory := func(_ typesP2P.RouterHandler) typesP2P.RouterHandler { + return func(_ []byte) error { + // noop + return nil + } + } + + p2pModule.GetRainTreeRouter().HandlerProxy( + s, noopHandlerProxyFactory, + ) + + p2pModule.GetBackgroundRouter().HandlerProxy( + s, handlerProxyFactory, + ) + + } + + // wait for bootstrapping to complete + bootstrapDone := make(chan struct{}, 0) + go func() { + s.bootstrapNetworkWaitGroup.Wait() + close(bootstrapDone) + }() + + var bootstrapTimeoutCh <-chan time.Time + if peerCount <= 20 { + bootstrapTimeoutCh = time.After(time.Second * bootstrapTimeoutMinSeconds) + } else { + largeNetworkTimeoutMilliseconds := (bootstrapTimeoutMinSeconds * 1000) + (250 * peerCount) + bootstrapTimeoutCh = time.After(time.Millisecond * time.Duration(largeNetworkTimeoutMilliseconds)) + } + + select { + case <-bootstrapTimeoutCh: + s.Fatal("timed out waiting for bootstrapping") + case <-bootstrapDone: + // TODO_THIS_COMMIT: try to remove... + // wait for DHT bootstrapping + time.Sleep(time.Millisecond * 500) + } +} + +func (s *backgroundGossipSuite) ANodeBroadcastsATestMessageViaItsBackgroundRouter() { + // TODO_THIS_COMMIT: refactor + s.timeoutDuration = broadcastTimeoutDuration + + // select arbitrary sender & store in context for reference later + s.sender = s.p2pModules[generics_testutil.GetKeys(s.p2pModules)[0]].(*p2p.P2PModule) + + // broadcast a test message + debugStringMsg := p2p_testutil.NewDebugStringMessage(s, "test message") + msg, err := anypb.New(debugStringMsg) + require.NoError(s, err) + + s.receivedWaitGroup.Add(1) + + // TODO_THIS_COMMIT: refactor + //for i := 0; i > 10; i++ { + s.receivedWaitGroup.Add(len(s.p2pModules) - 1) + err = s.sender.Broadcast(msg) + require.NoError(s, err) + //} + + s.receivedWaitGroup.Done() +} + +func (s *backgroundGossipSuite) MinusOneNumberOfNodesShouldReceiveTheTestMessage(receivedCountPlus1 int64) { + done := make(chan struct{}, 1) + + go func() { + s.receivedWaitGroup.Wait() + s.mu.Lock() + defer s.mu.Unlock() + + receivedCount := int(receivedCountPlus1 - 1) + require.Equalf( + s, receivedCount, s.receivedCount, + "received messages count doesn't match", + ) + done <- struct{}{} + }() + + select { + case <-time.After(s.timeoutDuration): + s.mu.Lock() + defer s.mu.Unlock() + + s.Fatalf("timed out waiting for messages to be received; received: %d; receivedServiceURLMap: %v", s.receivedCount, s.receivedServiceURLMap) + case <-done: + s.Logf("seenCount: %d; receivedServiceURLMap: %v", len(s.receivedServiceURLMap), s.receivedServiceURLMap) + } +} + +//func (s *backgroundGossipSuite) initBootstrapPeerIDChMap(p2pModule *p2p.P2PModule) { +// s.bootstrapMutex.Lock() +// defer s.bootstrapMutex.Unlock() +// +// selfID := p2pModule.GetHost().ID() +// // initialize `s.bootstrapPeerIDChMap` for each p2pModule +// if _, ok := s.bootstrapPeerIDChMap[selfID]; !ok { +// s.bootstrapPeerIDChMap[selfID] = make(chan libp2pPeer.ID) +// } +//} + +func (s *backgroundGossipSuite) initBootstrapPeerIDsMap(selfID libp2pPeer.ID) { + // TODO_THIS_COMMIT: need this? + s.bootstrapMutex.Lock() + defer s.bootstrapMutex.Unlock() + + // initialize `s.bootstrapPeerIDsMap` + if s.bootstrapPeerIDsMap == nil { + s.bootstrapPeerIDsMap = make(map[libp2pPeer.ID]PeerIDSet) + } + + // initialize `s.bootstrapPeerIDsMap` for each p2pModule + if _, ok := s.bootstrapPeerIDsMap[selfID]; !ok { + s.bootstrapPeerIDsMap[selfID] = make(PeerIDSet) + } +} + +func (s *backgroundGossipSuite) trackBootstrapProgress(peerCount int) { + //selfID := p2pModule.GetHost().ID() + + // add unique bootstrap peer IDs to `bootstrapPeerIDsMap` for this + // p2pModule (`selfID`) as they connect + for newPeerConnectionEvent := range s.bootstrapPeerIDCh { + //newBootstrapPeerID := <-s.bootstrapPeerIDChMap[selfID] + localID, remoteID := newPeerConnectionEvent.localID, newPeerConnectionEvent.remoteID + + s.initBootstrapPeerIDsMap(localID) + + if localID == remoteID { + // don't count self as a bootstrap peer + s.Logf("self bootstrap peer ID: %s") + continue + } + + if _, ok := s.bootstrapPeerIDsMap[localID][remoteID]; ok { + // already connected to this peer during bootstrapping + //s.Logf("duplicate bootstrap peer ID: %s", remoteID) + continue + } + s.bootstrapPeerIDsMap[localID][remoteID] = struct{}{} + s.bootstrapNetworkWaitGroup.Done() + + //p2pModule := s.p2pModules[serviceURL].(*p2p.P2PModule) + //if len(p2pModule.GetHost().Network().Conns()) == peerCount { + // s.Logf("bootstrap peer connections len: %d", len(p2pModule.GetHost().Network().Conns())) + // connections := p2pModule.GetHost().Network().Conns() + // remoteConnPeers := make([]libp2pPeer.ID, len(connections)) + // for i, conn := range connections { + // remoteConnPeers[i] = conn.RemotePeer() + // } + // s.Logf("p2pModule.GetHost().Network().Conns(): %v", remoteConnPeers) + // s.Logf("s.bootstrapPeerIDsMap[selfID]: %v", s.bootstrapPeerIDsMap[localID]) + // s.bootstrapNetworkWaitGroup.Done() + //} + } +} diff --git a/p2p/integration/peer_discovery.feature b/p2p/integration/peer_discovery.feature new file mode 100644 index 000000000..c968ca3ca --- /dev/null +++ b/p2p/integration/peer_discovery.feature @@ -0,0 +1,61 @@ +Feature: Background Router Peer Discovery + # TODO_THIS_COMMIT: reword `node`; at this level, it represents (a) P2P + # module(s) and/or Router implementation(s). + +# TODO: more scenarios +# Scenario: Client joins network +# Scenario: Inactive nodes are removed from the peerstore + + Scenario Outline: Fully connected network bootstrapping + Given a network containing a "bootstrap" node + When number of "other" nodes join the network + Then the "bootstrap" node should have plus one number of peers in its peerstore + And other nodes should have plus one number of peers in their peerstores + + Examples: + | count | + | 4 | +# | 6 | +# | 12 | +# | 100 | +# | 1024 | + +# Scenario Outline: Fully connected network churning +# Given a "bootstrap" node +# When number of nodes join the network +# And the "bootstrap" node leaves the network +# And number of nodes leave the network +# And number of nodes join the network +# Then the network should contain number of nodes +# And each node should have number of peers in their respective peerstores +# And each node should not have any leavers in their peerstores +# +# Examples: +# | initial | leavers | joiners | final | +# | 4 | 2 | 2 | 4 | +# | 4 | 3 | 4 | 5 | +# | 12 | 6 | 6 | 12 | +## | 100 | 50 | 55 | 105 | +## | 1024 | 1000 | 1200 | 1224 | +# +## TODO_THIS_COMMIT: very similar test will exercise libp2p relaying.. +# Scenario Outline: Discovery across pre-bootstrap network partitions +# Given a "bootstrap_A" node in partition "A" +# And a "bootstrap_B" node in partition "B" +# And a "bootstrap_C" node in partition "C" +# And number of nodes bootstrap in partition "A" +# And number of nodes bootstrap in partition "B" +# And number of nodes bootstrap in partition "C" +# When a "bridge_AB" node joins partitions "A" and "B" +# Then all nodes in partition "A" should discover all nodes in partition "B" +# And all nodes in partition "B" should discover all nodes in partition "A" +# When a "bridge_BC" node joins partitions "B" and "C" +# Then all nodes in partition "A" should discover all nodes in partition "C" +# And all nodes in partition "B" should discover all nodes in partition "C" +# And all nodes in partition "C" should discover all nodes in partition "A" +# And all nodes in partition "C" should discover all nodes in partition "B" +# +# Examples: +# | size_a | size_b | size_c | +# | 2 | 2 | 3 | +# | 10 | 5 | 12 | diff --git a/p2p/integration/peer_discovery_test.go b/p2p/integration/peer_discovery_test.go new file mode 100644 index 000000000..b914622ba --- /dev/null +++ b/p2p/integration/peer_discovery_test.go @@ -0,0 +1,243 @@ +//go:build test + +package integration + +import ( + "github.com/foxcpp/go-mockdns" + mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" + "github.com/pokt-network/pocket/internal/testutil" + "github.com/pokt-network/pocket/internal/testutil/constructors" + generics_testutil "github.com/pokt-network/pocket/internal/testutil/generics" + runtime_testutil "github.com/pokt-network/pocket/internal/testutil/runtime" + "github.com/pokt-network/pocket/p2p" + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/pocket/shared/modules" + mock_modules "github.com/pokt-network/pocket/shared/modules/mocks" + "github.com/regen-network/gocuke" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +const ( + peerDiscoveryFeaturePath = "peer_discovery.feature" + bootstrapNodeLabelPrefix = "bootstrap" + otherNodeLabelPrefix = "other" +) + +func TestPeerDiscoveryIntegration(t *testing.T) { + t.Parallel() + + gocuke.NewRunner(t, new(backgroundPeerDiscoverySuite)).Path(peerDiscoveryFeaturePath).Run() +} + +type backgroundPeerDiscoverySuite struct { + gocuke.TestingT + + dnsSrv *mockdns.Server + busMocks map[string]*mock_modules.MockBus + libp2pNetworkMock mocknet.Mocknet + p2pModules map[string]modules.P2PModule + + // labelServiceURLMap a list of serviceURLs to a set of labels; intended to + // be access and updated via `#getServiceURLsWithLabel()` and + // `#addServiceURLWithLabel()`, respectively. + labelServiceURLMap map[string][]string +} + +func (s *backgroundPeerDiscoverySuite) Before(_ gocuke.Scenario) { + s.labelServiceURLMap = make(map[string][]string) +} + +func (s *backgroundPeerDiscoverySuite) ANetworkContainingANode(nodeLabel string) { + s.dnsSrv = testutil.MinimalDNSMock(s) + + s.busMocks, s.libp2pNetworkMock, s.p2pModules = constructors.NewBusesMocknetAndP2PModules( + s, 1, s.dnsSrv, nil, nil, nil, + ) + + // i.e. "only" serviceURL as this step definition initializes the network + firstServiceURL := generics_testutil.GetKeys(s.busMocks)[0] + s.addServiceURLWithLabel(nodeLabel, firstServiceURL) + + //debugNotifee := testutil.NewDebugNotifee(s) + bootstrapP2PModule := s.p2pModules[firstServiceURL] + //bootstrapP2PModule.(*p2p.P2PModule).GetHost().Network().Notify(debugNotifee) + + err := bootstrapP2PModule.Start() + require.NoError(s, err) + + // TODO_THIS_COMMIT: revisit... + time.Sleep(time.Second * 1) +} + +func (s *backgroundPeerDiscoverySuite) NumberOfNodesJoinTheNetwork(nodeCount int64, nodeLabel string) { + // TECHDEBT: use an iterator instead.. + // plus 1 to account for the bootstrap node serviceURL which we used earlier + serviceURLKeyMap := testutil.SequentialServiceURLPrivKeyMap(s, int(nodeCount+1)) + + // TODO_THIS_COMMIT: clarify how to use `bootstrapNodeLabelPrefix` / what it is for + bootstrapNodeServiceURLs := s.getServiceURLsWithLabel(bootstrapNodeLabelPrefix) + require.Equalf(s, 1, len(bootstrapNodeServiceURLs), "expected exactly one bootstrap node") + + bootstrapNodeServiceURL := bootstrapNodeServiceURLs[0] + + // bootstrap node is the only validator in genesis + genesisState := runtime_testutil.BaseGenesisStateMockFromServiceURLKeyMap( + s, map[string]cryptoPocket.PrivateKey{ + bootstrapNodeServiceURL: serviceURLKeyMap[bootstrapNodeServiceURL], + }, + ) + + // remove the bootstrap node from the map + delete(serviceURLKeyMap, bootstrapNodeServiceURL) + + for serviceURL := range serviceURLKeyMap { + s.addServiceURLWithLabel(otherNodeLabelPrefix, serviceURL) + } + + joinersBusMocks, joinersP2PModules := constructors.NewBusesAndP2PModules( + s, nil, + s.dnsSrv, + genesisState, + s.libp2pNetworkMock, + serviceURLKeyMap, + nil, + ) + + err := s.libp2pNetworkMock.LinkAll() + require.NoError(s, err) + + for _, p2pModule := range joinersP2PModules { + debugNotifee := testutil.NewDebugNotifee(s) + p2pModule.(*p2p.P2PModule).GetHost().Network().Notify(debugNotifee) + + err := p2pModule.Start() + require.NoError(s, err) + } + + s.addBusMocks(joinersBusMocks) + s.addP2PModules(joinersP2PModules) + + // TODO_THIS_COMMIT: wait for bootstrapping + s.Log("STARTING DELAY...") + time.Sleep(time.Second * 5) + s.Log("DELAY OVER") +} + +// TODO_THIS_COMMIT: move below exported methods +func (s *backgroundPeerDiscoverySuite) addServiceURLWithLabel(nodeLabel, serviceURL string) { + serviceURLs := s.labelServiceURLMap[nodeLabel] + if serviceURLs == nil { + serviceURLs = make([]string, 0) + } + s.labelServiceURLMap[nodeLabel] = append(serviceURLs, serviceURL) +} +func (s *backgroundPeerDiscoverySuite) getServiceURLsWithLabel(nodeLabel string) []string { + serviceURLs, ok := s.labelServiceURLMap[nodeLabel] + if !ok { + return nil + } + + if len(serviceURLs) < 1 { + return nil + } + return serviceURLs +} + +func (s *backgroundPeerDiscoverySuite) TheNodeShouldHavePlusOneNumberOfPeersInItsPeerstore(nodeLabel string, peerCountMinus1 int64) { + labelServiceURLs := s.getServiceURLsWithLabel(nodeLabel) + require.NotEmptyf(s, labelServiceURLs, "node label %q not found", nodeLabel) + require.Equalf(s, 1, len(labelServiceURLs), "node label %q has more than one service url", nodeLabel) + + serviceURL := labelServiceURLs[0] + p2pModule, ok := s.p2pModules[serviceURL] + require.Truef(s, ok, "p2p module for service url %q not found", serviceURL) + + host := p2pModule.(*p2p.P2PModule).GetHost() + peers := host.Peerstore().Peers() + + require.Equalf(s, int(peerCountMinus1+1), len(peers), "unexpected number of peers in peerstore: %s", peers) +} + +func (s *backgroundPeerDiscoverySuite) OtherNodesShouldHavePlusOneNumberOfPeersInTheirPeerstores(peerCountMinus1 int64) { + // TODO_THIS_COMMIT: clarify how to use `bootstrapNodeLabelPrefix` / what it is for + otherNodeServiceURLs := s.getServiceURLsWithoutLabel(bootstrapNodeLabelPrefix) + + require.NotEmpty(s, otherNodeServiceURLs, "other nodes not found") + require.Equal(s, int(peerCountMinus1), len(otherNodeServiceURLs)) + + for _, serviceURL := range otherNodeServiceURLs { + p2pModule, ok := s.p2pModules[serviceURL] + require.Truef(s, ok, "p2p module for service url %q not found", serviceURL) + + host := p2pModule.(*p2p.P2PModule).GetHost() + peers := host.Peerstore().Peers() + + require.Equalf(s, int(peerCountMinus1+1), len(peers), "unexpected number of peers in peerstore: %s", peers) + } +} + +//func (s *backgroundPeerDiscoverySuite) EachNodeShouldNotHaveAnyLeaversInTheirPeerstores() { +// panic("PENDING") +//} +// +//func (s *backgroundPeerDiscoverySuite) LeaverNumberOfNodesLeaveTheNetwork() { +// panic("PENDING") +//} + +func (s *backgroundPeerDiscoverySuite) EachNodeShouldHaveNumberOfPeersInTheirRespectivePeerstores(peerCount int64) { + panic("PENDING") +} + +//func (s *backgroundPeerDiscoverySuite) NumberOfNodesBootstrapInPartition(a int64, b string) { +// panic("PENDING") +//} + +func (s *backgroundPeerDiscoverySuite) TheNetworkShouldContainNumberOfNodes(nodeCount int64) { + panic("PENDING") +} + +//func (s *backgroundPeerDiscoverySuite) ANodeInPartition(a string, b string) { +// panic("PENDING") +//} +// +//func (s *backgroundPeerDiscoverySuite) ANodeJoinsPartitionsAnd(a string, b string, c string) { +// panic("PENDING") +//} +// +//func (s *backgroundPeerDiscoverySuite) AllNodesInPartitionShouldDiscoverAllNodesInPartition(a string, b string) { +// panic("PENDING") +//} + +//func (s *backgroundPeerDiscoverySuite) TheNodeLeavesTheNetwork(a string) { +// panic("PENDING") +//} + +func (s *backgroundPeerDiscoverySuite) addBusMocks(busMocks map[string]*mock_modules.MockBus) { + s.Helper() + + for serviceURL, busMock := range busMocks { + require.Nilf(s, s.busMocks[serviceURL], "busMock for serviceURL %s already exists", serviceURL) + s.busMocks[serviceURL] = busMock + } +} + +func (s *backgroundPeerDiscoverySuite) addP2PModules(p2pModules map[string]modules.P2PModule) { + s.Helper() + + for serviceURL, p2pModule := range p2pModules { + require.Nilf(s, s.p2pModules[serviceURL], "p2pModule for serviceURL %s already exists", serviceURL) + s.p2pModules[serviceURL] = p2pModule + } +} + +func (s *backgroundPeerDiscoverySuite) getServiceURLsWithoutLabel(nodeLabel string) (serviceURLs []string) { + for label, urls := range s.labelServiceURLMap { + if label == nodeLabel { + continue + } + serviceURLs = append(serviceURLs, urls...) + } + return serviceURLs +} diff --git a/p2p/module.go b/p2p/module.go index 44095b53b..29f58eb2c 100644 --- a/p2p/module.go +++ b/p2p/module.go @@ -3,10 +3,18 @@ package p2p import ( "errors" "fmt" + "go.uber.org/multierr" + "sync/atomic" + + "github.com/hashicorp/go-multierror" "github.com/libp2p/go-libp2p" libp2pHost "github.com/libp2p/go-libp2p/core/host" "github.com/multiformats/go-multiaddr" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + "github.com/pokt-network/pocket/logger" + "github.com/pokt-network/pocket/p2p/background" "github.com/pokt-network/pocket/p2p/config" "github.com/pokt-network/pocket/p2p/providers" "github.com/pokt-network/pocket/p2p/providers/current_height_provider" @@ -24,8 +32,6 @@ import ( "github.com/pokt-network/pocket/shared/modules" "github.com/pokt-network/pocket/shared/modules/base_modules" "github.com/pokt-network/pocket/telemetry" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" ) var _ modules.P2PModule = &p2pModule{} @@ -33,6 +39,7 @@ var _ modules.P2PModule = &p2pModule{} type p2pModule struct { base_modules.IntegratableModule + started atomic.Bool address cryptoPocket.Address logger *modules.Logger options []modules.ModuleOption @@ -47,8 +54,9 @@ type p2pModule struct { nonceDeduper *mempool.GenericFIFOSet[uint64, uint64] // Assigned during `#Start()`. TLDR; `host` listens on instantiation. - // and `router` depends on `host`. - router typesP2P.Router + // `stakedActorRouter` and `unstakedActorRouter` depends on `host`. + stakedActorRouter typesP2P.Router + unstakedActorRouter typesP2P.Router // host represents a libp2p network node, it encapsulates a libp2p peerstore // & connection manager. `libp2p.New` configures and starts listening // according to options. Assigned via `#Start()` (starts on instantiation). @@ -135,8 +143,12 @@ func (m *p2pModule) GetModuleName() string { } // Start instantiates and assigns `m.host`, unless one already exists, and -// `m.router` (which depends on `m.host` as a required config field). +// `m.stakedActorRouter` (which depends on `m.host` as a required config field). func (m *p2pModule) Start() (err error) { + if !m.started.CompareAndSwap(false, true) { + return fmt.Errorf("p2p module already started") + } + m.GetBus(). GetTelemetryModule(). GetTimeSeriesAgent(). @@ -160,10 +172,16 @@ func (m *p2pModule) Start() (err error) { } } - if err := m.setupRouter(); err != nil { - return fmt.Errorf("setting up router: %w", err) + if err := m.setupRouters(); err != nil { + return fmt.Errorf("setting up routers: %w", err) } + // TODO_THIS_COMMIT: ensure this is addressed + //// Don't handle incoming streams in client debug mode. + //if !m.isClientDebugMode() { + // m.host.SetStreamHandler(protocol.PoktProtocolID, m.handleStream) + //} + m.GetBus(). GetTelemetryModule(). GetTimeSeriesAgent(). @@ -172,14 +190,37 @@ func (m *p2pModule) Start() (err error) { } func (m *p2pModule) Stop() error { - err := m.host.Close() + if !m.started.CompareAndSwap(true, false) { + return fmt.Errorf("p2p module already stopped") + } + + routerCloseErrs := multierr.Append( + m.unstakedActorRouter.Close(), + m.stakedActorRouter.Close(), + ) + + err := multierr.Append( + routerCloseErrs, + m.host.Close(), + ) // Don't reuse closed host, `#Start()` will re-create. m.host = nil + m.stakedActorRouter = nil + m.unstakedActorRouter = nil return err } func (m *p2pModule) Broadcast(msg *anypb.Any) error { + if m.stakedActorRouter == nil { + return fmt.Errorf("staked actor router not started") + } + + if m.unstakedActorRouter == nil { + return fmt.Errorf("unstaked actor router not started") + } + + // TODO: rename `c` c := &messaging.PocketEnvelope{ Content: msg, Nonce: cryptoPocket.GetNonce(), @@ -189,10 +230,15 @@ func (m *p2pModule) Broadcast(msg *anypb.Any) error { return err } - return m.router.Broadcast(data) + stakedBroadcastErr := m.stakedActorRouter.Broadcast(data) + unstakedBroadcastErr := m.unstakedActorRouter.Broadcast(data) + return multierror.Append(err, stakedBroadcastErr, unstakedBroadcastErr).ErrorOrNil() + //return multierror.Append(err, unstakedBroadcastErr).ErrorOrNil() + //return multierror.Append(err, stakedBroadcastErr).ErrorOrNil() } func (m *p2pModule) Send(addr cryptoPocket.Address, msg *anypb.Any) error { + // TODO: rename `c` c := &messaging.PocketEnvelope{ Content: msg, Nonce: cryptoPocket.GetNonce(), @@ -203,7 +249,8 @@ func (m *p2pModule) Send(addr cryptoPocket.Address, msg *anypb.Any) error { return err } - return m.router.Send(data, addr) + // TODO_THIS_COMMIT: send using "appropriate" router... + return m.stakedActorRouter.Send(data, addr) } // TECHDEBT(#348): Define what the node identity is throughout the codebase @@ -283,9 +330,9 @@ func (m *p2pModule) setupNonceDeduper() error { return nil } -// setupRouter instantiates the configured router implementation. -func (m *p2pModule) setupRouter() (err error) { - m.router, err = raintree.NewRainTreeRouter( +// setupRouters instantiates the configured router implementations. +func (m *p2pModule) setupRouters() (err error) { + m.stakedActorRouter, err = raintree.NewRainTreeRouter( m.GetBus(), &config.RainTreeConfig{ Addr: m.address, @@ -295,7 +342,24 @@ func (m *p2pModule) setupRouter() (err error) { Handler: m.handlePocketEnvelope, }, ) - return err + if err != nil { + return fmt.Errorf("staked actor router: %w", err) + } + + m.unstakedActorRouter, err = background.NewBackgroundRouter( + m.GetBus(), + &config.BackgroundConfig{ + Addr: m.address, + CurrentHeightProvider: m.currentHeightProvider, + PeerstoreProvider: m.pstoreProvider, + Host: m.host, + Handler: m.handlePocketEnvelope, + }, + ) + if err != nil { + return fmt.Errorf("unstaked actor router: %w", err) + } + return nil } // setupHost creates a new libp2p host and assignes it to `m.host`. Libp2p host diff --git a/p2p/module_raintree_test.go b/p2p/module_raintree_test.go index 9bd873913..b8a87b696 100644 --- a/p2p/module_raintree_test.go +++ b/p2p/module_raintree_test.go @@ -1,26 +1,31 @@ //go:build test -package p2p +package p2p_test import ( + runtime_testutil "github.com/pokt-network/pocket/internal/testutil/runtime" + telemetry_testutil "github.com/pokt-network/pocket/internal/testutil/telemetry" + "github.com/pokt-network/pocket/p2p" + typesP2P "github.com/pokt-network/pocket/p2p/types" + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" + "github.com/pokt-network/pocket/shared/modules" + mock_modules "github.com/pokt-network/pocket/shared/modules/mocks" + "google.golang.org/protobuf/types/known/anypb" "log" "os" "path/filepath" "regexp" - "sort" "strconv" "strings" "sync" "testing" - libp2pNetwork "github.com/libp2p/go-libp2p/core/network" - mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/anypb" - "github.com/pokt-network/pocket/internal/testutil" - "github.com/pokt-network/pocket/p2p/protocol" - "github.com/pokt-network/pocket/p2p/raintree" + "github.com/pokt-network/pocket/internal/testutil/constructors" + persistence_testutil "github.com/pokt-network/pocket/internal/testutil/persistence" + "github.com/pokt-network/pocket/shared/messaging" + "github.com/regen-network/gocuke" + "github.com/stretchr/testify/require" ) // TODO(#314): Add the tooling and instructions on how to generate unit tests in this file. @@ -40,7 +45,7 @@ func TestMain(m *testing.M) { // ### RainTree Unit Tests ### func TestRainTreeNetworkCompleteOneNodes(t *testing.T) { // val_1 - originatorNode := validatorId(1) + originatorNode := testutil.NewServiceURL(1) expectedCalls := TestNetworkSimulationConfig{ originatorNode: {0, 0}, // val_1, the originator, does 0 network reads or writes } @@ -51,14 +56,13 @@ func TestRainTreeNetworkCompleteTwoNodes(t *testing.T) { // val_1 // └───────┐ // val_2 - originatorNode := validatorId(1) + originatorNode := testutil.NewServiceURL(1) // Per the diagram above, in the case of a 2 node network, the originator node (val_1) does a // single write to another node (val_2), also the // originator node and never performs any reads or writes during a RainTree broadcast. expectedCalls := TestNetworkSimulationConfig{ - // Attempt: I think Validator 1 is sending a message in a 2 (including self) node network - originatorNode: {0, 1}, // val_1 does a single network write (to val_2) - validatorId(2): {1, 0}, // val_2 does a single network read (from val_1) + // Attempt: I think Validator 1 is sending a message in a 2 (including self) node network originatorNode: {0, 1}, // val_1 does a single network write (to val_2) + testutil.NewServiceURL(2): {1, 0}, // val_2 does a single network read (from val_1) } testRainTreeCalls(t, originatorNode, expectedCalls) } @@ -67,11 +71,11 @@ func TestRainTreeNetworkCompleteThreeNodes(t *testing.T) { // val_1 // ┌───────┴────┬─────────┐ // val_2 val_1 val_3 - originatorNode := validatorId(1) + originatorNode := testutil.NewServiceURL(1) expectedCalls := TestNetworkSimulationConfig{ - originatorNode: {0, 2}, // val_1 does two network writes (to val_2 and val_3) - validatorId(2): {1, 0}, // val_2 does a single network read (from val_1) - validatorId(3): {1, 0}, // val_2 does a single network read (from val_3) + originatorNode: {0, 2}, // val_1 does two network writes (to val_2 and val_3) + testutil.NewServiceURL(2): {1, 0}, // val_2 does a single network read (from val_1) + testutil.NewServiceURL(3): {1, 0}, // val_2 does a single network read (from val_3) } testRainTreeCalls(t, originatorNode, expectedCalls) } @@ -83,12 +87,12 @@ func TestRainTreeNetworkCompleteFourNodes(t *testing.T) { // val_2 val_1 val_3 // └───────┐ └───────┐ └───────┐ // val_3 val_2 val_4 - originatorNode := validatorId(1) + originatorNode := testutil.NewServiceURL(1) expectedCalls := TestNetworkSimulationConfig{ - originatorNode: {0, 3}, // val_1 does 3 network writes (two to val_2 and 1 to val_3) - validatorId(2): {2, 1}, // val_2 does 2 network reads (both from val_1) and 1 network write (to val_3) - validatorId(3): {2, 1}, // val_2 does 2 network reads (from val_1 and val_2) and 1 network write (to val_4) - validatorId(4): {1, 0}, // val_2 does 1 network read (from val_3) + originatorNode: {0, 3}, // val_1 does 3 network writes (two to val_2 and 1 to val_3) + testutil.NewServiceURL(2): {2, 1}, // val_2 does 2 network reads (both from val_1) and 1 network write (to val_3) + testutil.NewServiceURL(3): {2, 1}, // val_2 does 2 network reads (from val_1 and val_2) and 1 network write (to val_4) + testutil.NewServiceURL(4): {1, 0}, // val_2 does 1 network read (from val_3) } testRainTreeCalls(t, originatorNode, expectedCalls) } @@ -99,17 +103,17 @@ func TestRainTreeNetworkCompleteNineNodes(t *testing.T) { // val_4 val_1 val_7 // ┌───────┴────┬─────────┐ ┌───────┴────┬─────────┐ ┌───────┴────┬─────────┐ // val_6 val_4 val_8 val_3 val_1 val_5 val_9 val_7 val_2 - originatorNode := validatorId(1) + originatorNode := testutil.NewServiceURL(1) expectedCalls := TestNetworkSimulationConfig{ - originatorNode: {0, 4}, - validatorId(2): {1, 0}, - validatorId(3): {1, 0}, - validatorId(4): {1, 2}, - validatorId(5): {1, 0}, - validatorId(6): {1, 0}, - validatorId(7): {1, 2}, - validatorId(8): {1, 0}, - validatorId(9): {1, 0}, + originatorNode: {0, 4}, + testutil.NewServiceURL(2): {1, 0}, + testutil.NewServiceURL(3): {1, 0}, + testutil.NewServiceURL(4): {1, 2}, + testutil.NewServiceURL(5): {1, 0}, + testutil.NewServiceURL(6): {1, 0}, + testutil.NewServiceURL(7): {1, 2}, + testutil.NewServiceURL(8): {1, 0}, + testutil.NewServiceURL(9): {1, 0}, } testRainTreeCalls(t, originatorNode, expectedCalls) } @@ -123,20 +127,20 @@ func TestRainTreeNetworkCompleteNineNodes(t *testing.T) { // val_8 val_7 val_10 val_6 val_5 val_8 val_11 val_10 val_5 val_4 val_3 val_6 val_2 val_1 val_4 val_7 val_6 val_1 val_12 val_11 val_2 val_10 val_9 val_12 val_3 val_2 val_9 func TestRainTreeCompleteTwelveNodes(t *testing.T) { - originatorNode := validatorId(1) + originatorNode := testutil.NewServiceURL(1) expectedCalls := TestNetworkSimulationConfig{ - originatorNode: {1, 6}, - validatorId(2): {3, 2}, - validatorId(3): {2, 2}, - validatorId(4): {2, 0}, - validatorId(5): {2, 4}, - validatorId(6): {3, 2}, - validatorId(7): {2, 2}, - validatorId(8): {2, 0}, - validatorId(9): {2, 4}, - validatorId(10): {3, 2}, - validatorId(11): {2, 2}, - validatorId(12): {2, 0}, + originatorNode: {1, 6}, + testutil.NewServiceURL(2): {3, 2}, + testutil.NewServiceURL(3): {2, 2}, + testutil.NewServiceURL(4): {2, 0}, + testutil.NewServiceURL(5): {2, 4}, + testutil.NewServiceURL(6): {3, 2}, + testutil.NewServiceURL(7): {2, 2}, + testutil.NewServiceURL(8): {2, 0}, + testutil.NewServiceURL(9): {2, 4}, + testutil.NewServiceURL(10): {3, 2}, + testutil.NewServiceURL(11): {2, 2}, + testutil.NewServiceURL(12): {2, 0}, } testRainTreeCalls(t, originatorNode, expectedCalls) } @@ -149,26 +153,26 @@ func TestRainTreeNetworkCompleteEighteenNodes(t *testing.T) { // val_11 val_7 val_15 val_5 val_1 val_9 val_17 val_13 val_3 // ┌────────┴─────┬───────────┐ ┌───────┴────┬──────────┐ ┌────────┴─────┬──────────┐ ┌───────┴────┬──────────┐ ┌───────┴────┬─────────┐ ┌────────┴────┬─────────┐ ┌───────┴─────┬──────────┐ ┌────────┴─────┬───────────┐ ┌───────┴────┬──────────┐ // val_13 val_11 val_16 val_9 val_7 val_12 val_17 val_15 val_8 val_7 val_5 val_10 val_3 val_1 val_6 val_11 val_9 val_2 val_1 val_17 val_4 val_15 val_13 val_18 val_5 val_3 val_14 - originatorNode := validatorId(1) + originatorNode := testutil.NewServiceURL(1) expectedCalls := TestNetworkSimulationConfig{ - originatorNode: {1, 6}, - validatorId(2): {1, 0}, - validatorId(3): {2, 2}, - validatorId(4): {1, 0}, - validatorId(5): {2, 2}, - validatorId(6): {1, 0}, - validatorId(7): {2, 4}, - validatorId(8): {1, 0}, - validatorId(9): {2, 2}, - validatorId(10): {1, 0}, - validatorId(11): {2, 2}, - validatorId(12): {1, 0}, - validatorId(13): {2, 4}, - validatorId(14): {1, 0}, - validatorId(15): {2, 2}, - validatorId(16): {1, 0}, - validatorId(17): {2, 2}, - validatorId(18): {1, 0}, + originatorNode: {1, 6}, + testutil.NewServiceURL(2): {1, 0}, + testutil.NewServiceURL(3): {2, 2}, + testutil.NewServiceURL(4): {1, 0}, + testutil.NewServiceURL(5): {2, 2}, + testutil.NewServiceURL(6): {1, 0}, + testutil.NewServiceURL(7): {2, 4}, + testutil.NewServiceURL(8): {1, 0}, + testutil.NewServiceURL(9): {2, 2}, + testutil.NewServiceURL(10): {1, 0}, + testutil.NewServiceURL(11): {2, 2}, + testutil.NewServiceURL(12): {1, 0}, + testutil.NewServiceURL(13): {2, 4}, + testutil.NewServiceURL(14): {1, 0}, + testutil.NewServiceURL(15): {2, 2}, + testutil.NewServiceURL(16): {1, 0}, + testutil.NewServiceURL(17): {2, 2}, + testutil.NewServiceURL(18): {1, 0}, } testRainTreeCalls(t, originatorNode, expectedCalls) } @@ -181,35 +185,35 @@ func TestRainTreeNetworkCompleteTwentySevenNodes(t *testing.T) { // val_16 val_10 val_22 val_7 val_1 val_13 val_25 val_19 val_4 // ┌────────┴─────┬───────────┐ ┌────────┴─────┬───────────┐ ┌────────┴─────┬───────────┐ ┌────────┴────┬──────────┐ ┌───────┴────┬─────────┐ ┌────────┴─────┬──────────┐ ┌───────┴─────┬──────────┐ ┌────────┴─────┬───────────┐ ┌───────┴────┬──────────┐ // val_20 val_16 val_24 val_14 val_10 val_18 val_26 val_22 val_12 val_11 val_7 val_15 val_5 val_1 val_9 val_17 val_13 val_3 val_2 val_25 val_6 val_23 val_19 val_27 val_8 val_4 val_21 - originatorNode := validatorId(1) + originatorNode := testutil.NewServiceURL(1) expectedCalls := TestNetworkSimulationConfig{ - originatorNode: {0, 6}, - validatorId(2): {1, 0}, - validatorId(3): {1, 0}, - validatorId(4): {1, 2}, - validatorId(5): {1, 0}, - validatorId(6): {1, 0}, - validatorId(7): {1, 2}, - validatorId(8): {1, 0}, - validatorId(9): {1, 0}, - validatorId(10): {1, 4}, - validatorId(11): {1, 0}, - validatorId(12): {1, 0}, - validatorId(13): {1, 2}, - validatorId(14): {1, 0}, - validatorId(15): {1, 0}, - validatorId(16): {1, 2}, - validatorId(17): {1, 0}, - validatorId(18): {1, 0}, - validatorId(19): {1, 4}, - validatorId(20): {1, 0}, - validatorId(21): {1, 0}, - validatorId(22): {1, 2}, - validatorId(23): {1, 0}, - validatorId(24): {1, 0}, - validatorId(25): {1, 2}, - validatorId(26): {1, 0}, - validatorId(27): {1, 0}, + originatorNode: {0, 6}, + testutil.NewServiceURL(2): {1, 0}, + testutil.NewServiceURL(3): {1, 0}, + testutil.NewServiceURL(4): {1, 2}, + testutil.NewServiceURL(5): {1, 0}, + testutil.NewServiceURL(6): {1, 0}, + testutil.NewServiceURL(7): {1, 2}, + testutil.NewServiceURL(8): {1, 0}, + testutil.NewServiceURL(9): {1, 0}, + testutil.NewServiceURL(10): {1, 4}, + testutil.NewServiceURL(11): {1, 0}, + testutil.NewServiceURL(12): {1, 0}, + testutil.NewServiceURL(13): {1, 2}, + testutil.NewServiceURL(14): {1, 0}, + testutil.NewServiceURL(15): {1, 0}, + testutil.NewServiceURL(16): {1, 2}, + testutil.NewServiceURL(17): {1, 0}, + testutil.NewServiceURL(18): {1, 0}, + testutil.NewServiceURL(19): {1, 4}, + testutil.NewServiceURL(20): {1, 0}, + testutil.NewServiceURL(21): {1, 0}, + testutil.NewServiceURL(22): {1, 2}, + testutil.NewServiceURL(23): {1, 0}, + testutil.NewServiceURL(24): {1, 0}, + testutil.NewServiceURL(25): {1, 2}, + testutil.NewServiceURL(26): {1, 0}, + testutil.NewServiceURL(27): {1, 0}, } testRainTreeCalls(t, originatorNode, expectedCalls) } @@ -220,64 +224,169 @@ func TestRainTreeNetworkCompleteTwentySevenNodes(t *testing.T) { // 1. It creates and configures a "real" P2P module where all the other components of the node are mocked. // 2. It then triggers a single message and waits for all of the expected messages transmission to complete before announcing failure. func testRainTreeCalls(t *testing.T, origNode string, networkSimulationConfig TestNetworkSimulationConfig) { + dnsSrv := testutil.MinimalDNSMock(t) + // Configure & prepare test module numValidators := len(networkSimulationConfig) - runtimeConfigs := createMockRuntimeMgrs(t, numValidators) - genesisMock := runtimeConfigs[0].GetGenesis() - busMocks := createMockBuses(t, runtimeConfigs) + //runtimeConfigs := createMockRuntimeMgrs(t, numValidators) + //genesisState := runtimeConfigs[0].GetGenesis() - valIds := make([]string, 0, numValidators) - for valId := range networkSimulationConfig { - valIds = append(valIds, valId) + privKeys := testutil.LoadLocalnetPrivateKeys(t, numValidators) + pubKeys := make([]cryptoPocket.PublicKey, len(privKeys)) + for i, privKey := range privKeys { + pubKeys[i] = privKey.PublicKey() } - sort.Slice(valIds, func(i, j int) bool { - iId := extractNumericId(valIds[i]) - jId := extractNumericId(valIds[j]) - return iId < jId - }) + genesisState := runtime_testutil.GenesisWithSequentialServiceURLs(t, pubKeys) + + var ( + wg sync.WaitGroup + busMocks map[string]*mock_modules.MockBus + p2pModules map[string]modules.P2PModule + ) + //busMocks := createMockBuses(t, runtimeConfigs, &wg) + busEventHandlerFactory := func(t gocuke.TestingT, busMock *mock_modules.MockBus) testutil.BusEventHandler { + return func(data *messaging.PocketEnvelope) { + //p2pCfg := busMock.GetRuntimeMgr().GetConfig().P2P + // + //// `p2pModule#handleNetworkData()` calls `modules.Bus#PublishEventToBus()` + //// assumes that P2P module is the only bus event producer running during + //// the test. + //t.Logf("[valId: %s:%d] Read", p2pCfg.Hostname, p2pCfg.Port) + //wg.Done() + } + } - testutil.PrepareDNSMockFromServiceURLs(t, valIds) + busMocks, _, p2pModules = constructors.NewBusesMocknetAndP2PModules( + t, numValidators, + dnsSrv, + genesisState, + busEventHandlerFactory, + nil, + ) + + //for _, busMock := range busMocks { + // telemetryMock := busMock.GetTelemetryModule().(*mock_modules.MockTelemetryModule) + // telemetryMock.GetEventMetricsAgent().(*mock_modules.MockEventMetricsAgent).EXPECT().EmitEvent( + // gomock.Any(), + // gomock.Any(), + // gomock.Any(), + // gomock.Any(), + // ).AnyTimes() + //} + + //serviceURLs := make([]string, 0, numValidators) + //for valId := range networkSimulationConfig { + // serviceURLs = append(serviceURLs, valId) + //} + // + //// TODO_THIS_COMMIT: need this? + //// sort `serviceURLs` in ascending order + //sort.Slice(serviceURLs, func(i, j int) bool { + // iId := extractNumericId(serviceURLs[i]) + // jId := extractNumericId(serviceURLs[j]) + // return iId < jId + //}) // Create connection and bus mocks along with a shared WaitGroup to track the number of expected // reads and writes throughout the mocked local network - var wg sync.WaitGroup - for i, valId := range valIds { - expectedCall := networkSimulationConfig[valId] + for serviceURL, busMock := range busMocks { + expectedCall := networkSimulationConfig[serviceURL] expectedReads := expectedCall.numNetworkReads expectedWrites := expectedCall.numNetworkWrites - log.Printf("[valId: %s] expected reads: %d\n", valId, expectedReads) - log.Printf("[valId: %s] expected writes: %d\n", valId, expectedWrites) + log.Printf("[serviceURL: %s] expected reads: %d\n", serviceURL, expectedReads) + log.Printf("[serviceURL: %s] expected writes: %d\n", serviceURL, expectedWrites) wg.Add(expectedReads) wg.Add(expectedWrites) - persistenceMock := preparePersistenceMock(t, busMocks[i], genesisMock) - consensusMock := prepareConsensusMock(t, busMocks[i]) - telemetryMock := prepareTelemetryMock(t, busMocks[i], valId, &wg, expectedWrites) + // TODO_THIS_COMMIT: + if serviceURL == origNode { + t.Log("SELF SEND!!!! 1") + t.Log("SELF SEND!!!! 2") + t.Log("SELF SEND!!!! 3") + //... + } - prepareBusMock(busMocks[i], persistenceMock, consensusMock, telemetryMock) + // TODO_THIS_COMMIT: MOVE + // -- option 2a + //telemetryEventHandler := func(namespace, eventName string, labels ...any) { + // t.Log("telemetry event received") + // wg.Done() + //} + + // TODO_THIS_COMMIT: refactor + // -- option 1 + //eventMetricsAgentMock := telemetry_testutil.PrepareEventMetricsAgentMock(t, serviceURL, &wg, expectedWrites) + //busMock.GetTelemetryModule().(*mock_modules.MockTelemetryModule).EXPECT(). + // GetEventMetricsAgent().Return(eventMetricsAgentMock).AnyTimes() + + // option 1.5 + eventMetricsAgentMock := busMock. + GetTelemetryModule(). + GetEventMetricsAgent().(*mock_modules.MockEventMetricsAgent) + + //telemetry_testutil.WithEventMetricsHandler( + telemetry_testutil.WhyEventMetricsAgentMock( + t, eventMetricsAgentMock, + serviceURL, &wg, + //telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL, + //telemetryEventHandler, + expectedWrites, + ) + // -- + + _ = persistence_testutil.BasePersistenceMock(t, busMock, genesisState) + //telemetryMock := prepareTelemetryMock(t, busMock, serviceURL, &wg, expectedWrites) + + //telemetryMock := telemetry_testutil.WithTimeSeriesAgent( + // t, telemetry_testutil.MinimalTelemetryMock(t, busMock)) + // + //eventMetricsAgentMock := telemetry_testutil.EventMetricsAgentMockWithHandler( + // t, telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL, + // telemetryEventHandler, + // expectedWrites, + //) + //telemetryMock.EXPECT().GetEventMetricsAgent().Return(eventMetricsAgentMock).AnyTimes() + + // -- option 2b + //eventMetricsAgentMock := busMock. + // GetTelemetryModule(). + // GetEventMetricsAgent().(*mock_modules.MockEventMetricsAgent) + // + ////telemetry_testutil.WithEventMetricsHandler( + //telemetry_testutil.WhyEventMetricsAgentMock( + // t, eventMetricsAgentMock, + // //telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL, + // telemetryEventHandler, + // expectedWrites, + //) + // -- + + //telemetryMock := telemetry_testutil.BaseTelemetryMock(t, busMock) + //telemetryMock.GetEventMetricsAgent().(*mock_modules.MockEventMetricsAgent).EXPECT().EmitEvent( + // gomock.Any(), + // gomock.Any(), + // gomock.Eq(telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL), + // gomock.Any(), + // //).Do(telemetryEventHandler).Times(expectedWrites) + //).Do(telemetryEventHandler).AnyTimes() } - libp2pMockNet := mocknet.New() - defer func() { - err := libp2pMockNet.Close() - require.NoError(t, err) - }() - - // Inject the connection and bus mocks into the P2P modules - p2pModules := createP2PModules(t, busMocks, libp2pMockNet) - + // Start all p2p modules for serviceURL, p2pMod := range p2pModules { err := p2pMod.Start() require.NoError(t, err) + // TODO_THIS_COMMIT: consider using BusEventHandler instead... sURL := strings.Clone(serviceURL) - mod := *p2pMod - p2pMod.host.SetStreamHandler(protocol.PoktProtocolID, func(stream libp2pNetwork.Stream) { - log.Printf("[valID: %s] Read\n", sURL) - (&mod).router.(*raintree.RainTreeRouter).HandleStream(stream) - wg.Done() + mod := *(p2pMod.(*p2p.P2PModule)) + mod.GetRainTreeRouter().HandlerProxy(t, func(origHandler typesP2P.RouterHandler) typesP2P.RouterHandler { + return func(data []byte) error { + log.Printf("[valID: %s] Read\n", sURL) + wg.Done() + return nil + } }) } diff --git a/p2p/module_test.go b/p2p/module_test.go index 79cd17066..d39c9f097 100644 --- a/p2p/module_test.go +++ b/p2p/module_test.go @@ -9,6 +9,11 @@ import ( libp2pCrypto "github.com/libp2p/go-libp2p/core/crypto" libp2pHost "github.com/libp2p/go-libp2p/core/host" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/pocket/internal/testutil" + "github.com/pokt-network/pocket/internal/testutil/persistence" + "github.com/pokt-network/pocket/internal/testutil/runtime" typesP2P "github.com/pokt-network/pocket/p2p/types" "github.com/pokt-network/pocket/p2p/utils" "github.com/pokt-network/pocket/runtime/configs" @@ -16,7 +21,6 @@ import ( cryptoPocket "github.com/pokt-network/pocket/shared/crypto" "github.com/pokt-network/pocket/shared/modules" mockModules "github.com/pokt-network/pocket/shared/modules/mocks" - "github.com/stretchr/testify/require" ) // TECHDEBT(#609): move & de-dup. @@ -107,14 +111,23 @@ func Test_Create_configureBootstrapNodes(t *testing.T) { }, } + keys := testutil.LoadLocalnetPrivateKeys(t, 10) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) mockRuntimeMgr := mockModules.NewMockRuntimeMgr(ctrl) - mockBus := createMockBus(t, mockRuntimeMgr) + mockBus := testutil.BaseBusMock(t, mockRuntimeMgr) + + // TODO_THIS_COMMIT: refactor + pubKeys := make([]cryptoPocket.PublicKey, len(keys)) + for i, privKey := range keys { + pubKeys[i] = privKey.PublicKey() + } - genesisStateMock := createMockGenesisState(keys) - persistenceMock := preparePersistenceMock(t, mockBus, genesisStateMock) + serviceURLs := testutil.SequentialServiceURLs(t, len(pubKeys)) + genesisStateMock := runtime_testutil.BaseGenesisStateMock(t, pubKeys, serviceURLs) + persistenceMock := persistence_testutil.BasePersistenceMock(t, mockBus, genesisStateMock) mockBus.EXPECT().GetPersistenceModule().Return(persistenceMock).AnyTimes() mockConsensusModule := mockModules.NewMockConsensusModule(ctrl) @@ -155,10 +168,10 @@ func TestP2pModule_WithHostOption_Restart(t *testing.T) { privKey := cryptoPocket.GetPrivKeySeed(1) mockRuntimeMgr := mockModules.NewMockRuntimeMgr(ctrl) - mockBus := createMockBus(t, mockRuntimeMgr) + mockBus := testutil.BaseBusMock(t, mockRuntimeMgr) - genesisStateMock := createMockGenesisState(nil) - persistenceMock := preparePersistenceMock(t, mockBus, genesisStateMock) + genesisStateMock := runtime_testutil.BaseGenesisStateMock(t, nil, nil) + persistenceMock := persistence_testutil.BasePersistenceMock(t, mockBus, genesisStateMock) mockBus.EXPECT().GetPersistenceModule().Return(persistenceMock).AnyTimes() consensusModuleMock := mockModules.NewMockConsensusModule(ctrl) diff --git a/p2p/raintree/peers_manager_test.go b/p2p/raintree/peers_manager_test.go index 7fa01a3d3..1c199a5f4 100644 --- a/p2p/raintree/peers_manager_test.go +++ b/p2p/raintree/peers_manager_test.go @@ -101,6 +101,7 @@ func TestRainTree_Peerstore_HandleUpdate(t *testing.T) { Addr: pubKey.Address(), PeerstoreProvider: pstoreProviderMock, CurrentHeightProvider: currentHeightProviderMock, + Handler: noopHandler, } router, err := NewRainTreeRouter(mockBus, rtCfg) @@ -293,6 +294,7 @@ func testRainTreeMessageTargets(t *testing.T, expectedMsgProp *ExpectedRainTreeM Addr: []byte{expectedMsgProp.orig}, PeerstoreProvider: pstoreProviderMock, CurrentHeightProvider: currentHeightProviderMock, + Handler: noopHandler, } router, err := NewRainTreeRouter(busMock, rtCfg) @@ -356,5 +358,10 @@ func mockAlphabetValidatorServiceURLsDNS(t *testing.T) (done func()) { } } - return testutil.PrepareDNSMock(zones) + _ = testutil.BaseDNSMock(t, zones) + return done +} + +func noopHandler(data []byte) error { + return nil } diff --git a/p2p/raintree/router.go b/p2p/raintree/router.go index 707f0a0a9..65a4da710 100644 --- a/p2p/raintree/router.go +++ b/p2p/raintree/router.go @@ -63,6 +63,7 @@ func NewRainTreeRouter(bus modules.Bus, cfg *config.RainTreeConfig) (typesP2P.Ro } func (*rainTreeRouter) Create(bus modules.Bus, cfg *config.RainTreeConfig) (typesP2P.Router, error) { + // TODO_THIS_COMMIT: rename logger to support multiple routers routerLogger := logger.Global.CreateLoggerForModule("router") routerLogger.Info().Msg("Initializing rainTreeRouter") @@ -169,7 +170,6 @@ func (rtr *rainTreeRouter) sendInternal(data []byte, address cryptoPocket.Addres utils.LogOutgoingMsg(rtr.logger, hostname, peer) if err := utils.Libp2pSendToPeer(rtr.host, data, peer); err != nil { - rtr.logger.Debug().Err(err).Msg("from libp2pSendInternal") return err } @@ -211,11 +211,13 @@ func (rtr *rainTreeRouter) handleRainTreeMsg(data []byte) ([]byte, error) { return nil, err } + // TECHDEBT(#763): refactor as "pre-propagation validation" networkMessage := messaging.PocketEnvelope{} if err := proto.Unmarshal(rainTreeMsg.Data, &networkMessage); err != nil { rtr.logger.Error().Err(err).Msg("Error decoding network message") return nil, err } + // -- // Continue RainTree propagation if rainTreeMsg.Level > 0 { @@ -270,6 +272,10 @@ func (rtr *rainTreeRouter) Size() int { return rtr.peersManager.GetPeerstore().Size() } +func (rtr *rainTreeRouter) Close() error { + return nil +} + // handleStream ensures the peerstore contains the remote peer and then reads // the incoming stream in a new go routine. func (rtr *rainTreeRouter) handleStream(stream libp2pNetwork.Stream) { @@ -360,6 +366,20 @@ func (rtr *rainTreeRouter) setupDependencies() error { return err } + // TODO: remove me + //debugFieldsMap := map[string]any{"selfPeerID": rtr.host.ID()} + //for _, peer := range pstore.GetPeerList() { + // peerInfo, err := utils.Libp2pAddrInfoFromPeer(peer) + // if err != nil { + // panic(err) + // } + // + // debugFieldsMap[peerInfo.ID.String()] = peer.GetAddress().String() + //} + // + //rtr.logger.Debug().Fields(debugFieldsMap).Msg("peer IDs to pocket addresses") + // END TODO + if err := utils.PopulateLibp2pHost(rtr.host, pstore); err != nil { return err } diff --git a/p2p/raintree/router_test.go b/p2p/raintree/router_test.go index 1ef093a20..2865a584b 100644 --- a/p2p/raintree/router_test.go +++ b/p2p/raintree/router_test.go @@ -57,6 +57,7 @@ func TestRainTreeRouter_AddPeer(t *testing.T) { Addr: selfAddr, PeerstoreProvider: peerstoreProviderMock, CurrentHeightProvider: currentHeightProviderMock, + Handler: noopHandler, } router, err := NewRainTreeRouter(busMock, rtCfg) @@ -119,6 +120,7 @@ func TestRainTreeRouter_RemovePeer(t *testing.T) { Addr: selfAddr, PeerstoreProvider: peerstoreProviderMock, CurrentHeightProvider: currentHeightProviderMock, + Handler: noopHandler, } router, err := NewRainTreeRouter(busMock, rtCfg) diff --git a/p2p/raintree/testutil.go b/p2p/raintree/testutil.go index ebcb464b5..ef19b19cc 100644 --- a/p2p/raintree/testutil.go +++ b/p2p/raintree/testutil.go @@ -2,12 +2,30 @@ package raintree -import libp2pNetwork "github.com/libp2p/go-libp2p/core/network" +import ( + libp2pNetwork "github.com/libp2p/go-libp2p/core/network" + generics_testutil "github.com/pokt-network/pocket/internal/testutil/generics" + typesP2P "github.com/pokt-network/pocket/p2p/types" + "github.com/regen-network/gocuke" +) // RainTreeRouter exports `rainTreeRouter` for testing purposes. type RainTreeRouter = rainTreeRouter +// TOOD_THIS_COMMIT: move & dedup +type routerHandlerProxyFactory = generics_testutil.ProxyFactory[typesP2P.RouterHandler] + // HandleStream exports `rainTreeRouter#handleStream` for testing purposes. func (rtr *rainTreeRouter) HandleStream(stream libp2pNetwork.Stream) { rtr.handleStream(stream) } +func (rtr *rainTreeRouter) HandlerProxy( + t gocuke.TestingT, + handlerProxyFactory routerHandlerProxyFactory, +) { + t.Helper() + + // pass original handler to proxy factory & replace it with the proxy + origHandler := rtr.handler + rtr.handler = handlerProxyFactory(origHandler) +} diff --git a/p2p/testutil.go b/p2p/testutil.go new file mode 100644 index 000000000..7cf01cf83 --- /dev/null +++ b/p2p/testutil.go @@ -0,0 +1,27 @@ +//go:build test + +package p2p + +import ( + libp2pHost "github.com/libp2p/go-libp2p/core/host" + "github.com/pokt-network/pocket/p2p/background" + + "github.com/pokt-network/pocket/p2p/raintree" +) + +// P2PModule exports the `p2pModule` type for use in tests +type P2PModule = p2pModule + +func (m *p2pModule) GetHost() libp2pHost.Host { + return m.host +} + +// GetRainTreeRouter returns the `RainTreeRouter` for use in integration tests +func (m *p2pModule) GetRainTreeRouter() *raintree.RainTreeRouter { + return m.stakedActorRouter.(*raintree.RainTreeRouter) +} + +// GetBackgroundRouter returns the `BackgroundRouter` for use in integration tests +func (m *p2pModule) GetBackgroundRouter() *background.BackgroundRouter { + return m.unstakedActorRouter.(*background.BackgroundRouter) +} diff --git a/p2p/transport_encryption_test.go b/p2p/transport_encryption_test.go index d95cb6496..86e3c0f1d 100644 --- a/p2p/transport_encryption_test.go +++ b/p2p/transport_encryption_test.go @@ -1,8 +1,13 @@ -package p2p +package p2p_test import ( "context" "fmt" + "github.com/pokt-network/pocket/internal/testutil" + "github.com/pokt-network/pocket/internal/testutil/persistence" + "github.com/pokt-network/pocket/internal/testutil/runtime" + "github.com/pokt-network/pocket/internal/testutil/telemetry" + "github.com/pokt-network/pocket/p2p" "testing" "time" @@ -11,7 +16,6 @@ import ( "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" - "github.com/pokt-network/pocket/internal/testutil" "github.com/pokt-network/pocket/p2p/protocol" typesP2P "github.com/pokt-network/pocket/p2p/types" "github.com/pokt-network/pocket/p2p/utils" @@ -19,7 +23,6 @@ import ( "github.com/pokt-network/pocket/runtime/configs/types" "github.com/pokt-network/pocket/runtime/defaults" cryptoPocket "github.com/pokt-network/pocket/shared/crypto" - "github.com/pokt-network/pocket/shared/modules" mockModules "github.com/pokt-network/pocket/shared/modules/mocks" ) @@ -45,35 +48,28 @@ func TestP2pModule_Insecure_Error(t *testing.T) { }, }).AnyTimes() - timeSeriesAgentMock := prepareNoopTimeSeriesAgentMock(t) - eventMetricsAgentMock := mockModules.NewMockEventMetricsAgent(ctrl) - eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() - - telemetryMock := mockModules.NewMockTelemetryModule(ctrl) - telemetryMock.EXPECT().GetTimeSeriesAgent().Return(timeSeriesAgentMock).AnyTimes() - telemetryMock.EXPECT().GetEventMetricsAgent().Return(eventMetricsAgentMock).AnyTimes() - telemetryMock.EXPECT().GetModuleName().Return(modules.TelemetryModuleName).AnyTimes() - - busMock := createMockBus(t, runtimeMgrMock) + busMock := testutil.BaseBusMock(t, runtimeMgrMock) busMock.EXPECT().GetConsensusModule().Return(mockConsensusModule).AnyTimes() - busMock.EXPECT().GetRuntimeMgr().Return(runtimeMgrMock).AnyTimes() - busMock.EXPECT().GetTelemetryModule().Return(telemetryMock).AnyTimes() - genesisStateMock := createMockGenesisState(keys[:1]) - persistenceMock := preparePersistenceMock(t, busMock, genesisStateMock) - busMock.EXPECT().GetPersistenceModule().Return(persistenceMock).AnyTimes() + telemetryMock := telemetry_testutil.BaseTelemetryMock(t, busMock) + busMock.EXPECT().GetTelemetryModule().Return(telemetryMock).AnyTimes() - telemetryMock.EXPECT().GetBus().Return(busMock).AnyTimes() - telemetryMock.EXPECT().SetBus(busMock).AnyTimes() + keys := testutil.LoadLocalnetPrivateKeys(t, 1) - serviceURLs := make([]string, len(genesisStateMock.Validators)) - for i, actor := range genesisStateMock.Validators { - serviceURLs[i] = actor.ServiceUrl + // TODO_THIS_COMMIT: refactor + pubKeys := make([]cryptoPocket.PublicKey, len(keys)) + for i, privKey := range keys { + pubKeys[i] = privKey.PublicKey() } - dnsDone := testutil.PrepareDNSMockFromServiceURLs(t, serviceURLs) - t.Cleanup(dnsDone) + serviceURLs := testutil.SequentialServiceURLs(t, len(pubKeys)) + genesisStateMock := runtime_testutil.BaseGenesisStateMock(t, pubKeys, serviceURLs) + persistenceMock := persistence_testutil.BasePersistenceMock(t, busMock, genesisStateMock) + busMock.EXPECT().GetPersistenceModule().Return(persistenceMock).AnyTimes() + + // mock DNS for service URL hostnames + _ = testutil.DNSMockFromServiceURLs(t, serviceURLs) - p2pMod, err := Create(busMock) + p2pMod, err := p2p.Create(busMock) require.NoError(t, err) err = p2pMod.Start() diff --git a/p2p/types/proto/background.proto b/p2p/types/proto/background.proto new file mode 100644 index 000000000..d5046ff71 --- /dev/null +++ b/p2p/types/proto/background.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package background; + +option go_package = "github.com/pokt-network/pocket/p2p/types"; + +message BackgroundMessage { + bytes data = 1; +} diff --git a/p2p/raintree/types/proto/raintree.proto b/p2p/types/proto/raintree.proto similarity index 100% rename from p2p/raintree/types/proto/raintree.proto rename to p2p/types/proto/raintree.proto diff --git a/p2p/types/router.go b/p2p/types/router.go index f9c7f2d74..915755c18 100644 --- a/p2p/types/router.go +++ b/p2p/types/router.go @@ -14,6 +14,7 @@ type Router interface { Broadcast(data []byte) error Send(data []byte, address cryptoPocket.Address) error + Close() error // Address book helpers // TECHDEBT: simplify - remove `GetPeerstore` diff --git a/p2p/utils/host.go b/p2p/utils/host.go index e9c6e130b..b016bcc47 100644 --- a/p2p/utils/host.go +++ b/p2p/utils/host.go @@ -94,6 +94,17 @@ func Libp2pSendToPeer(host libp2pHost.Host, data []byte, peer typesP2P.Peer) err logger.Global.Debug().Err(err).Msg("logging resource scope stats") } + // TODO: remove me! + //pstore := host.Peerstore() + //for _, peerID := range pstore.Peers() { + // addr := pstore.Addrs(peerID)[0] + // logger.Global.Debug(). + // Str("peerID", peerID.String()). + // Str("addr", addr.String()). + // Msg("peerstore") + //} + // -- end TODO + stream, err := host.NewStream(ctx, peerInfo.ID, protocol.PoktProtocolID) if err != nil { return fmt.Errorf("opening stream: %w", err) diff --git a/p2p/utils_test.go b/p2p/utils_test.go index 66466acb0..57f5729d2 100644 --- a/p2p/utils_test.go +++ b/p2p/utils_test.go @@ -1,42 +1,19 @@ -package p2p +//go:build test + +package p2p_test import ( - "fmt" - "log" - "net" "sort" - "strconv" "sync" "testing" "time" - "github.com/golang/mock/gomock" - libp2pCrypto "github.com/libp2p/go-libp2p/core/crypto" - libp2pPeer "github.com/libp2p/go-libp2p/core/peer" - mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" - "github.com/pokt-network/pocket/p2p/providers/current_height_provider" - "github.com/pokt-network/pocket/p2p/providers/peerstore_provider" - typesP2P "github.com/pokt-network/pocket/p2p/types" - "github.com/pokt-network/pocket/p2p/utils" - "github.com/pokt-network/pocket/runtime" - "github.com/pokt-network/pocket/runtime/configs" - "github.com/pokt-network/pocket/runtime/configs/types" - "github.com/pokt-network/pocket/runtime/defaults" - "github.com/pokt-network/pocket/runtime/genesis" - "github.com/pokt-network/pocket/runtime/test_artifacts" - coreTypes "github.com/pokt-network/pocket/shared/core/types" cryptoPocket "github.com/pokt-network/pocket/shared/crypto" - "github.com/pokt-network/pocket/shared/modules" - mockModules "github.com/pokt-network/pocket/shared/modules/mocks" - "github.com/pokt-network/pocket/telemetry" - "github.com/stretchr/testify/require" ) // ~~~~~~ RainTree Unit Test Configurations ~~~~~~ const ( - serviceURLFormat = "node%d.consensus:42069" - eventsChannelSize = 10000 // Since we simulate up to a 27 node network, we will pre-generate a n >= 27 number of keys to avoid generation // every time. The genesis config seed start is set for deterministic key generation and 42 was chosen arbitrarily. genesisConfigSeedStart = 42 @@ -75,12 +52,6 @@ type TestNetworkSimulationConfig map[string]struct { // node IDs the specific read or write is coming from or going to. } -// CLEANUP: This could (should?) be a codebase-wide shared test helper -// TECHDEBT: rename `validatorId()` to `serviceURL()` -func validatorId(i int) string { - return fmt.Sprintf(serviceURLFormat, i) -} - func waitForNetworkSimulationCompletion(t *testing.T, wg *sync.WaitGroup) { // Wait for all messages to be transmitted done := make(chan struct{}) @@ -98,217 +69,3 @@ func waitForNetworkSimulationCompletion(t *testing.T, wg *sync.WaitGroup) { t.Fatal("Timeout waiting for message to be handled") } } - -// ~~~~~~ RainTree Unit Test Mocks ~~~~~~ - -// createP2PModules returns a map of configured p2pModules keyed by an incremental naming convention (eg: `val_1`, `val_2`, etc.) -func createP2PModules(t *testing.T, busMocks []*mockModules.MockBus, netMock mocknet.Mocknet) (p2pModules map[string]*p2pModule) { - peerIDs := setupMockNetPeers(t, netMock, len(busMocks)) - - p2pModules = make(map[string]*p2pModule, len(busMocks)) - for i := range busMocks { - host := netMock.Host(peerIDs[i]) - p2pMod, err := Create(busMocks[i], WithHostOption(host)) - require.NoError(t, err) - p2pModules[validatorId(i+1)] = p2pMod.(*p2pModule) - } - return -} - -func setupMockNetPeers(t *testing.T, netMock mocknet.Mocknet, numPeers int) (peerIDs []libp2pPeer.ID) { - // Add a libp2p peers/hosts to the `MockNet` with the keypairs corresponding - // to the genesis validators' keypairs - for i, privKey := range keys[:numPeers] { - peerInfo, err := utils.Libp2pAddrInfoFromPeer(&typesP2P.NetworkPeer{ - PublicKey: privKey.PublicKey(), - Address: privKey.Address(), - ServiceURL: validatorId(i + 1), - }) - require.NoError(t, err) - - libp2pPrivKey, err := libp2pCrypto.UnmarshalEd25519PrivateKey(privKey.Bytes()) - require.NoError(t, err) - - _, err = netMock.AddPeer(libp2pPrivKey, peerInfo.Addrs[0]) - require.NoError(t, err) - - peerIDs = append(peerIDs, peerInfo.ID) - } - - // Link all peers such that any may dial/connect to any other. - err := netMock.LinkAll() - require.NoError(t, err) - - return peerIDs -} - -// createMockRuntimeMgrs creates `numValidators` instances of mocked `RuntimeMgr` that are essentially -// representing the runtime environments of the validators that we will use in our tests -func createMockRuntimeMgrs(t *testing.T, numValidators int) []modules.RuntimeMgr { - ctrl := gomock.NewController(t) - mockRuntimeMgrs := make([]modules.RuntimeMgr, numValidators) - valKeys := make([]cryptoPocket.PrivateKey, numValidators) - copy(valKeys, keys[:numValidators]) - mockGenesisState := createMockGenesisState(valKeys) - for i := range mockRuntimeMgrs { - serviceURL := validatorId(i + 1) - hostname, portStr, err := net.SplitHostPort(serviceURL) - require.NoError(t, err) - - port, err := strconv.Atoi(portStr) - require.NoError(t, err) - - cfg := &configs.Config{ - RootDirectory: "", - PrivateKey: valKeys[i].String(), - P2P: &configs.P2PConfig{ - Hostname: hostname, - PrivateKey: valKeys[i].String(), - Port: uint32(port), - ConnectionType: types.ConnectionType_EmptyConnection, - MaxNonces: defaults.DefaultP2PMaxNonces, - }, - } - - mockRuntimeMgr := mockModules.NewMockRuntimeMgr(ctrl) - mockRuntimeMgr.EXPECT().GetConfig().Return(cfg).AnyTimes() - mockRuntimeMgr.EXPECT().GetGenesis().Return(mockGenesisState).AnyTimes() - mockRuntimeMgrs[i] = mockRuntimeMgr - } - return mockRuntimeMgrs -} - -func createMockBuses(t *testing.T, runtimeMgrs []modules.RuntimeMgr) []*mockModules.MockBus { - mockBuses := make([]*mockModules.MockBus, len(runtimeMgrs)) - for i := range mockBuses { - mockBuses[i] = createMockBus(t, runtimeMgrs[i]) - } - return mockBuses -} - -func createMockBus(t *testing.T, runtimeMgr modules.RuntimeMgr) *mockModules.MockBus { - ctrl := gomock.NewController(t) - mockBus := mockModules.NewMockBus(ctrl) - mockBus.EXPECT().GetRuntimeMgr().Return(runtimeMgr).AnyTimes() - mockBus.EXPECT().RegisterModule(gomock.Any()).DoAndReturn(func(m modules.Module) { - m.SetBus(mockBus) - }).AnyTimes() - mockModulesRegistry := mockModules.NewMockModulesRegistry(ctrl) - mockModulesRegistry.EXPECT().GetModule(peerstore_provider.ModuleName).Return(nil, runtime.ErrModuleNotRegistered(peerstore_provider.ModuleName)).AnyTimes() - mockModulesRegistry.EXPECT().GetModule(current_height_provider.ModuleName).Return(nil, runtime.ErrModuleNotRegistered(current_height_provider.ModuleName)).AnyTimes() - mockBus.EXPECT().GetModulesRegistry().Return(mockModulesRegistry).AnyTimes() - mockBus.EXPECT().PublishEventToBus(gomock.Any()).AnyTimes() - return mockBus -} - -// createMockGenesisState configures and returns a mocked GenesisState -func createMockGenesisState(valKeys []cryptoPocket.PrivateKey) *genesis.GenesisState { - genesisState := new(genesis.GenesisState) - validators := make([]*coreTypes.Actor, len(valKeys)) - for i, valKey := range valKeys { - addr := valKey.Address().String() - mockActor := &coreTypes.Actor{ - ActorType: coreTypes.ActorType_ACTOR_TYPE_VAL, - Address: addr, - PublicKey: valKey.PublicKey().String(), - ServiceUrl: validatorId(i + 1), - StakedAmount: test_artifacts.DefaultStakeAmountString, - PausedHeight: int64(0), - UnstakingHeight: int64(0), - Output: addr, - } - validators[i] = mockActor - } - genesisState.Validators = validators - - return genesisState -} - -// Bus Mock - needed to return the appropriate modules when accessed -func prepareBusMock(busMock *mockModules.MockBus, - persistenceMock *mockModules.MockPersistenceModule, - consensusMock *mockModules.MockConsensusModule, - telemetryMock *mockModules.MockTelemetryModule, -) { - busMock.EXPECT().GetPersistenceModule().Return(persistenceMock).AnyTimes() - busMock.EXPECT().GetConsensusModule().Return(consensusMock).AnyTimes() - busMock.EXPECT().GetTelemetryModule().Return(telemetryMock).AnyTimes() -} - -// Consensus mock - only needed for validatorMap access -func prepareConsensusMock(t *testing.T, busMock *mockModules.MockBus) *mockModules.MockConsensusModule { - ctrl := gomock.NewController(t) - consensusMock := mockModules.NewMockConsensusModule(ctrl) - consensusMock.EXPECT().CurrentHeight().Return(uint64(1)).AnyTimes() - - consensusMock.EXPECT().GetBus().Return(busMock).AnyTimes() - consensusMock.EXPECT().SetBus(busMock).AnyTimes() - consensusMock.EXPECT().GetModuleName().Return(modules.ConsensusModuleName).AnyTimes() - busMock.RegisterModule(consensusMock) - - return consensusMock -} - -// Persistence mock - only needed for validatorMap access -func preparePersistenceMock(t *testing.T, busMock *mockModules.MockBus, genesisState *genesis.GenesisState) *mockModules.MockPersistenceModule { - ctrl := gomock.NewController(t) - - persistenceModuleMock := mockModules.NewMockPersistenceModule(ctrl) - readCtxMock := mockModules.NewMockPersistenceReadContext(ctrl) - - readCtxMock.EXPECT().GetAllValidators(gomock.Any()).Return(genesisState.GetValidators(), nil).AnyTimes() - persistenceModuleMock.EXPECT().NewReadContext(gomock.Any()).Return(readCtxMock, nil).AnyTimes() - readCtxMock.EXPECT().Release().AnyTimes() - - persistenceModuleMock.EXPECT().GetBus().Return(busMock).AnyTimes() - persistenceModuleMock.EXPECT().SetBus(busMock).AnyTimes() - persistenceModuleMock.EXPECT().GetModuleName().Return(modules.PersistenceModuleName).AnyTimes() - busMock.RegisterModule(persistenceModuleMock) - - return persistenceModuleMock -} - -// Telemetry mock - Needed to help with proper counts for number of expected network writes -func prepareTelemetryMock(t *testing.T, busMock *mockModules.MockBus, valId string, wg *sync.WaitGroup, expectedNumNetworkWrites int) *mockModules.MockTelemetryModule { - ctrl := gomock.NewController(t) - telemetryMock := mockModules.NewMockTelemetryModule(ctrl) - - timeSeriesAgentMock := prepareNoopTimeSeriesAgentMock(t) - eventMetricsAgentMock := prepareEventMetricsAgentMock(t, valId, wg, expectedNumNetworkWrites) - - telemetryMock.EXPECT().GetTimeSeriesAgent().Return(timeSeriesAgentMock).AnyTimes() - telemetryMock.EXPECT().GetEventMetricsAgent().Return(eventMetricsAgentMock).AnyTimes() - - telemetryMock.EXPECT().GetModuleName().Return(modules.TelemetryModuleName).AnyTimes() - telemetryMock.EXPECT().GetBus().Return(busMock).AnyTimes() - telemetryMock.EXPECT().SetBus(busMock).AnyTimes() - busMock.RegisterModule(telemetryMock) - - return telemetryMock -} - -// Noop mock - no specific business logic to tend to in the timeseries agent mock -func prepareNoopTimeSeriesAgentMock(t *testing.T) *mockModules.MockTimeSeriesAgent { - ctrl := gomock.NewController(t) - timeseriesAgentMock := mockModules.NewMockTimeSeriesAgent(ctrl) - - timeseriesAgentMock.EXPECT().CounterRegister(gomock.Any(), gomock.Any()).AnyTimes() - timeseriesAgentMock.EXPECT().CounterIncrement(gomock.Any()).AnyTimes() - - return timeseriesAgentMock -} - -// Events metric mock - Needed to help with proper counts for number of expected network writes -func prepareEventMetricsAgentMock(t *testing.T, valId string, wg *sync.WaitGroup, expectedNumNetworkWrites int) *mockModules.MockEventMetricsAgent { - ctrl := gomock.NewController(t) - eventMetricsAgentMock := mockModules.NewMockEventMetricsAgent(ctrl) - - eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() - eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Eq(telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL), gomock.Any()).Do(func(n, e any, l ...any) { - log.Printf("[valId: %s] Write\n", valId) - wg.Done() - }).Times(expectedNumNetworkWrites) - eventMetricsAgentMock.EXPECT().EmitEvent(gomock.Any(), gomock.Any(), gomock.Not(telemetry.P2P_RAINTREE_MESSAGE_EVENT_METRIC_SEND_LABEL), gomock.Any()).AnyTimes() - - return eventMetricsAgentMock -} diff --git a/shared/k8s/debug.go b/shared/k8s/debug.go index 531c2ab7a..a58785c24 100644 --- a/shared/k8s/debug.go +++ b/shared/k8s/debug.go @@ -31,6 +31,7 @@ func init() { // FetchValidatorPrivateKeys returns a map corresponding to the data section of // the validator private keys k8s secret (yaml), located at `privateKeysSecretResourceName`. +// NB: depends on running k8s cluster. func FetchValidatorPrivateKeys(clientset *kubernetes.Clientset) (map[string]string, error) { validatorKeysMap := make(map[string]string) diff --git a/shared/messaging/envelope.go b/shared/messaging/envelope.go index 4150fbfba..f65304007 100644 --- a/shared/messaging/envelope.go +++ b/shared/messaging/envelope.go @@ -4,6 +4,8 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/anypb" + + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" ) // PackMessage returns a *PocketEnvelope after having packed the message supplied as an argument @@ -12,7 +14,10 @@ func PackMessage(message proto.Message) (*PocketEnvelope, error) { if err != nil { return nil, err } - return &PocketEnvelope{Content: anyMsg}, nil + return &PocketEnvelope{ + Content: anyMsg, + Nonce: cryptoPocket.GetNonce(), + }, nil } // UnpackMessage extracts the message inside the PocketEnvelope decorating it with typing information diff --git a/shared/messaging/proto/debug_message.proto b/shared/messaging/proto/debug_message.proto index 7ce079afa..7e42f7ddd 100644 --- a/shared/messaging/proto/debug_message.proto +++ b/shared/messaging/proto/debug_message.proto @@ -30,6 +30,10 @@ message DebugMessage { google.protobuf.Any message = 3; } +message DebugStringMessage { + string value = 1; +} + // NB: See https://en.wikipedia.org/wiki/Routing for more info on routing and delivery schemes. enum DebugMessageRoutingType { DEBUG_MESSAGE_TYPE_UNKNOWN = 0;