diff --git a/Makefile b/Makefile index bb174325e..1b8e7320e 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,8 @@ define NOT_IN_GOPATH_ERROR Current dir is $(CURDIR), which seems to be different from your GOPATH. Please, build status-go from GOPATH for proper build. - GOPATH = $(shell go env GOPATH) - Current dir = $(CURDIR) + GOPATH = $(shell go env GOPATH) + Current dir = $(CURDIR) Expected dir = $(EXPECTED_PATH)) See https://golang.org/doc/code.html#GOPATH for more info @@ -161,7 +161,7 @@ mock: ##@other Regenerate mocks mockgen -package=fake -destination=transactions/fake/mock.go -source=transactions/fake/txservice.go mockgen -package=account -destination=account/accounts_mock.go -source=account/accounts.go mockgen -package=jail -destination=jail/cell_mock.go -source=jail/cell.go - mockgen -package=status -destination=services/status/account_mock.go -source=services/status/service.go + mockgen -package=status -destination=services/status/account_mock.go -source=services/status/service.go docker-test: ##@tests Run tests in a docker container with golang. docker run --privileged --rm -it -v "$(shell pwd):$(DOCKER_TEST_WORKDIR)" -w "$(DOCKER_TEST_WORKDIR)" $(DOCKER_TEST_IMAGE) go test ${ARGS} diff --git a/cmd/statusd/main.go b/cmd/statusd/main.go index f3c34c445..539adf57a 100644 --- a/cmd/statusd/main.go +++ b/cmd/statusd/main.go @@ -41,6 +41,7 @@ var ( lesEnabled = flag.Bool("les", false, "Enable LES protocol") whisperEnabled = flag.Bool("shh", false, "Enable Whisper protocol") statusService = flag.String("status", "", `Enable StatusService, possible values: "ipc", "http"`) + debugAPI = flag.Bool("debug", false, `Enable debug API endpoints under "debug_" namespace`) swarmEnabled = flag.Bool("swarm", false, "Enable Swarm protocol") maxPeers = flag.Int("maxpeers", 25, "maximum number of p2p peers (including all protocols)") httpEnabled = flag.Bool("http", false, "Enable HTTP RPC endpoint") @@ -259,11 +260,15 @@ func makeNodeConfig() (*params.NodeConfig, error) { nodeConfig.ClusterConfig.BootNodes = strings.Split(*bootnodes, ",") } - nodeConfig, err = configureStatusService(*statusService, nodeConfig) - if err != nil { + if nodeConfig, err = configureStatusService(*statusService, nodeConfig); err != nil { return nil, err } + nodeConfig.DebugAPIEnabled = *debugAPI + if nodeConfig.DebugAPIEnabled { + nodeConfig.AddAPIModule("debug") + } + if *whisperEnabled { return whisperConfig(nodeConfig) } diff --git a/node/node.go b/node/node.go index 49f2211c5..f8d692d92 100644 --- a/node/node.go +++ b/node/node.go @@ -259,7 +259,7 @@ func activateShhService(stack *node.Node, config *params.NodeConfig, db *leveldb return nil, err } - svc := shhext.New(whisper, shhext.EnvelopeSignalHandler{}, db) + svc := shhext.New(whisper, shhext.EnvelopeSignalHandler{}, db, config.DebugAPIEnabled) return svc, nil }) } diff --git a/params/config.go b/params/config.go index 4e4e4167b..9c3b1570d 100644 --- a/params/config.go +++ b/params/config.go @@ -318,6 +318,9 @@ type NodeConfig struct { // StatusServiceEnabled enables status service api StatusServiceEnabled bool + + // DebugAPIEnabled enables debug api + DebugAPIEnabled bool } // NewNodeConfig creates new node configuration object diff --git a/services/shhext/debug.go b/services/shhext/debug.go new file mode 100644 index 000000000..7a2ce7b9f --- /dev/null +++ b/services/shhext/debug.go @@ -0,0 +1,74 @@ +package shhext + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" + "github.com/status-im/status-go/services" +) + +var ( + postSyncTimeout = 60 * time.Second + errEnvelopeExpired = errors.New("envelope expired before being sent") + errNoShhextAttachedAPI = errors.New("No shhext attached") +) + +// DebugAPI represents a set of APIs from the `web3.debug` namespace. +type DebugAPI struct { + s *Service +} + +// NewDebugAPI creates an instance of the debug API. +func NewDebugAPI(s *Service) *DebugAPI { + return &DebugAPI{s: s} +} + +// PostSync sends an envelope through shhext_post and waits until it's sent. +func (api *DebugAPI) PostSync(ctx context.Context, req whisper.NewMessage) (hash hexutil.Bytes, err error) { + shhAPI := services.APIByNamespace(api.s.APIs(), "shhext") + if shhAPI == nil { + err = errNoShhextAttachedAPI + return + } + s, ok := shhAPI.(*PublicAPI) + if !ok { + err = errNoShhextAttachedAPI + return + } + hash, err = s.Post(ctx, req) + if err != nil { + return + } + ctxTimeout, cancel := context.WithTimeout(ctx, postSyncTimeout) + defer cancel() + err = api.waitForHash(ctxTimeout, hash) + return +} + +// waitForHash waits for a specific hash to be sent +func (api *DebugAPI) waitForHash(ctx context.Context, hash hexutil.Bytes) error { + h := common.BytesToHash(hash) + events := make(chan whisper.EnvelopeEvent, 100) + sub := api.s.w.SubscribeEnvelopeEvents(events) + defer sub.Unsubscribe() + for { + select { + case ev := <-events: + if ev.Hash == h { + if ev.Event == whisper.EventEnvelopeSent { + return nil + } + if ev.Event == whisper.EventEnvelopeExpired { + return errEnvelopeExpired + } + } + case <-ctx.Done(): + return fmt.Errorf("wait for hash canceled: %v", ctx.Err()) + } + } +} diff --git a/services/shhext/service.go b/services/shhext/service.go index 56690a1ca..eea0a77d8 100644 --- a/services/shhext/service.go +++ b/services/shhext/service.go @@ -41,13 +41,14 @@ type Service struct { tracker *tracker nodeID *ecdsa.PrivateKey deduplicator *dedup.Deduplicator + debug bool } // Make sure that Service implements node.Service interface. var _ node.Service = (*Service)(nil) // New returns a new Service. -func New(w *whisper.Whisper, handler EnvelopeEventsHandler, db *leveldb.DB) *Service { +func New(w *whisper.Whisper, handler EnvelopeEventsHandler, db *leveldb.DB, debug bool) *Service { track := &tracker{ w: w, handler: handler, @@ -57,6 +58,7 @@ func New(w *whisper.Whisper, handler EnvelopeEventsHandler, db *leveldb.DB) *Ser w: w, tracker: track, deduplicator: dedup.NewDeduplicator(w, db), + debug: debug, } } @@ -67,7 +69,7 @@ func (s *Service) Protocols() []p2p.Protocol { // APIs returns a list of new APIs. func (s *Service) APIs() []rpc.API { - return []rpc.API{ + apis := []rpc.API{ { Namespace: "shhext", Version: "1.0", @@ -75,6 +77,17 @@ func (s *Service) APIs() []rpc.API { Public: true, }, } + + if s.debug { + apis = append(apis, rpc.API{ + Namespace: "debug", + Version: "1.0", + Service: NewDebugAPI(s), + Public: true, + }) + } + + return apis } // Start is run when a service is started. diff --git a/services/shhext/service_test.go b/services/shhext/service_test.go index 72a15fa1b..a753f0de7 100644 --- a/services/shhext/service_test.go +++ b/services/shhext/service_test.go @@ -2,6 +2,7 @@ package shhext import ( "context" + "errors" "fmt" "math" "testing" @@ -78,7 +79,7 @@ func (s *ShhExtSuite) SetupTest() { s.NoError(stack.Register(func(n *node.ServiceContext) (node.Service, error) { return s.whisper[i], nil })) - s.services[i] = New(s.whisper[i], nil, nil) + s.services[i] = New(s.whisper[i], nil, nil, true) s.NoError(stack.Register(func(n *node.ServiceContext) (node.Service, error) { return s.services[i], nil })) @@ -164,7 +165,7 @@ func (s *ShhExtSuite) TestRequestMessages() { }() mock := newHandlerMock(1) - service := New(shh, mock, nil) + service := New(shh, mock, nil, false) api := NewPublicAPI(service) const ( @@ -231,6 +232,98 @@ func (s *ShhExtSuite) TestRequestMessages() { s.Contains(api.service.tracker.cache, common.BytesToHash(hash)) } +func (s *ShhExtSuite) TestDebugPostSync() { + mock := newHandlerMock(1) + s.services[0].tracker.handler = mock + symID, err := s.whisper[0].GenerateSymKey() + s.NoError(err) + s.nodes[0].Server().AddPeer(s.nodes[1].Server().Self()) + client, err := s.nodes[0].Attach() + s.NoError(err) + var hash common.Hash + + var testCases = []struct { + name string + msg whisper.NewMessage + postSyncTimeout time.Duration + expectedErr error + }{ + { + name: "timeout", + msg: whisper.NewMessage{ + SymKeyID: symID, + PowTarget: whisper.DefaultMinimumPoW, + PowTime: 200, + Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01}, + Payload: []byte("hello"), + }, + postSyncTimeout: postSyncTimeout, + expectedErr: nil, + }, + { + name: "invalid message", + msg: whisper.NewMessage{ + PowTarget: whisper.DefaultMinimumPoW, + PowTime: 200, + Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01}, + Payload: []byte("hello"), + }, + postSyncTimeout: postSyncTimeout, + expectedErr: whisper.ErrSymAsym, + }, + { + name: "context deadline exceeded", + msg: whisper.NewMessage{ + SymKeyID: symID, + PowTarget: whisper.DefaultMinimumPoW, + PowTime: 10, + Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01}, + TTL: 100, + Payload: []byte("hello"), + }, + postSyncTimeout: 1 * time.Millisecond, + expectedErr: errors.New("context deadline exceeded"), + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), tc.postSyncTimeout) + defer cancel() + err := client.CallContext(ctx, &hash, "debug_postSync", tc.msg) + + if tc.expectedErr != nil { + s.Equal(tc.expectedErr.Error(), err.Error()) + } else { + s.NoError(err) + } + }) + } +} + +func (s *ShhExtSuite) TestEnvelopeExpiredOnDebugPostSync() { + mock := newHandlerMock(1) + s.services[0].tracker.handler = mock + symID, err := s.whisper[0].GenerateSymKey() + s.NoError(err) + client, err := s.nodes[0].Attach() + s.NoError(err) + var hash common.Hash + + ctx, cancel := context.WithTimeout(context.Background(), postSyncTimeout) + defer cancel() + err = client.CallContext(ctx, &hash, "debug_postSync", whisper.NewMessage{ + SymKeyID: symID, + PowTarget: whisper.DefaultMinimumPoW, + PowTime: 200, + Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01}, + Payload: []byte("hello"), + TTL: 1, + }) + + s.Equal(errEnvelopeExpired.Error(), err.Error()) +} + func (s *ShhExtSuite) TearDown() { for _, n := range s.nodes { s.NoError(n.Stop()) diff --git a/services/status/service.go b/services/status/service.go index 3e7024a84..7c02448f2 100644 --- a/services/status/service.go +++ b/services/status/service.go @@ -25,7 +25,7 @@ type AccountManager interface { CreateAccount(password string) (address, pubKey, mnemonic string, err error) } -// Service represents out own implementation of status status operations. +// Service represents our own implementation of status status operations. type Service struct { am AccountManager w WhisperService diff --git a/services/utils.go b/services/utils.go new file mode 100644 index 000000000..8111c934e --- /dev/null +++ b/services/utils.go @@ -0,0 +1,13 @@ +package services + +import "github.com/ethereum/go-ethereum/rpc" + +// APIByNamespace retrieve an api by its namespace or returns nil. +func APIByNamespace(apis []rpc.API, namespace string) interface{} { + for _, api := range apis { + if api.Namespace == namespace { + return api.Service + } + } + return nil +} diff --git a/t/benchmarks/mailserver_test.go b/t/benchmarks/mailserver_test.go index 8e334e9d7..dae9b2a04 100644 --- a/t/benchmarks/mailserver_test.go +++ b/t/benchmarks/mailserver_test.go @@ -35,7 +35,7 @@ func testMailserverPeer(t *testing.T) { shhService := createWhisperService() shhAPI := whisper.NewPublicWhisperAPI(shhService) - mailService := shhext.New(shhService, nil, nil) + mailService := shhext.New(shhService, nil, nil, false) shhextAPI := shhext.NewPublicAPI(mailService) // create node with services diff --git a/t/e2e/services/base_api_test.go b/t/e2e/services/base_api_test.go index 91d7c8ef9..8756f6ada 100644 --- a/t/e2e/services/base_api_test.go +++ b/t/e2e/services/base_api_test.go @@ -56,7 +56,7 @@ func (s *BaseJSONRPCSuite) isMethodExported(method string, private bool) bool { return !(response.Error != nil && response.Error.Code == methodNotFoundErrorCode) } -func (s *BaseJSONRPCSuite) SetupTest(upstreamEnabled bool, statusServiceEnabled bool) error { +func (s *BaseJSONRPCSuite) SetupTest(upstreamEnabled, statusServiceEnabled, debugAPIEnabled bool) error { s.Backend = api.NewStatusBackend() s.NotNil(s.Backend) @@ -65,6 +65,10 @@ func (s *BaseJSONRPCSuite) SetupTest(upstreamEnabled bool, statusServiceEnabled nodeConfig.IPCEnabled = false nodeConfig.StatusServiceEnabled = statusServiceEnabled + nodeConfig.DebugAPIEnabled = debugAPIEnabled + if nodeConfig.DebugAPIEnabled { + nodeConfig.AddAPIModule("debug") + } nodeConfig.HTTPHost = "" // to make sure that no HTTP interface is started if upstreamEnabled { diff --git a/t/e2e/services/debug_api_test.go b/t/e2e/services/debug_api_test.go new file mode 100644 index 000000000..9bbbf2f7d --- /dev/null +++ b/t/e2e/services/debug_api_test.go @@ -0,0 +1,145 @@ +package services + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" + "github.com/status-im/status-go/node" + "github.com/status-im/status-go/params" + "github.com/stretchr/testify/suite" + + . "github.com/status-im/status-go/t/utils" +) + +func TestDebugAPISuite(t *testing.T) { + s := new(DebugAPISuite) + s.upstream = false + suite.Run(t, s) +} + +func TestDebugAPISuiteUpstream(t *testing.T) { + s := new(DebugAPISuite) + s.upstream = true + suite.Run(t, s) +} + +type DebugAPISuite struct { + BaseJSONRPCSuite + upstream bool +} + +func (s *DebugAPISuite) TestAccessibleDebugAPIsUnexported() { + if s.upstream && GetNetworkID() == params.StatusChainNetworkID { + s.T().Skip() + return + } + + err := s.SetupTest(s.upstream, false, false) + s.NoError(err) + // Debug APIs should be unavailable + s.AssertAPIMethodUnexported("debug_postSync") + err = s.Backend.StopNode() + s.NoError(err) + + err = s.SetupTest(s.upstream, false, true) + s.NoError(err) + defer func() { + err := s.Backend.StopNode() + s.NoError(err) + }() + // Debug APIs should be available + s.AssertAPIMethodExported("debug_postSync") +} + +func (s *DebugAPISuite) TestDebugPostSyncSuccess() { + // Test upstream if that's not StatusChain + if s.upstream && GetNetworkID() == params.StatusChainNetworkID { + s.T().Skip() + return + } + + err := s.SetupTest(s.upstream, false, true) + s.NoError(err) + defer func() { + err := s.Backend.StopNode() + s.NoError(err) + }() + + dir, err := ioutil.TempDir("", "test-debug") + s.NoError(err) + defer os.RemoveAll(dir) //nolint: errcheck + s.addPeerToCurrentNode(dir) + + symID := s.generateSymKey() + result := s.sendPostConfirmMessage(symID) + + var r struct { + Error struct { + Message string `json:"message"` + } `json:"error"` + Result hexutil.Bytes `json:"result"` + } + s.NoError(json.Unmarshal([]byte(result), &r)) + s.Empty(r.Error.Message) + s.NotEmpty(r.Result) +} + +// generateSymKey generates and stores a symetric key. +func (s *DebugAPISuite) generateSymKey() string { + w, err := s.Backend.StatusNode().WhisperService() + s.Require().NoError(err) + symID, err := w.GenerateSymKey() + s.Require().NoError(err) + + return symID +} + +// sendPostConfirmMessage calls debug_postSync endpoint with valid +// parameters. +func (s *DebugAPISuite) sendPostConfirmMessage(symID string) string { + req := whisper.NewMessage{ + SymKeyID: symID, + PowTarget: whisper.DefaultMinimumPoW, + PowTime: 200, + Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01}, + Payload: []byte("hello"), + } + body, err := json.Marshal(req) + s.NoError(err) + + basicCall := fmt.Sprintf( + `{"jsonrpc":"2.0","method":"debug_postSync","params":[%s],"id":67}`, + body) + + return s.Backend.CallPrivateRPC(basicCall) +} + +// addPeers adds a peer to the running node +func (s *DebugAPISuite) addPeerToCurrentNode(dir string) { + s.Require().NotNil(s.Backend) + node1 := s.Backend.StatusNode().GethNode() + s.NotNil(node1) + node2 := s.newPeer("test2", dir).GethNode() + s.NotNil(node2) + + node1.Server().AddPeer(node2.Server().Self()) +} + +// newNode creates, configures and starts a new peer. +func (s *DebugAPISuite) newPeer(name, dir string) *node.StatusNode { + // network id is irrelevant + cfg, err := params.NewNodeConfig(dir, "", 777) + cfg.LightEthConfig.Enabled = false + cfg.Name = name + cfg.NetworkID = uint64(GetNetworkID()) + s.Require().NoError(err) + n := node.New() + s.Require().NoError(n.Start(cfg)) + + return n +} diff --git a/t/e2e/services/filters_latest_test.go b/t/e2e/services/filters_latest_test.go index 185409710..84486dddf 100644 --- a/t/e2e/services/filters_latest_test.go +++ b/t/e2e/services/filters_latest_test.go @@ -36,7 +36,7 @@ type FiltersAPISuite struct { } func (s *FiltersAPISuite) TestFilters() { - err := s.SetupTest(s.upstream, false) + err := s.SetupTest(s.upstream, false, false) s.NoError(err) defer func() { err := s.Backend.StopNode() diff --git a/t/e2e/services/personal_api_test.go b/t/e2e/services/personal_api_test.go index d4d6a69eb..642b9becd 100644 --- a/t/e2e/services/personal_api_test.go +++ b/t/e2e/services/personal_api_test.go @@ -61,7 +61,7 @@ func (s *PersonalSignSuite) TestRestrictedPersonalAPIs() { return } - err := s.SetupTest(s.upstream, false) + err := s.SetupTest(s.upstream, false, false) s.NoError(err) defer func() { err := s.Backend.StopNode() @@ -191,7 +191,7 @@ func (s *PersonalSignSuite) testPersonalSign(testParams testParams) string { testParams.HandlerFactory = s.notificationHandlerSuccess } - err := s.SetupTest(s.upstream, false) + err := s.SetupTest(s.upstream, false, false) s.NoError(err) defer func() { err := s.Backend.StopNode() @@ -252,7 +252,7 @@ func (s *PersonalSignSuite) TestPersonalRecoverSuccess() { return } - err := s.SetupTest(s.upstream, false) + err := s.SetupTest(s.upstream, false, false) s.NoError(err) defer func() { err := s.Backend.StopNode() diff --git a/t/e2e/services/status_api_test.go b/t/e2e/services/status_api_test.go index 39df3edad..01a6aee7c 100644 --- a/t/e2e/services/status_api_test.go +++ b/t/e2e/services/status_api_test.go @@ -46,7 +46,7 @@ func (s *StatusAPISuite) TestAccessibleStatusAPIs() { return } - err := s.SetupTest(s.upstream, true) + err := s.SetupTest(s.upstream, true, false) s.NoError(err) defer func() { err := s.Backend.StopNode() @@ -113,7 +113,7 @@ func (s *StatusAPISuite) testStatusLogin(testParams statusTestParams) *status.Lo testParams.HandlerFactory = s.notificationHandlerSuccess } - err := s.SetupTest(s.upstream, true) + err := s.SetupTest(s.upstream, true, false) s.NoError(err) defer func() { err := s.Backend.StopNode() @@ -159,7 +159,7 @@ func (s *StatusAPISuite) testStatusSignup(testParams statusTestParams) *status.S testParams.HandlerFactory = s.notificationHandlerSuccess } - err := s.SetupTest(s.upstream, true) + err := s.SetupTest(s.upstream, true, false) s.NoError(err) defer func() { err := s.Backend.StopNode()