From 25d46c6d82fbf3e4b6659cb96cf1789bac87f12a Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Fri, 17 Jan 2020 13:39:09 +0100 Subject: [PATCH 1/9] Fix waku tests & contact ens (#1802) --- Makefile | 2 +- VERSION | 2 +- eth-node/core/types/transaction.go | 8 +++ eth-node/go.mod | 2 + eth-node/go.sum | 1 + params/config.go | 7 ++- protocol/contact.go | 2 +- protocol/message_handler.go | 2 +- protocol/messenger.go | 54 ++++++++++++++----- protocol/messenger_test.go | 15 ++++-- protocol/persistence_legacy.go | 5 +- protocol/transaction_validator.go | 13 +++-- protocol/transport/filters_manager_test.go | 25 +++++++-- protocol/transport/transport.go | 1 + protocol/transport/waku/envelopes_test.go | 3 +- protocol/transport/whisper/envelopes_test.go | 3 +- services/shhext/api.go | 3 +- services/shhext/service.go | 44 ++++++++++++--- services/wallet/downloader.go | 4 +- .../eth-node/core/types/transaction.go | 8 +++ .../status-im/status-go/protocol/contact.go | 2 +- .../status-go/protocol/message_handler.go | 2 +- .../status-im/status-go/protocol/messenger.go | 54 ++++++++++++++----- .../status-go/protocol/persistence_legacy.go | 5 +- .../protocol/transaction_validator.go | 13 +++-- .../status-go/protocol/transport/transport.go | 1 + 26 files changed, 219 insertions(+), 62 deletions(-) diff --git a/Makefile b/Makefile index e04f413641..97fe08c0ef 100644 --- a/Makefile +++ b/Makefile @@ -244,7 +244,7 @@ test-unit: UNIT_TEST_PACKAGES = $(shell go list ./... | \ grep -v /lib | \ grep -v /transactions/fake ) test-unit: ##@tests Run unit and integration tests - go test -v -failfast $(UNIT_TEST_PACKAGES) $(gotest_extraflags) + go test -v -failfast $(UNIT_TEST_PACKAGES) $(gotest_extraflags) && cd ./protocol && $(MAKE) test test-unit-race: gotest_extraflags=-race test-unit-race: test-unit ##@tests Run unit and integration tests with -race flag diff --git a/VERSION b/VERSION index adbb8d4522..e5d9eb181f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.39.5 +0.39.6 diff --git a/eth-node/core/types/transaction.go b/eth-node/core/types/transaction.go index 6bd2fee2c9..d6c3c5d83e 100644 --- a/eth-node/core/types/transaction.go +++ b/eth-node/core/types/transaction.go @@ -6,6 +6,14 @@ import ( "github.com/status-im/status-go/eth-node/types" ) +type TransactionStatus uint64 + +const ( + TransactionStatusFailed = 0 + TransactionStatusSuccess = 1 + TransactionStatusPending = 2 +) + type Message struct { to *types.Address from types.Address diff --git a/eth-node/go.mod b/eth-node/go.mod index 2edd418de8..3d0660db4b 100644 --- a/eth-node/go.mod +++ b/eth-node/go.mod @@ -13,7 +13,9 @@ replace github.com/status-im/status-go/waku => ../waku require ( github.com/ethereum/go-ethereum v1.9.5 github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f + github.com/pborman/uuid v1.2.0 github.com/status-im/doubleratchet v3.0.0+incompatible + github.com/status-im/status-go/extkeys v1.0.0 github.com/status-im/status-go/waku v1.0.0 github.com/status-im/status-go/whisper/v6 v6.0.1 // indirect github.com/stretchr/testify v1.4.0 diff --git a/eth-node/go.sum b/eth-node/go.sum index 4d664f67c7..c8360de90d 100644 --- a/eth-node/go.sum +++ b/eth-node/go.sum @@ -323,6 +323,7 @@ golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf h1:fnPsqIDRbCSgumaMCRpoIo golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba h1:9bFeDpN3gTqNanMVqNcoR/pJQuP5uroC3t1D7eXozTE= golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c h1:/nJuwDLoL/zrqY6gf57vxC+Pi+pZ8bfhpPkicO5H7W4= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= diff --git a/params/config.go b/params/config.go index cc3285b914..bcd1588531 100644 --- a/params/config.go +++ b/params/config.go @@ -518,7 +518,12 @@ type ShhextConfig struct { // VerifyTransactionURL is the URL for verifying transactions. // IMPORTANT: It should always be mainnet unless used for testing - VerifyTransactionURL string + VerifyTransactionURL string + + // VerifyENSURL is the URL for verifying ens names. + // IMPORTANT: It should always be mainnet unless used for testing + VerifyENSURL string + VerifyTransactionChainID int64 } diff --git a/protocol/contact.go b/protocol/contact.go index 6f42f6be6a..fdbe8158ed 100644 --- a/protocol/contact.go +++ b/protocol/contact.go @@ -54,7 +54,7 @@ type Contact struct { SystemTags []string `json:"systemTags"` DeviceInfo []ContactDeviceInfo `json:"deviceInfo"` - TributeToTalk string `json:"tributeToTalk"` + TributeToTalk string `json:"tributeToTalk,omitEmpty"` } func (c Contact) PublicKey() (*ecdsa.PublicKey, error) { diff --git a/protocol/message_handler.go b/protocol/message_handler.go index 8324ea0ffb..5e0e286931 100644 --- a/protocol/message_handler.go +++ b/protocol/message_handler.go @@ -419,7 +419,7 @@ func (m *MessageHandler) HandleAcceptRequestAddressForTransaction(messageState * initialMessage.CommandParameters.CommandState = CommandStateRequestAddressForTransactionAccepted // Hide previous message - previousMessage, err := m.persistence.MessageByCommandID(command.Id) + previousMessage, err := m.persistence.MessageByCommandID(messageState.CurrentMessageState.Contact.ID, command.Id) if err != nil && err != errRecordNotFound { return err } diff --git a/protocol/messenger.go b/protocol/messenger.go index 30db150dd0..1a5d5871d1 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -678,6 +678,11 @@ func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string, response.Chats = []*Chat{&chat} response.Messages = buildSystemMessages(chat.MembershipUpdates, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } + return &response, m.saveChat(&chat) } @@ -737,6 +742,11 @@ func (m *Messenger) RemoveMemberFromGroupChat(ctx context.Context, chatID string chat.updateChatFromProtocolGroup(group) response.Chats = []*Chat{chat} response.Messages = buildSystemMessages(chat.MembershipUpdates, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } + return &response, m.saveChat(chat) } @@ -796,6 +806,10 @@ func (m *Messenger) AddMembersToGroupChat(ctx context.Context, chatID string, me response.Chats = []*Chat{chat} response.Messages = buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } return &response, m.saveChat(chat) } @@ -857,6 +871,10 @@ func (m *Messenger) AddAdminsToGroupChat(ctx context.Context, chatID string, mem response.Chats = []*Chat{chat} response.Messages = buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } return &response, m.saveChat(chat) } @@ -920,6 +938,10 @@ func (m *Messenger) ConfirmJoiningGroup(ctx context.Context, chatID string) (*Me response.Chats = []*Chat{chat} response.Messages = buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } return &response, m.saveChat(chat) } @@ -985,6 +1007,11 @@ func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string) (*Messeng response.Chats = []*Chat{chat} response.Messages = buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } + return &response, m.saveChat(chat) } @@ -2042,6 +2069,7 @@ func (m *Messenger) VerifyENSNames(rpcEndpoint, contractAddress string, ensDetai contact.ENSVerifiedAt = details.VerifiedAt contact.Name = details.Name + m.allContacts[contact.ID] = contact contacts = append(contacts, contact) } else { m.logger.Warn("Failed to resolve ens name", @@ -2254,7 +2282,7 @@ func (m *Messenger) AcceptRequestAddressForTransaction(ctx context.Context, mess message.OutgoingStatus = OutgoingStatusSending // Hide previous message - previousMessage, err := m.persistence.MessageByCommandID(messageID) + previousMessage, err := m.persistence.MessageByCommandID(chatID, messageID) if err != nil { return nil, err } @@ -2513,7 +2541,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas message.OutgoingStatus = OutgoingStatusSending // Hide previous message - previousMessage, err := m.persistence.MessageByCommandID(messageID) + previousMessage, err := m.persistence.MessageByCommandID(chatID, messageID) if err != nil && err != errRecordNotFound { return nil, err } @@ -2734,18 +2762,20 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. return nil, err } - // Hide previous message - previousMessage, err := m.persistence.MessageByCommandID(message.CommandParameters.ID) - if err != nil && err != errRecordNotFound { - return nil, err - } - - if previousMessage != nil { - err = m.persistence.HideMessage(previousMessage.ID) - if err != nil { + if len(message.CommandParameters.ID) != 0 { + // Hide previous message + previousMessage, err := m.persistence.MessageByCommandID(chatID, message.CommandParameters.ID) + if err != nil && err != errRecordNotFound { return nil, err } - message.Replace = previousMessage.ID + + if previousMessage != nil { + err = m.persistence.HideMessage(previousMessage.ID) + if err != nil { + return nil, err + } + message.Replace = previousMessage.ID + } } response.Messages = append(response.Messages, message) diff --git a/protocol/messenger_test.go b/protocol/messenger_test.go index 7f7fc01d5e..3576056175 100644 --- a/protocol/messenger_test.go +++ b/protocol/messenger_test.go @@ -72,6 +72,10 @@ func (n *testNode) RemovePeer(_ string) error { panic("not implemented") } +func (n *testNode) GetWaku(_ interface{}) (types.Waku, error) { + panic("not implemented") +} + func (n *testNode) GetWhisper(_ interface{}) (types.Whisper, error) { return n.shh, nil } @@ -1631,6 +1635,7 @@ func (s *MessengerSuite) TestSendEthTransaction() { s.Require().True(ok) client.messages = make(map[string]MockTransaction) client.messages[transactionHash] = MockTransaction{ + Status: coretypes.TransactionStatusSuccess, Message: coretypes.NewMessage( senderAddress, &receiverAddress, @@ -1731,6 +1736,7 @@ func (s *MessengerSuite) TestSendTokenTransaction() { s.Require().True(ok) client.messages = make(map[string]MockTransaction) client.messages[transactionHash] = MockTransaction{ + Status: coretypes.TransactionStatusSuccess, Message: coretypes.NewMessage( senderAddress, &contractAddress, @@ -2064,6 +2070,7 @@ func (s *MessengerSuite) TestRequestTransaction() { s.Require().True(ok) client.messages = make(map[string]MockTransaction) client.messages[transactionHash] = MockTransaction{ + Status: coretypes.TransactionStatusSuccess, Message: coretypes.NewMessage( senderAddress, &contractAddress, @@ -2100,7 +2107,7 @@ func (s *MessengerSuite) TestRequestTransaction() { } type MockTransaction struct { - Pending bool + Status coretypes.TransactionStatus Message coretypes.Message } @@ -2113,12 +2120,12 @@ type mockSendMessagesRequest struct { req types.MessagesRequest } -func (m MockEthClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, bool, error) { +func (m MockEthClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) { mockTransaction, ok := m.messages[hash.Hex()] if !ok { - return coretypes.Message{}, false, nil + return coretypes.Message{}, coretypes.TransactionStatusFailed, nil } else { - return mockTransaction.Message, mockTransaction.Pending, nil + return mockTransaction.Message, mockTransaction.Status, nil } } diff --git a/protocol/persistence_legacy.go b/protocol/persistence_legacy.go index ed4364c4a5..95cb7a31c5 100644 --- a/protocol/persistence_legacy.go +++ b/protocol/persistence_legacy.go @@ -237,7 +237,7 @@ func (db sqlitePersistence) messageByID(tx *sql.Tx, id string) (*Message, error) } } -func (db sqlitePersistence) MessageByCommandID(id string) (*Message, error) { +func (db sqlitePersistence) MessageByCommandID(chatID, id string) (*Message, error) { var message Message @@ -259,10 +259,13 @@ func (db sqlitePersistence) MessageByCommandID(id string) (*Message, error) { m1.source = c.id WHERE m1.command_id = ? + AND + m1.local_chat_id = ? ORDER BY m1.clock_value DESC LIMIT 1 `, allFields), id, + chatID, ) err := db.tableUserMessagesLegacyScanAllFields(row, &message) switch err { diff --git a/protocol/transaction_validator.go b/protocol/transaction_validator.go index b4160f895a..e19d72caa3 100644 --- a/protocol/transaction_validator.go +++ b/protocol/transaction_validator.go @@ -62,7 +62,7 @@ func NewTransactionValidator(addresses []types.Address, persistence *sqlitePersi } type EthClient interface { - TransactionByHash(context.Context, types.Hash) (coretypes.Message, bool, error) + TransactionByHash(context.Context, types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) } func (t *TransactionValidator) verifyTransactionSignature(ctx context.Context, from *ecdsa.PublicKey, address types.Address, transactionHash string, signature []byte) error { @@ -243,7 +243,8 @@ func (t *TransactionValidator) ValidateTransactions(ctx context.Context) ([]*Ver var validationResult *VerifyTransactionResponse t.logger.Debug("Validating transaction", zap.Any("transaction", transaction)) if transaction.CommandID != "" { - message, err := t.persistence.MessageByCommandID(transaction.CommandID) + chatID := contactIDFromPublicKey(transaction.From) + message, err := t.persistence.MessageByCommandID(chatID, transaction.CommandID) if err != nil { t.logger.Error("error pulling message", zap.Error(err)) @@ -313,13 +314,17 @@ func (t *TransactionValidator) ValidateTransaction(ctx context.Context, paramete c, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - message, pending, err := t.client.TransactionByHash(c, types.HexToHash(hash)) + message, status, err := t.client.TransactionByHash(c, types.HexToHash(hash)) if err != nil { return nil, err } - if pending { + switch status { + case coretypes.TransactionStatusPending: t.logger.Debug("Transaction pending") return &VerifyTransactionResponse{Pending: true}, nil + case coretypes.TransactionStatusFailed: + + return invalidResponse, nil } return t.validateTransaction(ctx, message, parameters, from) diff --git a/protocol/transport/filters_manager_test.go b/protocol/transport/filters_manager_test.go index 881246cfe0..6a901e90a4 100644 --- a/protocol/transport/filters_manager_test.go +++ b/protocol/transport/filters_manager_test.go @@ -9,7 +9,6 @@ import ( "testing" gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" - "github.com/status-im/status-go/protocol/sqlite" "github.com/status-im/status-go/protocol/tt" _ "github.com/mutecomm/go-sqlcipher" @@ -20,6 +19,23 @@ import ( "github.com/status-im/status-go/whisper/v6" ) +type testKeysPersistence struct { + keys map[string][]byte +} + +func newTestKeysPersistence() *testKeysPersistence { + return &testKeysPersistence{keys: make(map[string][]byte)} +} + +func (s *testKeysPersistence) Add(chatID string, key []byte) error { + s.keys[chatID] = key + return nil +} + +func (s *testKeysPersistence) All() (map[string][]byte, error) { + return s.keys, nil +} + func TestFiltersManagerSuite(t *testing.T) { suite.Run(t, new(FiltersManagerSuite)) } @@ -72,12 +88,11 @@ func (s *FiltersManagerSuite) SetupTest() { s.manager = append(s.manager, testKey) } - db, err := sqlite.Open(s.dbPath, "filter-key") - s.Require().NoError(err) + keysPersistence := newTestKeysPersistence() whisper := gethbridge.NewGethWhisperWrapper(whisper.New(nil)) - s.chats, err = NewFiltersManager(db, whisper, s.manager[0].privateKey, s.logger) + s.chats, err = NewFiltersManager(keysPersistence, whisper, s.manager[0].privateKey, s.logger) s.Require().NoError(err) } @@ -101,7 +116,7 @@ func (s *FiltersManagerSuite) TestPartitionedTopicWithDiscoveryDisabled() { func (s *FiltersManagerSuite) assertRequiredFilters() { partitionedTopic := fmt.Sprintf("contact-discovery-%d", s.manager[0].partitionedTopic) personalDiscoveryTopic := fmt.Sprintf("contact-discovery-%s", s.manager[0].publicKeyString()) - contactCodeTopic := contactCodeTopic(&s.manager[0].privateKey.PublicKey) + contactCodeTopic := ContactCodeTopic(&s.manager[0].privateKey.PublicKey) personalDiscoveryFilter := s.chats.filters[personalDiscoveryTopic] s.Require().NotNil(personalDiscoveryFilter, "It adds the discovery filter") diff --git a/protocol/transport/transport.go b/protocol/transport/transport.go index ea082f4ed9..9ec4c55dbd 100644 --- a/protocol/transport/transport.go +++ b/protocol/transport/transport.go @@ -33,6 +33,7 @@ type Transport interface { LoadFilters(filters []*Filter) ([]*Filter, error) RemoveFilters(filters []*Filter) error ResetFilters() error + Filters() []*Filter ProcessNegotiatedSecret(secret types.NegotiatedSecret) (*Filter, error) RetrieveRawAll() (map[Filter][]*types.Message, error) } diff --git a/protocol/transport/waku/envelopes_test.go b/protocol/transport/waku/envelopes_test.go index 14807f6e7f..c07eaf308c 100644 --- a/protocol/transport/waku/envelopes_test.go +++ b/protocol/transport/waku/envelopes_test.go @@ -11,6 +11,7 @@ import ( "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/transport" ) var ( @@ -31,7 +32,7 @@ func TestEnvelopesMonitorSuite(t *testing.T) { func (s *EnvelopesMonitorSuite) SetupTest() { s.monitor = NewEnvelopesMonitor( nil, - EnvelopesMonitorConfig{ + transport.EnvelopesMonitorConfig{ EnvelopeEventsHandler: nil, MaxAttempts: 0, MailserverConfirmationsEnabled: false, diff --git a/protocol/transport/whisper/envelopes_test.go b/protocol/transport/whisper/envelopes_test.go index 4445f6464f..aaae6b11e6 100644 --- a/protocol/transport/whisper/envelopes_test.go +++ b/protocol/transport/whisper/envelopes_test.go @@ -11,6 +11,7 @@ import ( "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/transport" ) var ( @@ -31,7 +32,7 @@ func TestEnvelopesMonitorSuite(t *testing.T) { func (s *EnvelopesMonitorSuite) SetupTest() { s.monitor = NewEnvelopesMonitor( nil, - EnvelopesMonitorConfig{ + transport.EnvelopesMonitorConfig{ EnvelopeEventsHandler: nil, MaxAttempts: 0, MailserverConfirmationsEnabled: false, diff --git a/services/shhext/api.go b/services/shhext/api.go index 84fb754968..2744f68ee0 100644 --- a/services/shhext/api.go +++ b/services/shhext/api.go @@ -15,7 +15,6 @@ import ( "github.com/status-im/status-go/db" "github.com/status-im/status-go/mailserver" - "github.com/status-im/status-go/params" "github.com/status-im/status-go/services/shhext/mailservers" "github.com/status-im/status-go/whisper/v6" @@ -657,7 +656,7 @@ func (api *PublicAPI) SetInstallationMetadata(installationID string, data *multi // VerifyENSNames takes a list of ensdetails and returns whether they match the public key specified func (api *PublicAPI) VerifyENSNames(details []enstypes.ENSDetails) (map[string]enstypes.ENSResponse, error) { - return api.service.messenger.VerifyENSNames(params.MainnetEthereumNetworkURL, ensContractAddress, details) + return api.service.messenger.VerifyENSNames(api.service.config.VerifyENSURL, ensContractAddress, details) } type ApplicationMessagesResponse struct { diff --git a/services/shhext/service.go b/services/shhext/service.go index f3dea0bf7d..8a61cf2c09 100644 --- a/services/shhext/service.go +++ b/services/shhext/service.go @@ -25,6 +25,7 @@ import ( "github.com/status-im/status-go/multiaccounts/accounts" "github.com/status-im/status-go/params" "github.com/status-im/status-go/services/shhext/mailservers" + "github.com/status-im/status-go/services/wallet" "github.com/status-im/status-go/signal" "github.com/syndtr/goleveldb/leveldb" @@ -194,26 +195,44 @@ type verifyTransactionClient struct { url string } -func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, bool, error) { +func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) { signer := gethtypes.NewEIP155Signer(c.chainID) client, err := ethclient.Dial(c.url) if err != nil { - return coretypes.Message{}, false, err + return coretypes.Message{}, coretypes.TransactionStatusPending, err } transaction, pending, err := client.TransactionByHash(ctx, commongethtypes.BytesToHash(hash.Bytes())) if err != nil { - return coretypes.Message{}, false, err + return coretypes.Message{}, coretypes.TransactionStatusPending, err } message, err := transaction.AsMessage(signer) if err != nil { - return coretypes.Message{}, false, err + return coretypes.Message{}, coretypes.TransactionStatusPending, err } from := types.BytesToAddress(message.From().Bytes()) to := types.BytesToAddress(message.To().Bytes()) - return coretypes.NewMessage( + if pending { + return coretypes.NewMessage( + from, + &to, + message.Nonce(), + message.Value(), + message.Gas(), + message.GasPrice(), + message.Data(), + message.CheckNonce(), + ), coretypes.TransactionStatusPending, nil + } + + receipt, err := client.TransactionReceipt(ctx, commongethtypes.BytesToHash(hash.Bytes())) + if err != nil { + return coretypes.Message{}, coretypes.TransactionStatusPending, err + } + + coremessage := coretypes.NewMessage( from, &to, message.Nonce(), @@ -222,7 +241,20 @@ func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash ty message.GasPrice(), message.Data(), message.CheckNonce(), - ), pending, nil + ) + + // Token transfer, check the logs + if len(coremessage.Data()) != 0 { + if wallet.IsTokenTransfer(receipt.Logs) { + return coremessage, coretypes.TransactionStatus(receipt.Status), nil + } else { + return coremessage, coretypes.TransactionStatusFailed, nil + } + + } + + return coremessage, coretypes.TransactionStatus(receipt.Status), nil + } func (s *Service) verifyTransactionLoop(tick time.Duration, cancel <-chan struct{}) { diff --git a/services/wallet/downloader.go b/services/wallet/downloader.go index f97c3ba94f..5036f05a24 100644 --- a/services/wallet/downloader.go +++ b/services/wallet/downloader.go @@ -111,7 +111,7 @@ func (d *ETHTransferDownloader) getTransfersInBlock(ctx context.Context, blk *ty if err != nil { return nil, err } - if isTokenTransfer(receipt.Logs) { + if IsTokenTransfer(receipt.Logs) { log.Debug("eth downloader found token transfer", "hash", tx.Hash()) continue } @@ -307,7 +307,7 @@ func (d *ERC20TransfersDownloader) GetTransfersInRange(parent context.Context, f return transfers, nil } -func isTokenTransfer(logs []*types.Log) bool { +func IsTokenTransfer(logs []*types.Log) bool { signature := crypto.Keccak256Hash([]byte(erc20TransferEventSignature)) for _, l := range logs { if len(l.Topics) > 0 && l.Topics[0] == signature { diff --git a/vendor/github.com/status-im/status-go/eth-node/core/types/transaction.go b/vendor/github.com/status-im/status-go/eth-node/core/types/transaction.go index 6bd2fee2c9..d6c3c5d83e 100644 --- a/vendor/github.com/status-im/status-go/eth-node/core/types/transaction.go +++ b/vendor/github.com/status-im/status-go/eth-node/core/types/transaction.go @@ -6,6 +6,14 @@ import ( "github.com/status-im/status-go/eth-node/types" ) +type TransactionStatus uint64 + +const ( + TransactionStatusFailed = 0 + TransactionStatusSuccess = 1 + TransactionStatusPending = 2 +) + type Message struct { to *types.Address from types.Address diff --git a/vendor/github.com/status-im/status-go/protocol/contact.go b/vendor/github.com/status-im/status-go/protocol/contact.go index 6f42f6be6a..fdbe8158ed 100644 --- a/vendor/github.com/status-im/status-go/protocol/contact.go +++ b/vendor/github.com/status-im/status-go/protocol/contact.go @@ -54,7 +54,7 @@ type Contact struct { SystemTags []string `json:"systemTags"` DeviceInfo []ContactDeviceInfo `json:"deviceInfo"` - TributeToTalk string `json:"tributeToTalk"` + TributeToTalk string `json:"tributeToTalk,omitEmpty"` } func (c Contact) PublicKey() (*ecdsa.PublicKey, error) { diff --git a/vendor/github.com/status-im/status-go/protocol/message_handler.go b/vendor/github.com/status-im/status-go/protocol/message_handler.go index 8324ea0ffb..5e0e286931 100644 --- a/vendor/github.com/status-im/status-go/protocol/message_handler.go +++ b/vendor/github.com/status-im/status-go/protocol/message_handler.go @@ -419,7 +419,7 @@ func (m *MessageHandler) HandleAcceptRequestAddressForTransaction(messageState * initialMessage.CommandParameters.CommandState = CommandStateRequestAddressForTransactionAccepted // Hide previous message - previousMessage, err := m.persistence.MessageByCommandID(command.Id) + previousMessage, err := m.persistence.MessageByCommandID(messageState.CurrentMessageState.Contact.ID, command.Id) if err != nil && err != errRecordNotFound { return err } diff --git a/vendor/github.com/status-im/status-go/protocol/messenger.go b/vendor/github.com/status-im/status-go/protocol/messenger.go index 30db150dd0..1a5d5871d1 100644 --- a/vendor/github.com/status-im/status-go/protocol/messenger.go +++ b/vendor/github.com/status-im/status-go/protocol/messenger.go @@ -678,6 +678,11 @@ func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string, response.Chats = []*Chat{&chat} response.Messages = buildSystemMessages(chat.MembershipUpdates, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } + return &response, m.saveChat(&chat) } @@ -737,6 +742,11 @@ func (m *Messenger) RemoveMemberFromGroupChat(ctx context.Context, chatID string chat.updateChatFromProtocolGroup(group) response.Chats = []*Chat{chat} response.Messages = buildSystemMessages(chat.MembershipUpdates, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } + return &response, m.saveChat(chat) } @@ -796,6 +806,10 @@ func (m *Messenger) AddMembersToGroupChat(ctx context.Context, chatID string, me response.Chats = []*Chat{chat} response.Messages = buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } return &response, m.saveChat(chat) } @@ -857,6 +871,10 @@ func (m *Messenger) AddAdminsToGroupChat(ctx context.Context, chatID string, mem response.Chats = []*Chat{chat} response.Messages = buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } return &response, m.saveChat(chat) } @@ -920,6 +938,10 @@ func (m *Messenger) ConfirmJoiningGroup(ctx context.Context, chatID string) (*Me response.Chats = []*Chat{chat} response.Messages = buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } return &response, m.saveChat(chat) } @@ -985,6 +1007,11 @@ func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string) (*Messeng response.Chats = []*Chat{chat} response.Messages = buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations) + err = m.persistence.SaveMessagesLegacy(response.Messages) + if err != nil { + return nil, err + } + return &response, m.saveChat(chat) } @@ -2042,6 +2069,7 @@ func (m *Messenger) VerifyENSNames(rpcEndpoint, contractAddress string, ensDetai contact.ENSVerifiedAt = details.VerifiedAt contact.Name = details.Name + m.allContacts[contact.ID] = contact contacts = append(contacts, contact) } else { m.logger.Warn("Failed to resolve ens name", @@ -2254,7 +2282,7 @@ func (m *Messenger) AcceptRequestAddressForTransaction(ctx context.Context, mess message.OutgoingStatus = OutgoingStatusSending // Hide previous message - previousMessage, err := m.persistence.MessageByCommandID(messageID) + previousMessage, err := m.persistence.MessageByCommandID(chatID, messageID) if err != nil { return nil, err } @@ -2513,7 +2541,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas message.OutgoingStatus = OutgoingStatusSending // Hide previous message - previousMessage, err := m.persistence.MessageByCommandID(messageID) + previousMessage, err := m.persistence.MessageByCommandID(chatID, messageID) if err != nil && err != errRecordNotFound { return nil, err } @@ -2734,18 +2762,20 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. return nil, err } - // Hide previous message - previousMessage, err := m.persistence.MessageByCommandID(message.CommandParameters.ID) - if err != nil && err != errRecordNotFound { - return nil, err - } - - if previousMessage != nil { - err = m.persistence.HideMessage(previousMessage.ID) - if err != nil { + if len(message.CommandParameters.ID) != 0 { + // Hide previous message + previousMessage, err := m.persistence.MessageByCommandID(chatID, message.CommandParameters.ID) + if err != nil && err != errRecordNotFound { return nil, err } - message.Replace = previousMessage.ID + + if previousMessage != nil { + err = m.persistence.HideMessage(previousMessage.ID) + if err != nil { + return nil, err + } + message.Replace = previousMessage.ID + } } response.Messages = append(response.Messages, message) diff --git a/vendor/github.com/status-im/status-go/protocol/persistence_legacy.go b/vendor/github.com/status-im/status-go/protocol/persistence_legacy.go index ed4364c4a5..95cb7a31c5 100644 --- a/vendor/github.com/status-im/status-go/protocol/persistence_legacy.go +++ b/vendor/github.com/status-im/status-go/protocol/persistence_legacy.go @@ -237,7 +237,7 @@ func (db sqlitePersistence) messageByID(tx *sql.Tx, id string) (*Message, error) } } -func (db sqlitePersistence) MessageByCommandID(id string) (*Message, error) { +func (db sqlitePersistence) MessageByCommandID(chatID, id string) (*Message, error) { var message Message @@ -259,10 +259,13 @@ func (db sqlitePersistence) MessageByCommandID(id string) (*Message, error) { m1.source = c.id WHERE m1.command_id = ? + AND + m1.local_chat_id = ? ORDER BY m1.clock_value DESC LIMIT 1 `, allFields), id, + chatID, ) err := db.tableUserMessagesLegacyScanAllFields(row, &message) switch err { diff --git a/vendor/github.com/status-im/status-go/protocol/transaction_validator.go b/vendor/github.com/status-im/status-go/protocol/transaction_validator.go index b4160f895a..e19d72caa3 100644 --- a/vendor/github.com/status-im/status-go/protocol/transaction_validator.go +++ b/vendor/github.com/status-im/status-go/protocol/transaction_validator.go @@ -62,7 +62,7 @@ func NewTransactionValidator(addresses []types.Address, persistence *sqlitePersi } type EthClient interface { - TransactionByHash(context.Context, types.Hash) (coretypes.Message, bool, error) + TransactionByHash(context.Context, types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) } func (t *TransactionValidator) verifyTransactionSignature(ctx context.Context, from *ecdsa.PublicKey, address types.Address, transactionHash string, signature []byte) error { @@ -243,7 +243,8 @@ func (t *TransactionValidator) ValidateTransactions(ctx context.Context) ([]*Ver var validationResult *VerifyTransactionResponse t.logger.Debug("Validating transaction", zap.Any("transaction", transaction)) if transaction.CommandID != "" { - message, err := t.persistence.MessageByCommandID(transaction.CommandID) + chatID := contactIDFromPublicKey(transaction.From) + message, err := t.persistence.MessageByCommandID(chatID, transaction.CommandID) if err != nil { t.logger.Error("error pulling message", zap.Error(err)) @@ -313,13 +314,17 @@ func (t *TransactionValidator) ValidateTransaction(ctx context.Context, paramete c, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - message, pending, err := t.client.TransactionByHash(c, types.HexToHash(hash)) + message, status, err := t.client.TransactionByHash(c, types.HexToHash(hash)) if err != nil { return nil, err } - if pending { + switch status { + case coretypes.TransactionStatusPending: t.logger.Debug("Transaction pending") return &VerifyTransactionResponse{Pending: true}, nil + case coretypes.TransactionStatusFailed: + + return invalidResponse, nil } return t.validateTransaction(ctx, message, parameters, from) diff --git a/vendor/github.com/status-im/status-go/protocol/transport/transport.go b/vendor/github.com/status-im/status-go/protocol/transport/transport.go index ea082f4ed9..9ec4c55dbd 100644 --- a/vendor/github.com/status-im/status-go/protocol/transport/transport.go +++ b/vendor/github.com/status-im/status-go/protocol/transport/transport.go @@ -33,6 +33,7 @@ type Transport interface { LoadFilters(filters []*Filter) ([]*Filter, error) RemoveFilters(filters []*Filter) error ResetFilters() error + Filters() []*Filter ProcessNegotiatedSecret(secret types.NegotiatedSecret) (*Filter, error) RetrieveRawAll() (map[Filter][]*types.Message, error) } From 6537cae60668b008484fd2a8808a179724099935 Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro Date: Wed, 27 Nov 2019 13:22:23 +0100 Subject: [PATCH 2/9] Nimbus node support --- Makefile | 14 +- account/accounts_geth.go | 4 +- account/accounts_nimbus.go | 13 +- account/accounts_test.go | 6 +- api/geth_backend.go | 6 +- api/nimbus_backend.go | 1027 +++++++++++++++++ eth-node/bridge/geth/node.go | 4 + eth-node/bridge/geth/whisper.go | 5 + eth-node/bridge/nimbus/build-nimbus.sh | 10 +- eth-node/bridge/nimbus/node.go | 4 + eth-node/bridge/nimbus/whisper.go | 10 + eth-node/types/whisper.go | 2 + go.mod | 1 + go.sum | 54 +- mobile/status.go | 82 +- mobile/status_geth.go | 9 + mobile/status_nimbus.go | 9 + node/{status_node.go => get_status_node.go} | 2 + node/{node.go => geth_node.go} | 4 +- ...node_api_test.go => geth_node_api_test.go} | 0 node/{node_test.go => geth_node_test.go} | 2 + ..._node_test.go => geth_status_node_test.go} | 0 node/nimbus_node.go | 392 +++++++ node/nimbus_status_node.go | 798 +++++++++++++ rpc/client.go | 4 + services/accounts/service_nimbus.go | 15 + services/nimbus/service.go | 84 ++ services/nodebridge/node_service_nimbus.go | 14 + services/nodebridge/whisper_service_nimbus.go | 14 + services/rpcfilters/service_nimbus.go | 15 + services/shhext/api.go | 684 +---------- services/shhext/api_geth.go | 676 +++++++++++ .../shhext/{api_test.go => api_geth_test.go} | 2 + services/shhext/api_nimbus.go | 661 +++++++++++ services/shhext/context.go | 5 - services/shhext/context_geth.go | 14 + services/shhext/history.go | 331 ------ services/shhext/history_geth.go | 340 ++++++ .../{history_test.go => history_geth_test.go} | 2 + services/shhext/mailrequests.go | 2 + services/shhext/mailrequests_test.go | 2 + services/shhext/service.go | 2 + services/shhext/service_nimbus.go | 448 +++++++ services/status/service_nimbus.go | 16 + services/subscriptions/service.go | 2 +- services/subscriptions/service_nimbus.go | 15 + timesource/timesource.go | 4 + vendor/github.com/mattn/go-pointer/LICENSE | 21 + vendor/github.com/mattn/go-pointer/README.md | 29 + .../mattn/go-pointer/_example/callback.h | 9 + vendor/github.com/mattn/go-pointer/doc.go | 1 + vendor/github.com/mattn/go-pointer/pointer.go | 57 + .../status-go/eth-node/bridge/geth/node.go | 4 + .../status-go/eth-node/bridge/geth/whisper.go | 5 + .../eth-node/bridge/nimbus/build-nimbus.sh | 40 + .../eth-node/bridge/nimbus/cfuncs.go | 16 + .../eth-node/bridge/nimbus/filter.go | 61 + .../status-go/eth-node/bridge/nimbus/node.go | 159 +++ .../bridge/nimbus/public_whisper_api.go | 212 ++++ .../eth-node/bridge/nimbus/routine_queue.go | 64 + .../eth-node/bridge/nimbus/whisper.go | 431 +++++++ .../status-go/eth-node/types/whisper.go | 2 + vendor/golang.org/x/tools/go/analysis/doc.go | 75 +- vendor/golang.org/x/tools/go/packages/doc.go | 3 +- .../x/tools/go/packages/external.go | 7 +- .../golang.org/x/tools/go/packages/golist.go | 169 +-- .../x/tools/go/packages/loadmode_string.go | 57 + .../x/tools/go/packages/packages.go | 4 +- .../x/tools/internal/gopathwalk/walk.go | 5 +- .../golang.org/x/tools/internal/span/parse.go | 100 -- .../golang.org/x/tools/internal/span/span.go | 285 ----- .../golang.org/x/tools/internal/span/token.go | 151 --- .../x/tools/internal/span/token111.go | 39 - .../x/tools/internal/span/token112.go | 16 - .../golang.org/x/tools/internal/span/uri.go | 152 --- .../golang.org/x/tools/internal/span/utf16.go | 94 -- vendor/modules.txt | 6 +- 77 files changed, 5940 insertions(+), 2144 deletions(-) create mode 100644 api/nimbus_backend.go create mode 100644 mobile/status_geth.go create mode 100644 mobile/status_nimbus.go rename node/{status_node.go => get_status_node.go} (99%) rename node/{node.go => geth_node.go} (99%) rename node/{node_api_test.go => geth_node_api_test.go} (100%) rename node/{node_test.go => geth_node_test.go} (99%) rename node/{status_node_test.go => geth_status_node_test.go} (100%) create mode 100644 node/nimbus_node.go create mode 100644 node/nimbus_status_node.go create mode 100644 services/accounts/service_nimbus.go create mode 100644 services/nimbus/service.go create mode 100644 services/nodebridge/node_service_nimbus.go create mode 100644 services/nodebridge/whisper_service_nimbus.go create mode 100644 services/rpcfilters/service_nimbus.go create mode 100644 services/shhext/api_geth.go rename services/shhext/{api_test.go => api_geth_test.go} (99%) create mode 100644 services/shhext/api_nimbus.go create mode 100644 services/shhext/context_geth.go create mode 100644 services/shhext/history_geth.go rename services/shhext/{history_test.go => history_geth_test.go} (99%) create mode 100644 services/shhext/service_nimbus.go create mode 100644 services/status/service_nimbus.go create mode 100644 services/subscriptions/service_nimbus.go create mode 100644 vendor/github.com/mattn/go-pointer/LICENSE create mode 100644 vendor/github.com/mattn/go-pointer/README.md create mode 100644 vendor/github.com/mattn/go-pointer/_example/callback.h create mode 100644 vendor/github.com/mattn/go-pointer/doc.go create mode 100644 vendor/github.com/mattn/go-pointer/pointer.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/build-nimbus.sh create mode 100644 vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/cfuncs.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/filter.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/node.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/public_whisper_api.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/routine_queue.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/whisper.go create mode 100644 vendor/golang.org/x/tools/go/packages/loadmode_string.go delete mode 100644 vendor/golang.org/x/tools/internal/span/parse.go delete mode 100644 vendor/golang.org/x/tools/internal/span/span.go delete mode 100644 vendor/golang.org/x/tools/internal/span/token.go delete mode 100644 vendor/golang.org/x/tools/internal/span/token111.go delete mode 100644 vendor/golang.org/x/tools/internal/span/token112.go delete mode 100644 vendor/golang.org/x/tools/internal/span/uri.go delete mode 100644 vendor/golang.org/x/tools/internal/span/utf16.go diff --git a/Makefile b/Makefile index 97fe08c0ef..760a06b921 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,15 @@ HELP_FUN = \ print "\n"; \ } +nimbus: ##@build Build Nimbus + ./eth-node/bridge/nimbus/build-nimbus.sh + +nimbus-statusgo: nimbus ##@build Build status-go (based on Nimbus node) as statusd server + C_INCLUDE_PATH="./eth-node/bridge/nimbus" go build -mod=vendor -i -o $(GOBIN)/statusd -v -tags '$(BUILD_TAGS) nimbus' $(BUILD_FLAGS) ./cmd/statusd && \ + cp vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/libnimbus.so $(GOBIN) + @echo "Compilation done." + @echo "Run \"build/bin/statusd -h\" to view available commands." + statusgo: ##@build Build status-go as statusd server go build -i -o $(GOBIN)/statusd -v -tags '$(BUILD_TAGS)' $(BUILD_FLAGS) ./cmd/statusd @echo "Compilation done." @@ -93,7 +102,7 @@ statusgo-cross: statusgo-android statusgo-ios statusgo-android: ##@cross-compile Build status-go for Android @echo "Building status-go for Android..." gomobile init - gomobile bind -target=android -ldflags="-s -w" $(BUILD_FLAGS_MOBILE) -o build/bin/statusgo.aar github.com/status-im/status-go/mobile + gomobile bind -v -target=android -ldflags="-s -w" $(BUILD_FLAGS_MOBILE) -o build/bin/statusgo.aar github.com/status-im/status-go/mobile @echo "Android cross compilation done in build/bin/statusgo.aar" statusgo-ios: ##@cross-compile Build status-go for iOS @@ -282,7 +291,8 @@ ci: lint canary-test test-unit test-e2e ##@tests Run all linters and tests at on ci-race: lint canary-test test-unit test-e2e-race ##@tests Run all linters and tests at once + race clean: ##@other Cleanup - rm -fr build/bin/* mailserver-config.json + rm -fr build/bin/* mailserver-config.json vendor/github.com/status-im/nimbus + git clean -xf deep-clean: clean rm -Rdf .ethereumtest/StatusChain diff --git a/account/accounts_geth.go b/account/accounts_geth.go index 59031a69b5..e6cf27d689 100644 --- a/account/accounts_geth.go +++ b/account/accounts_geth.go @@ -15,8 +15,8 @@ type GethManager struct { gethAccManager *accounts.Manager } -// NewManager returns new node account manager. -func NewManager() *GethManager { +// NewGethManager returns new node account manager. +func NewGethManager() *GethManager { m := &GethManager{} m.Manager = &Manager{accountsGenerator: generator.New(m)} return m diff --git a/account/accounts_nimbus.go b/account/accounts_nimbus.go index 082237213e..b3ceb62ead 100644 --- a/account/accounts_nimbus.go +++ b/account/accounts_nimbus.go @@ -6,10 +6,15 @@ import ( "github.com/status-im/status-go/account/generator" ) -// NewManager returns new node account manager. -func NewManager() *Manager { - m := &Manager{} - m.accountsGenerator = generator.New(m) +// NimbusManager represents account manager interface. +type NimbusManager struct { + *Manager +} + +// NewNimbusManager returns new node account manager. +func NewNimbusManager() *NimbusManager { + m := &NimbusManager{} + m.Manager = &Manager{accountsGenerator: generator.New(m)} return m } diff --git a/account/accounts_test.go b/account/accounts_test.go index ec6c36f990..cb09199a44 100644 --- a/account/accounts_test.go +++ b/account/accounts_test.go @@ -18,7 +18,7 @@ import ( ) func TestVerifyAccountPassword(t *testing.T) { - accManager := NewManager() + accManager := NewGethManager() keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts") require.NoError(t, err) defer os.RemoveAll(keyStoreDir) //nolint: errcheck @@ -106,7 +106,7 @@ func TestVerifyAccountPasswordWithAccountBeforeEIP55(t *testing.T) { err = utils.ImportTestAccount(keyStoreDir, "test-account3-before-eip55.pk") require.NoError(t, err) - accManager := NewManager() + accManager := NewGethManager() address := types.HexToAddress(utils.TestConfig.Account3.WalletAddress) _, err = accManager.VerifyAccountPassword(keyStoreDir, address.Hex(), utils.TestConfig.Account3.Password) @@ -136,7 +136,7 @@ type testAccount struct { // SetupTest is used here for reinitializing the mock before every // test function to avoid faulty execution. func (s *ManagerTestSuite) SetupTest() { - s.accManager = NewManager() + s.accManager = NewGethManager() keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts") s.Require().NoError(err) diff --git a/api/geth_backend.go b/api/geth_backend.go index 5eb6b1c8b6..5ab90c5886 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -1,3 +1,5 @@ +// +build !nimbus + package api import ( @@ -82,10 +84,10 @@ type GethStatusBackend struct { // NewGethStatusBackend create a new GethStatusBackend instance func NewGethStatusBackend() *GethStatusBackend { - defer log.Info("Status backend initialized", "version", params.Version, "commit", params.GitCommit) + defer log.Info("Status backend initialized", "backend", "geth", "version", params.Version, "commit", params.GitCommit) statusNode := node.New() - accountManager := account.NewManager() + accountManager := account.NewGethManager() transactor := transactions.NewTransactor() personalAPI := personal.NewAPI() rpcFilters := rpcfilters.New(statusNode) diff --git a/api/nimbus_backend.go b/api/nimbus_backend.go new file mode 100644 index 0000000000..d18d18449b --- /dev/null +++ b/api/nimbus_backend.go @@ -0,0 +1,1027 @@ +// +build nimbus + +package api + +import ( + "context" + "database/sql" + "fmt" + "math/big" + "path/filepath" + "sync" + "time" + + "github.com/pkg/errors" + + "github.com/ethereum/go-ethereum/event" + + "github.com/ethereum/go-ethereum/log" + + "github.com/status-im/status-go/account" + "github.com/status-im/status-go/appdatabase" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/logutils" + "github.com/status-im/status-go/multiaccounts" + "github.com/status-im/status-go/multiaccounts/accounts" + "github.com/status-im/status-go/node" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/rpc" + accountssvc "github.com/status-im/status-go/services/accounts" + nimbussvc "github.com/status-im/status-go/services/nimbus" + "github.com/status-im/status-go/services/personal" + "github.com/status-im/status-go/services/rpcfilters" + "github.com/status-im/status-go/services/subscriptions" + "github.com/status-im/status-go/services/typeddata" + "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/transactions" +) + +// const ( +// contractQueryTimeout = 1000 * time.Millisecond +// ) + +var ( + // ErrWhisperClearIdentitiesFailure clearing whisper identities has failed. + ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities") + // ErrWhisperIdentityInjectionFailure injecting whisper identities has failed. + ErrWhisperIdentityInjectionFailure = errors.New("failed to inject identity into Whisper") + // ErrUnsupportedRPCMethod is for methods not supported by the RPC interface + ErrUnsupportedRPCMethod = errors.New("method is unsupported by RPC interface") + // ErrRPCClientUnavailable is returned if an RPC client can't be retrieved. + // This is a normal situation when a node is stopped. + ErrRPCClientUnavailable = errors.New("JSON-RPC client is unavailable") +) + +var _ StatusBackend = (*nimbusStatusBackend)(nil) + +// nimbusStatusBackend implements the Status.im service over Nimbus +type nimbusStatusBackend struct { + StatusBackend + + mu sync.Mutex + // rootDataDir is the same for all networks. + rootDataDir string + appDB *sql.DB + statusNode *node.NimbusStatusNode + // personalAPI *personal.PublicAPI + // rpcFilters *rpcfilters.Service + multiaccountsDB *multiaccounts.Database + accountManager *account.GethManager + // transactor *transactions.Transactor + connectionState connectionState + appState appState + selectedAccountShhKeyID string + log log.Logger + allowAllRPC bool // used only for tests, disables api method restrictions +} + +// NewNimbusStatusBackend create a new nimbusStatusBackend instance +func NewNimbusStatusBackend() *nimbusStatusBackend { + defer log.Info("Status backend initialized", "backend", "nimbus", "version", params.Version, "commit", params.GitCommit) + + statusNode := node.NewNimbus() + accountManager := account.NewGethManager() + // transactor := transactions.NewTransactor() + // personalAPI := personal.NewAPI() + // rpcFilters := rpcfilters.New(statusNode) + return &nimbusStatusBackend{ + statusNode: statusNode, + accountManager: accountManager, + // transactor: transactor, + // personalAPI: personalAPI, + // rpcFilters: rpcFilters, + log: log.New("package", "status-go/api.nimbusStatusBackend"), + } +} + +// StatusNode returns reference to node manager +func (b *nimbusStatusBackend) StatusNode() *node.NimbusStatusNode { + return b.statusNode +} + +// AccountManager returns reference to account manager +func (b *nimbusStatusBackend) AccountManager() *account.GethManager { + return b.accountManager +} + +// // Transactor returns reference to a status transactor +// func (b *nimbusStatusBackend) Transactor() *transactions.Transactor { +// return b.transactor +// } + +// SelectedAccountShhKeyID returns a Whisper key ID of the selected chat key pair. +func (b *nimbusStatusBackend) SelectedAccountShhKeyID() string { + return b.selectedAccountShhKeyID +} + +// IsNodeRunning confirm that node is running +func (b *nimbusStatusBackend) IsNodeRunning() bool { + return b.statusNode.IsRunning() +} + +// StartNode start Status node, fails if node is already started +func (b *nimbusStatusBackend) StartNode(config *params.NodeConfig) error { + b.mu.Lock() + defer b.mu.Unlock() + if err := b.startNode(config); err != nil { + signal.SendNodeCrashed(err) + return err + } + return nil +} + +func (b *nimbusStatusBackend) UpdateRootDataDir(datadir string) { + b.mu.Lock() + defer b.mu.Unlock() + b.rootDataDir = datadir +} + +func (b *nimbusStatusBackend) OpenAccounts() error { + b.mu.Lock() + defer b.mu.Unlock() + if b.multiaccountsDB != nil { + return nil + } + db, err := multiaccounts.InitializeDB(filepath.Join(b.rootDataDir, "accounts.sql")) + if err != nil { + return err + } + b.multiaccountsDB = db + return nil +} + +func (b *nimbusStatusBackend) GetAccounts() ([]multiaccounts.Account, error) { + b.mu.Lock() + defer b.mu.Unlock() + if b.multiaccountsDB == nil { + return nil, errors.New("accounts db wasn't initialized") + } + return b.multiaccountsDB.GetAccounts() +} + +func (b *nimbusStatusBackend) SaveAccount(account multiaccounts.Account) error { + b.mu.Lock() + defer b.mu.Unlock() + if b.multiaccountsDB == nil { + return errors.New("accounts db wasn't initialized") + } + return b.multiaccountsDB.SaveAccount(account) +} + +func (b *nimbusStatusBackend) ensureAppDBOpened(account multiaccounts.Account, password string) (err error) { + b.mu.Lock() + defer b.mu.Unlock() + if b.appDB != nil { + return nil + } + if len(b.rootDataDir) == 0 { + return errors.New("root datadir wasn't provided") + } + path := filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", account.KeyUID)) + b.appDB, err = appdatabase.InitializeDB(path, password) + if err != nil { + return err + } + return nil +} + +// StartNodeWithKey instead of loading addresses from database this method derives address from key +// and uses it in application. +// TODO: we should use a proper struct with optional values instead of duplicating the regular functions +// with small variants for keycard, this created too many bugs +func (b *nimbusStatusBackend) startNodeWithKey(acc multiaccounts.Account, password string, keyHex string) error { + err := b.ensureAppDBOpened(acc, password) + if err != nil { + return err + } + conf, err := b.loadNodeConfig() + if err != nil { + return err + } + if err := logutils.OverrideRootLogWithConfig(conf, false); err != nil { + return err + } + accountsDB := accounts.NewDB(b.appDB) + walletAddr, err := accountsDB.GetWalletAddress() + if err != nil { + return err + } + watchAddrs, err := accountsDB.GetAddresses() + if err != nil { + return err + } + chatKey, err := crypto.HexToECDSA(keyHex) + if err != nil { + return err + } + err = b.StartNode(conf) + if err != nil { + return err + } + b.accountManager.SetChatAccount(chatKey) + _, err = b.accountManager.SelectedChatAccount() + if err != nil { + return err + } + b.accountManager.SetAccountAddresses(walletAddr, watchAddrs...) + err = b.injectAccountIntoServices() + if err != nil { + return err + } + err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix()) + if err != nil { + return err + } + return nil +} + +func (b *nimbusStatusBackend) StartNodeWithKey(acc multiaccounts.Account, password string, keyHex string) error { + err := b.startNodeWithKey(acc, password, keyHex) + if err != nil { + // Stop node for clean up + _ = b.StopNode() + } + signal.SendLoggedIn(err) + return err +} + +func (b *nimbusStatusBackend) startNodeWithAccount(acc multiaccounts.Account, password string) error { + err := b.ensureAppDBOpened(acc, password) + if err != nil { + return err + } + conf, err := b.loadNodeConfig() + if err != nil { + return err + } + if err := logutils.OverrideRootLogWithConfig(conf, false); err != nil { + return err + } + accountsDB := accounts.NewDB(b.appDB) + chatAddr, err := accountsDB.GetChatAddress() + if err != nil { + return err + } + walletAddr, err := accountsDB.GetWalletAddress() + if err != nil { + return err + } + watchAddrs, err := accountsDB.GetAddresses() + if err != nil { + return err + } + login := account.LoginParams{ + Password: password, + ChatAddress: chatAddr, + WatchAddresses: watchAddrs, + MainAccount: walletAddr, + } + err = b.StartNode(conf) + if err != nil { + return err + } + err = b.SelectAccount(login) + if err != nil { + return err + } + err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix()) + if err != nil { + return err + } + return nil +} + +func (b *nimbusStatusBackend) StartNodeWithAccount(acc multiaccounts.Account, password string) error { + err := b.startNodeWithAccount(acc, password) + if err != nil { + // Stop node for clean up + _ = b.StopNode() + } + signal.SendLoggedIn(err) + return err +} + +func (b *nimbusStatusBackend) SaveAccountAndStartNodeWithKey(acc multiaccounts.Account, password string, settings accounts.Settings, nodecfg *params.NodeConfig, subaccs []accounts.Account, keyHex string) error { + err := b.SaveAccount(acc) + if err != nil { + return err + } + err = b.ensureAppDBOpened(acc, password) + if err != nil { + return err + } + err = b.saveAccountsAndSettings(settings, nodecfg, subaccs) + if err != nil { + return err + } + return b.StartNodeWithKey(acc, password, keyHex) +} + +// StartNodeWithAccountAndConfig is used after account and config was generated. +// In current setup account name and config is generated on the client side. Once/if it will be generated on +// status-go side this flow can be simplified. +func (b *nimbusStatusBackend) StartNodeWithAccountAndConfig( + account multiaccounts.Account, + password string, + settings accounts.Settings, + nodecfg *params.NodeConfig, + subaccs []accounts.Account, +) error { + err := b.SaveAccount(account) + if err != nil { + return err + } + err = b.ensureAppDBOpened(account, password) + if err != nil { + return err + } + err = b.saveAccountsAndSettings(settings, nodecfg, subaccs) + if err != nil { + return err + } + return b.StartNodeWithAccount(account, password) +} + +func (b *nimbusStatusBackend) saveAccountsAndSettings(settings accounts.Settings, nodecfg *params.NodeConfig, subaccs []accounts.Account) error { + b.mu.Lock() + defer b.mu.Unlock() + accdb := accounts.NewDB(b.appDB) + err := accdb.CreateSettings(settings, *nodecfg) + if err != nil { + return err + } + return accdb.SaveAccounts(subaccs) +} + +func (b *nimbusStatusBackend) loadNodeConfig() (*params.NodeConfig, error) { + b.mu.Lock() + defer b.mu.Unlock() + var conf params.NodeConfig + err := accounts.NewDB(b.appDB).GetNodeConfig(&conf) + if err != nil { + return nil, err + } + // NodeConfig.Version should be taken from params.Version + // which is set at the compile time. + // What's cached is usually outdated so we overwrite it here. + conf.Version = params.Version + return &conf, nil +} + +func (b *nimbusStatusBackend) rpcFiltersService() nimbussvc.ServiceConstructor { + return func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { + return rpcfilters.New(b.statusNode), nil + } +} + +func (b *nimbusStatusBackend) subscriptionService() nimbussvc.ServiceConstructor { + return func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { + return subscriptions.New(b.statusNode), nil + } +} + +func (b *nimbusStatusBackend) accountsService(accountsFeed *event.Feed) nimbussvc.ServiceConstructor { + return func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { + return accountssvc.NewService(accounts.NewDB(b.appDB), b.multiaccountsDB, b.accountManager.Manager, accountsFeed), nil + } +} + +// func (b *nimbusStatusBackend) browsersService() nimbussvc.ServiceConstructor { +// return func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { +// return browsers.NewService(browsers.NewDB(b.appDB)), nil +// } +// } + +// func (b *nimbusStatusBackend) permissionsService() nimbussvc.ServiceConstructor { +// return func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { +// return permissions.NewService(permissions.NewDB(b.appDB)), nil +// } +// } + +// func (b *nimbusStatusBackend) mailserversService() nimbussvc.ServiceConstructor { +// return func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { +// return mailservers.NewService(mailservers.NewDB(b.appDB)), nil +// } +// } + +// func (b *nimbusStatusBackend) walletService(network uint64, accountsFeed *event.Feed) nimbussvc.ServiceConstructor { +// return func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { +// return wallet.NewService(wallet.NewDB(b.appDB, network), accountsFeed), nil +// } +// } + +func (b *nimbusStatusBackend) startNode(config *params.NodeConfig) (err error) { + // defer func() { + // if r := recover(); r != nil { + // err = fmt.Errorf("node crashed on start: %v", err) + // } + // }() + + // Start by validating configuration + if err := config.Validate(); err != nil { + return err + } + + accountsFeed := &event.Feed{} + services := []nimbussvc.ServiceConstructor{} + services = appendIf(config.UpstreamConfig.Enabled, services, b.rpcFiltersService()) + services = append(services, b.subscriptionService()) + services = appendIf(b.appDB != nil && b.multiaccountsDB != nil, services, b.accountsService(accountsFeed)) + // services = appendIf(config.BrowsersConfig.Enabled, services, b.browsersService()) + // services = appendIf(config.PermissionsConfig.Enabled, services, b.permissionsService()) + // services = appendIf(config.MailserversConfig.Enabled, services, b.mailserversService()) + // services = appendIf(config.WalletConfig.Enabled, services, b.walletService(config.NetworkID, accountsFeed)) + + // manager := b.accountManager.GetManager() + // if manager == nil { + // return errors.New("ethereum accounts.Manager is nil") + // } + if err = b.statusNode.StartWithOptions(config, node.NimbusStartOptions{ + Services: services, + // The peers discovery protocols are started manually after + // `node.ready` signal is sent. + // It was discussed in https://github.com/status-im/status-go/pull/1333. + StartDiscovery: false, + // AccountsManager: manager, + }); err != nil { + return + } + signal.SendNodeStarted() + + // b.transactor.SetNetworkID(config.NetworkID) + // b.transactor.SetRPC(b.statusNode.RPCClient(), rpc.DefaultCallTimeout) + // b.personalAPI.SetRPC(b.statusNode.RPCPrivateClient(), rpc.DefaultCallTimeout) + + if err = b.registerHandlers(); err != nil { + b.log.Error("Handler registration failed", "err", err) + return + } + b.log.Info("Handlers registered") + + if st, err := b.statusNode.StatusService(); err == nil { + st.SetAccountManager(b.accountManager) + } + + // if st, err := b.statusNode.PeerService(); err == nil { + // st.SetDiscoverer(b.StatusNode()) + // } + + // Handle a case when a node is stopped and resumed. + // If there is no account selected, an error is returned. + if _, err := b.accountManager.SelectedChatAccount(); err == nil { + if err := b.injectAccountIntoServices(); err != nil { + return err + } + } else if err != account.ErrNoAccountSelected { + return err + } + + signal.SendNodeReady() + + // if err := b.statusNode.StartDiscovery(); err != nil { + // return err + // } + + return nil +} + +// StopNode stop Status node. Stopped node cannot be resumed. +func (b *nimbusStatusBackend) StopNode() error { + b.mu.Lock() + defer b.mu.Unlock() + return b.stopNode() +} + +func (b *nimbusStatusBackend) stopNode() error { + if !b.IsNodeRunning() { + return node.ErrNoRunningNode + } + defer signal.SendNodeStopped() + return b.statusNode.Stop() +} + +// RestartNode restart running Status node, fails if node is not running +func (b *nimbusStatusBackend) RestartNode() error { + b.mu.Lock() + defer b.mu.Unlock() + + if !b.IsNodeRunning() { + return node.ErrNoRunningNode + } + + newcfg := *(b.statusNode.Config()) + if err := b.stopNode(); err != nil { + return err + } + return b.startNode(&newcfg) +} + +// ResetChainData remove chain data from data directory. +// Node is stopped, and new node is started, with clean data directory. +func (b *nimbusStatusBackend) ResetChainData() error { + panic("ResetChainData") + // b.mu.Lock() + // defer b.mu.Unlock() + // newcfg := *(b.statusNode.Config()) + // if err := b.stopNode(); err != nil { + // return err + // } + // // config is cleaned when node is stopped + // if err := b.statusNode.ResetChainData(&newcfg); err != nil { + // return err + // } + // signal.SendChainDataRemoved() + // return b.startNode(&newcfg) +} + +// CallRPC executes public RPC requests on node's in-proc RPC server. +func (b *nimbusStatusBackend) CallRPC(inputJSON string) (string, error) { + client := b.statusNode.RPCClient() + if client == nil { + return "", ErrRPCClientUnavailable + } + return client.CallRaw(inputJSON), nil +} + +// GetNodesFromContract returns a list of nodes from the contract +func (b *nimbusStatusBackend) GetNodesFromContract(rpcEndpoint string, contractAddress string) ([]string, error) { + panic("GetNodesFromContract") + // var response []string + + // ctx, cancel := context.WithTimeout(context.Background(), contractQueryTimeout) + // defer cancel() + + // ethclient, err := ethclient.DialContext(ctx, rpcEndpoint) + // if err != nil { + // return response, err + // } + + // contract, err := registry.NewNodes(types.HexToAddress(contractAddress), ethclient) + // if err != nil { + // return response, err + // } + + // nodeCount, err := contract.NodeCount(nil) + // if err != nil { + // return response, err + // } + + // one := big.NewInt(1) + // for i := big.NewInt(0); i.Cmp(nodeCount) < 0; i.Add(i, one) { + // node, err := contract.Nodes(nil, i) + // if err != nil { + // return response, err + // } + // response = append(response, node) + // } + + // return response, nil +} + +// CallPrivateRPC executes public and private RPC requests on node's in-proc RPC server. +func (b *nimbusStatusBackend) CallPrivateRPC(inputJSON string) (string, error) { + client := b.statusNode.RPCPrivateClient() + if client == nil { + return "", ErrRPCClientUnavailable + } + return client.CallRaw(inputJSON), nil +} + +// SendTransaction creates a new transaction and waits until it's complete. +func (b *nimbusStatusBackend) SendTransaction(sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error) { + panic("SendTransaction") + // verifiedAccount, err := b.getVerifiedWalletAccount(sendArgs.From.String(), password) + // if err != nil { + // return hash, err + // } + + // hash, err = b.transactor.SendTransaction(sendArgs, verifiedAccount) + // if err != nil { + // return + // } + + // go b.rpcFilters.TriggerTransactionSentToUpstreamEvent(hash) + + // return +} + +func (b *nimbusStatusBackend) SendTransactionWithSignature(sendArgs transactions.SendTxArgs, sig []byte) (hash types.Hash, err error) { + panic("SendTransactionWithSignature") + // hash, err = b.transactor.SendTransactionWithSignature(sendArgs, sig) + // if err != nil { + // return + // } + + // go b.rpcFilters.TriggerTransactionSentToUpstreamEvent(hash) + + // return +} + +// HashTransaction validate the transaction and returns new sendArgs and the transaction hash. +func (b *nimbusStatusBackend) HashTransaction(sendArgs transactions.SendTxArgs) (transactions.SendTxArgs, types.Hash, error) { + panic("HashTransaction") + // return b.transactor.HashTransaction(sendArgs) +} + +// SignMessage checks the pwd vs the selected account and passes on the signParams +// to personalAPI for message signature +func (b *nimbusStatusBackend) SignMessage(rpcParams personal.SignParams) (types.HexBytes, error) { + panic("SignMessage") + // verifiedAccount, err := b.getVerifiedWalletAccount(rpcParams.Address, rpcParams.Password) + // if err != nil { + // return types.Bytes{}, err + // } + // return b.personalAPI.Sign(rpcParams, verifiedAccount) +} + +// Recover calls the personalAPI to return address associated with the private +// key that was used to calculate the signature in the message +func (b *nimbusStatusBackend) Recover(rpcParams personal.RecoverParams) (types.Address, error) { + panic("Recover") + // return b.personalAPI.Recover(rpcParams) +} + +// SignTypedData accepts data and password. Gets verified account and signs typed data. +func (b *nimbusStatusBackend) SignTypedData(typed typeddata.TypedData, address string, password string) (types.HexBytes, error) { + account, err := b.getVerifiedWalletAccount(address, password) + if err != nil { + return types.HexBytes{}, err + } + chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID) + sig, err := typeddata.Sign(typed, account.AccountKey.PrivateKey, chain) + if err != nil { + return types.HexBytes{}, err + } + return types.HexBytes(sig), err +} + +// HashTypedData generates the hash of TypedData. +func (b *nimbusStatusBackend) HashTypedData(typed typeddata.TypedData) (types.Hash, error) { + chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID) + hash, err := typeddata.ValidateAndHash(typed, chain) + if err != nil { + return types.Hash{}, err + } + return types.Hash(hash), err +} + +func (b *nimbusStatusBackend) getVerifiedWalletAccount(address, password string) (*account.SelectedExtKey, error) { + config := b.StatusNode().Config() + + db := accounts.NewDB(b.appDB) + exists, err := db.AddressExists(types.HexToAddress(address)) + if err != nil { + b.log.Error("failed to query db for a given address", "address", address, "error", err) + return nil, err + } + + if !exists { + b.log.Error("failed to get a selected account", "err", transactions.ErrInvalidTxSender) + return nil, transactions.ErrAccountDoesntExist + } + + key, err := b.accountManager.VerifyAccountPassword(config.KeyStoreDir, address, password) + if err != nil { + b.log.Error("failed to verify account", "account", address, "error", err) + return nil, err + } + + return &account.SelectedExtKey{ + Address: key.Address, + AccountKey: key, + }, nil +} + +// registerHandlers attaches Status callback handlers to running node +func (b *nimbusStatusBackend) registerHandlers() error { + var clients []*rpc.Client + + if c := b.StatusNode().RPCClient(); c != nil { + clients = append(clients, c) + } else { + return errors.New("RPC client unavailable") + } + + if c := b.StatusNode().RPCPrivateClient(); c != nil { + clients = append(clients, c) + } else { + return errors.New("RPC private client unavailable") + } + + for _, client := range clients { + client.RegisterHandler( + params.AccountsMethodName, + func(context.Context, ...interface{}) (interface{}, error) { + return b.accountManager.Accounts() + }, + ) + + if b.allowAllRPC { + // this should only happen in unit-tests, this variable is not available outside this package + continue + } + client.RegisterHandler(params.SendTransactionMethodName, unsupportedMethodHandler) + client.RegisterHandler(params.PersonalSignMethodName, unsupportedMethodHandler) + client.RegisterHandler(params.PersonalRecoverMethodName, unsupportedMethodHandler) + } + + return nil +} + +func unsupportedMethodHandler(ctx context.Context, rpcParams ...interface{}) (interface{}, error) { + return nil, ErrUnsupportedRPCMethod +} + +// ConnectionChange handles network state changes logic. +func (b *nimbusStatusBackend) ConnectionChange(typ string, expensive bool) { + b.mu.Lock() + defer b.mu.Unlock() + + state := connectionState{ + Type: newConnectionType(typ), + Expensive: expensive, + } + if typ == none { + state.Offline = true + } + + b.log.Info("Network state change", "old", b.connectionState, "new", state) + + b.connectionState = state + + // logic of handling state changes here + // restart node? force peers reconnect? etc +} + +// AppStateChange handles app state changes (background/foreground). +// state values: see https://facebook.github.io/react-native/docs/appstate.html +func (b *nimbusStatusBackend) AppStateChange(state string) { + s, err := parseAppState(state) + if err != nil { + log.Error("AppStateChange failed, ignoring", "error", err) + return // and do nothing + } + + b.log.Info("App State changed", "new-state", s) + b.appState = s + + // TODO: put node in low-power mode if the app is in background (or inactive) + // and normal mode if the app is in foreground. +} + +// Logout clears whisper identities. +func (b *nimbusStatusBackend) Logout() error { + b.mu.Lock() + defer b.mu.Unlock() + + err := b.cleanupServices() + if err != nil { + return err + } + err = b.closeAppDB() + if err != nil { + return err + } + + b.accountManager.Logout() + + return nil +} + +// cleanupServices stops parts of services that doesn't managed by a node and removes injected data from services. +func (b *nimbusStatusBackend) cleanupServices() error { + whisperService, err := b.statusNode.WhisperService() + switch err { + case node.ErrServiceUnknown: // Whisper was never registered + case nil: + if err := whisperService.Whisper.DeleteKeyPairs(); err != nil { + return fmt.Errorf("%s: %v", ErrWhisperClearIdentitiesFailure, err) + } + b.selectedAccountShhKeyID = "" + default: + return err + } + // if b.statusNode.Config().WalletConfig.Enabled { + // wallet, err := b.statusNode.WalletService() + // switch err { + // case node.ErrServiceUnknown: + // case nil: + // err = wallet.StopReactor() + // if err != nil { + // return err + // } + // default: + // return err + // } + // } + return nil +} + +func (b *nimbusStatusBackend) closeAppDB() error { + if b.appDB != nil { + err := b.appDB.Close() + if err != nil { + return err + } + b.appDB = nil + return nil + } + return nil +} + +// SelectAccount selects current wallet and chat accounts, by verifying that each address has corresponding account which can be decrypted +// using provided password. Once verification is done, the decrypted chat key is injected into Whisper (as a single identity, +// all previous identities are removed). +func (b *nimbusStatusBackend) SelectAccount(loginParams account.LoginParams) error { + b.mu.Lock() + defer b.mu.Unlock() + + b.AccountManager().RemoveOnboarding() + + err := b.accountManager.SelectAccount(loginParams) + if err != nil { + return err + } + + if err := b.injectAccountIntoServices(); err != nil { + return err + } + + // if err := b.startWallet(); err != nil { + // return err + // } + + return nil +} + +func (b *nimbusStatusBackend) injectAccountIntoServices() error { + chatAccount, err := b.accountManager.SelectedChatAccount() + if err != nil { + return err + } + + identity := chatAccount.AccountKey.PrivateKey + whisperService, err := b.statusNode.WhisperService() + + switch err { + case node.ErrServiceUnknown: // Whisper was never registered + case nil: + if err := whisperService.Whisper.DeleteKeyPairs(); err != nil { // err is not possible; method return value is incorrect + return err + } + b.selectedAccountShhKeyID, err = whisperService.Whisper.AddKeyPair(identity) + if err != nil { + return ErrWhisperIdentityInjectionFailure + } + default: + return err + } + + if whisperService != nil { + st, err := b.statusNode.ShhExtService() + if err != nil { + return err + } + + if err := st.InitProtocol(identity, b.appDB); err != nil { + return err + } + } + return nil +} + +// func (b *nimbusStatusBackend) startWallet() error { +// if !b.statusNode.Config().WalletConfig.Enabled { +// return nil +// } + +// wallet, err := b.statusNode.WalletService() +// if err != nil { +// return err +// } + +// watchAddresses := b.accountManager.WatchAddresses() +// mainAccountAddress, err := b.accountManager.MainAccountAddress() +// if err != nil { +// return err +// } + +// allAddresses := make([]types.Address, len(watchAddresses)+1) +// allAddresses[0] = mainAccountAddress +// copy(allAddresses[1:], watchAddresses) +// return wallet.StartReactor( +// b.statusNode.RPCClient().Ethclient(), +// allAddresses, +// new(big.Int).SetUint64(b.statusNode.Config().NetworkID), +// ) +// } + +// InjectChatAccount selects the current chat account using chatKeyHex and injects the key into whisper. +// TODO: change the interface and omit the last argument. +func (b *nimbusStatusBackend) InjectChatAccount(chatKeyHex, _ string) error { + b.mu.Lock() + defer b.mu.Unlock() + + b.accountManager.Logout() + + chatKey, err := crypto.HexToECDSA(chatKeyHex) + if err != nil { + return err + } + b.accountManager.SetChatAccount(chatKey) + + return b.injectAccountIntoServices() +} + +func appendIf(condition bool, services []nimbussvc.ServiceConstructor, service nimbussvc.ServiceConstructor) []nimbussvc.ServiceConstructor { + if !condition { + return services + } + return append(services, service) +} + +// ExtractGroupMembershipSignatures extract signatures from tuples of content/signature +func (b *nimbusStatusBackend) ExtractGroupMembershipSignatures(signaturePairs [][2]string) ([]string, error) { + return crypto.ExtractSignatures(signaturePairs) +} + +// SignGroupMembership signs a piece of data containing membership information +func (b *nimbusStatusBackend) SignGroupMembership(content string) (string, error) { + selectedChatAccount, err := b.accountManager.SelectedChatAccount() + if err != nil { + return "", err + } + + return crypto.SignStringAsHex(content, selectedChatAccount.AccountKey.PrivateKey) +} + +// EnableInstallation enables an installation for multi-device sync. +func (b *nimbusStatusBackend) EnableInstallation(installationID string) error { + st, err := b.statusNode.ShhExtService() + if err != nil { + return err + } + + if err := st.EnableInstallation(installationID); err != nil { + b.log.Error("error enabling installation", "err", err) + return err + } + + return nil +} + +// DisableInstallation disables an installation for multi-device sync. +func (b *nimbusStatusBackend) DisableInstallation(installationID string) error { + st, err := b.statusNode.ShhExtService() + if err != nil { + return err + } + + if err := st.DisableInstallation(installationID); err != nil { + b.log.Error("error disabling installation", "err", err) + return err + } + + return nil +} + +// UpdateMailservers on ShhExtService. +func (b *nimbusStatusBackend) UpdateMailservers(enodes []string) error { + // TODO + return nil + // st, err := b.statusNode.ShhExtService() + // if err != nil { + // return err + // } + // nodes := make([]*enode.Node, len(enodes)) + // for i, rawurl := range enodes { + // node, err := enode.ParseV4(rawurl) + // if err != nil { + // return err + // } + // nodes[i] = node + // } + // return st.UpdateMailservers(nodes) +} + +// SignHash exposes vanilla ECDSA signing for signing a message for Swarm +func (b *nimbusStatusBackend) SignHash(hexEncodedHash string) (string, error) { + hash, err := types.DecodeHex(hexEncodedHash) + if err != nil { + return "", fmt.Errorf("SignHash: could not unmarshal the input: %v", err) + } + + chatAccount, err := b.accountManager.SelectedChatAccount() + if err != nil { + return "", fmt.Errorf("SignHash: could not select account: %v", err.Error()) + } + + signature, err := crypto.Sign(hash, chatAccount.AccountKey.PrivateKey) + if err != nil { + return "", fmt.Errorf("SignHash: could not sign the hash: %v", err) + } + + hexEncodedSignature := types.EncodeHex(signature) + return hexEncodedSignature, nil +} diff --git a/eth-node/bridge/geth/node.go b/eth-node/bridge/geth/node.go index c4666fe5f4..a629ee835a 100644 --- a/eth-node/bridge/geth/node.go +++ b/eth-node/bridge/geth/node.go @@ -24,6 +24,10 @@ func NewNodeBridge(stack *node.Node) types.Node { return &gethNodeWrapper{stack: stack} } +func (w *gethNodeWrapper) Poll() { + // noop +} + func (w *gethNodeWrapper) NewENSVerifier(logger *zap.Logger) enstypes.ENSVerifier { return gethens.NewVerifier(logger) } diff --git a/eth-node/bridge/geth/whisper.go b/eth-node/bridge/geth/whisper.go index 2423a3264e..87f381c931 100644 --- a/eth-node/bridge/geth/whisper.go +++ b/eth-node/bridge/geth/whisper.go @@ -80,6 +80,11 @@ func (w *gethWhisperWrapper) DeleteKeyPair(keyID string) bool { return w.whisper.DeleteKeyPair(keyID) } +// DeleteKeyPairs removes all cryptographic identities known to the node +func (w *gethWhisperWrapper) DeleteKeyPairs() error { + return w.whisper.DeleteKeyPairs() +} + func (w *gethWhisperWrapper) AddSymKeyDirect(key []byte) (string, error) { return w.whisper.AddSymKeyDirect(key) } diff --git a/eth-node/bridge/nimbus/build-nimbus.sh b/eth-node/bridge/nimbus/build-nimbus.sh index a139d51d6c..48751f54f7 100755 --- a/eth-node/bridge/nimbus/build-nimbus.sh +++ b/eth-node/bridge/nimbus/build-nimbus.sh @@ -13,7 +13,7 @@ target_dir="${GIT_ROOT}/vendor/github.com/status-im/status-go/eth-node/bridge/ni if [ -z "$nimbus_dir" ]; then # The git ref of Nimbus to fetch and build. This should represent a commit SHA or a tag, for reproducible builds - nimbus_ref='master' # TODO: Use a tag once + nimbus_ref='feature/android-api' # TODO: Use a tag once nimbus_src='https://github.com/status-im/nimbus/' nimbus_dir="${GIT_ROOT}/vendor/github.com/status-im/nimbus" @@ -32,13 +32,9 @@ if [ -z "$nimbus_dir" ]; then fi # Build Nimbus wrappers and copy them into the Nimbus bridge in status-eth-node -build_dir=$(cd $nimbus_dir && nix-build --pure --no-out-link -A wrappers) -# Ideally we'd use the static version of the Nimbus library (.a), -# however that causes link errors due to duplicate symbols: -# ${target_dir}/libnimbus.a(secp256k1.c.o): In function `secp256k1_context_create': -# (.text+0xca80): multiple definition of `secp256k1_context_create' -# /tmp/go-link-476687730/000014.o:${GIT_ROOT}/vendor/github.com/ethereum/go-ethereum/crypto/secp256k1/./libsecp256k1/src/secp256k1.c:56: first defined here +build_dir=$(nix-build --pure --no-out-link -A wrappers-native $nimbus_dir/nix/default.nix) rm -f ${target_dir}/libnimbus.* mkdir -p ${target_dir} cp -f ${build_dir}/include/* ${build_dir}/lib/libnimbus.so \ ${target_dir}/ +chmod +w ${target_dir}/libnimbus.{so,h} diff --git a/eth-node/bridge/nimbus/node.go b/eth-node/bridge/nimbus/node.go index 9e058aa08f..1feff085d7 100644 --- a/eth-node/bridge/nimbus/node.go +++ b/eth-node/bridge/nimbus/node.go @@ -104,6 +104,10 @@ func (n *nimbusNodeWrapper) GetWhisper(ctx interface{}) (types.Whisper, error) { return n.w, nil } +func (w *nimbusNodeWrapper) GetWaku(ctx interface{}) (types.Waku, error) { + panic("not implemented") +} + func (n *nimbusNodeWrapper) AddPeer(url string) error { urlC := C.CString(url) defer C.free(unsafe.Pointer(urlC)) diff --git a/eth-node/bridge/nimbus/whisper.go b/eth-node/bridge/nimbus/whisper.go index 917b01c5f6..de241bf33f 100644 --- a/eth-node/bridge/nimbus/whisper.go +++ b/eth-node/bridge/nimbus/whisper.go @@ -164,6 +164,16 @@ func (w *nimbusWhisperWrapper) DeleteKeyPair(keyID string) bool { return retVal.value.(bool) } +// DeleteKeyPairs removes all cryptographic identities known to the node +func (w *nimbusWhisperWrapper) DeleteKeyPairs() error { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + C.nimbus_delete_keypairs() + c <- callReturn{} + }) + + return retVal.err +} + func (w *nimbusWhisperWrapper) AddSymKeyDirect(key []byte) (string, error) { retVal := w.routineQueue.Send(func(c chan<- callReturn) { keyC := C.CBytes(key) diff --git a/eth-node/types/whisper.go b/eth-node/types/whisper.go index 23529c4daf..f3266bbab8 100644 --- a/eth-node/types/whisper.go +++ b/eth-node/types/whisper.go @@ -31,6 +31,8 @@ type Whisper interface { AddKeyPair(key *ecdsa.PrivateKey) (string, error) // DeleteKeyPair deletes the key with the specified ID if it exists. DeleteKeyPair(keyID string) bool + // DeleteKeyPairs removes all cryptographic identities known to the node + DeleteKeyPairs() error AddSymKeyDirect(key []byte) (string, error) AddSymKeyFromPassword(password string) (string, error) DeleteSymKey(id string) bool diff --git a/go.mod b/go.mod index c87f0fa7ea..25879432ba 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,7 @@ require ( github.com/syndtr/goleveldb v1.0.0 go.uber.org/zap v1.13.0 golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c + golang.org/x/tools v0.0.0-20200116062425-473961ec044c // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 diff --git a/go.sum b/go.sum index 8ee4f0395e..3c3c1d397a 100644 --- a/go.sum +++ b/go.sum @@ -22,14 +22,11 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/allegro/bigcache v0.0.0-20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/allegro/bigcache v1.2.0 h1:qDaE0QoF29wKBb3+pXFrJFy1ihe5OT9OiXhg1t85SxM= @@ -40,7 +37,6 @@ github.com/aristanetworks/fsnotify v1.4.2/go.mod h1:D/rtu7LpjYM8tRJphJ0hUBYpjai8 github.com/aristanetworks/glog v0.0.0-20180419172825-c15b03b3054f/go.mod h1:KASm+qXFKs/xjSoWn30NrWBBvdTTQq+UjkhjEJHfSFA= github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= github.com/aristanetworks/goarista v0.0.0-20190219163901-728bce664cf5/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= -github.com/aristanetworks/goarista v0.0.0-20190502180301-283422fc1708 h1:tS7jSmwRqSxTnonTRlDD1oHo6Q9YOK4xHS9/v4L56eg= github.com/aristanetworks/goarista v0.0.0-20190502180301-283422fc1708/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= github.com/aristanetworks/goarista v0.0.0-20191106175434-873d404c7f40 h1:ZdRuixFqR3mfx4FHzclG3COrRgWrYq0VhNgIoYoObcM= github.com/aristanetworks/goarista v0.0.0-20191106175434-873d404c7f40/go.mod h1:Z4RTxGAuYhPzcq8+EdRM+R8M48Ssle2TsWtwRKa+vns= @@ -72,7 +68,6 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= @@ -132,13 +127,10 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elastic/gosigar v0.0.0-20180330100440-37f05ff46ffa h1:o8OuEkracbk3qH6GvlI6XpEN1HTSxkzOG42xZpfDv/s= github.com/elastic/gosigar v0.0.0-20180330100440-37f05ff46ffa/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs= -github.com/elastic/gosigar v0.10.4 h1:6jfw75dsoflhBMRdO6QPzQUgLqUYTsQQQRkkcsHsuPo= github.com/elastic/gosigar v0.10.4/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs= github.com/elastic/gosigar v0.10.5 h1:GzPQ+78RaAb4J63unidA/JavQRKrB6s8IOzN6Ib59jo= github.com/elastic/gosigar v0.10.5/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs= @@ -146,7 +138,6 @@ github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo github.com/ethereum/go-ethereum v1.8.20/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY= github.com/ethereum/go-ethereum v1.9.2/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY= github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc h1:jtW8jbpkO4YirRSyepBOH8E+2HEw6/hKkBvFPwhUN8c= github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a h1:1znxn4+q2MrEdTk1eCk6KIV3muTYVclBIB6CTVR/zBc= github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= @@ -164,7 +155,6 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -178,7 +168,6 @@ github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhD github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -273,7 +262,6 @@ github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGk github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc= github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= github.com/jackpal/go-nat-pmp v0.0.0-20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= -github.com/jackpal/go-nat-pmp v1.0.1 h1:i0LektDkO1QlrTm/cSuP+PyBCDnYvjPLGl4LdWEMiaA= github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= @@ -312,7 +300,6 @@ github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/koron/go-ssdp v0.0.0-20180514024734-4a0ed625a78b h1:wxtKgYHEncAU00muMD06dzLiahtGM1eouRNOzVV7tdQ= github.com/koron/go-ssdp v0.0.0-20180514024734-4a0ed625a78b/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d h1:68u9r4wEvL3gYg2jvAOgROwZ3H+Y3hIDk4tbbmIjcYQ= github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= @@ -341,19 +328,15 @@ github.com/libp2p/go-eventbus v0.1.0 h1:mlawomSAjjkk97QnYiEmHsLu7E136+2oCWSHRUvM github.com/libp2p/go-eventbus v0.1.0/go.mod h1:vROgu5cs5T7cv7POWlWxBaVLxfSegC5UGQf8A2eEmx4= github.com/libp2p/go-flow-metrics v0.0.1 h1:0gxuFd2GuK7IIP5pKljLwps6TvcuYgvG7Atqi3INF5s= github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZxBdp967ls1g+k8= -github.com/libp2p/go-libp2p v0.1.1 h1:52sB0TJuDk2nYMcMfHOKaPoaayDZjaYVCq6Vk1ejUTk= github.com/libp2p/go-libp2p v0.1.1/go.mod h1:I00BRo1UuUSdpuc8Q2mN7yDF/oTUTRAX6JWpTiK9Rp8= github.com/libp2p/go-libp2p v0.4.2 h1:p0cthB0jDNHO4gH2HzS8/nAMMXbfUlFHs0jwZ4U+F2g= github.com/libp2p/go-libp2p v0.4.2/go.mod h1:MNmgUxUw5pMsdOzMlT0EE7oKjRasl+WyVwM0IBlpKgQ= -github.com/libp2p/go-libp2p-autonat v0.1.0 h1:aCWAu43Ri4nU0ZPO7NyLzUvvfqd0nE3dX0R/ZGYVgOU= github.com/libp2p/go-libp2p-autonat v0.1.0/go.mod h1:1tLf2yXxiE/oKGtDwPYWTSYG3PtvYlJmg7NeVtPRqH8= github.com/libp2p/go-libp2p-autonat v0.1.1 h1:WLBZcIRsjZlWdAZj9CiBSvU2wQXoUOiS1Zk1tM7DTJI= github.com/libp2p/go-libp2p-autonat v0.1.1/go.mod h1:OXqkeGOY2xJVWKAGV2inNF5aKN/djNA3fdpCWloIudE= -github.com/libp2p/go-libp2p-blankhost v0.1.1 h1:X919sCh+KLqJcNRApj43xCSiQRYqOSI88Fdf55ngf78= github.com/libp2p/go-libp2p-blankhost v0.1.1/go.mod h1:pf2fvdLJPsC1FsVrNP3DUUvMzUts2dsLLBEpo1vW1ro= github.com/libp2p/go-libp2p-blankhost v0.1.4 h1:I96SWjR4rK9irDHcHq3XHN6hawCRTPUADzkJacgZLvk= github.com/libp2p/go-libp2p-blankhost v0.1.4/go.mod h1:oJF0saYsAXQCSfDq254GMNmLNz6ZTHTOvtF4ZydUvwU= -github.com/libp2p/go-libp2p-circuit v0.1.0 h1:eniLL3Y9aq/sryfyV1IAHj5rlvuyj3b7iz8tSiZpdhY= github.com/libp2p/go-libp2p-circuit v0.1.0/go.mod h1:Ahq4cY3V9VJcHcn1SBXjr78AbFkZeIRmfunbA7pmFh8= github.com/libp2p/go-libp2p-circuit v0.1.4 h1:Phzbmrg3BkVzbqd4ZZ149JxCuUWu2wZcXf/Kr6hZJj8= github.com/libp2p/go-libp2p-circuit v0.1.4/go.mod h1:CY67BrEjKNDhdTk8UgBX1Y/H5c3xkAcs3gnksxY7osU= @@ -364,7 +347,6 @@ github.com/libp2p/go-libp2p-core v0.2.0/go.mod h1:X0eyB0Gy93v0DZtSYbEM7RnMChm9Uv github.com/libp2p/go-libp2p-core v0.2.2/go.mod h1:8fcwTbsG2B+lTgRJ1ICZtiM5GWCWZVoVrLaDRvIRng0= github.com/libp2p/go-libp2p-core v0.2.4 h1:Et6ykkTwI6PU44tr8qUF9k43vP0aduMNniShAbUJJw8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= -github.com/libp2p/go-libp2p-crypto v0.1.0 h1:k9MFy+o2zGDNGsaoZl0MA3iZ75qXxr9OOoAZF+sD5OQ= github.com/libp2p/go-libp2p-crypto v0.1.0/go.mod h1:sPUokVISZiy+nNuTTH/TY+leRSxnFj/2GLjtOTW90hI= github.com/libp2p/go-libp2p-discovery v0.1.0 h1:j+R6cokKcGbnZLf4kcNwpx6mDEUPF3N6SrqMymQhmvs= github.com/libp2p/go-libp2p-discovery v0.1.0/go.mod h1:4F/x+aldVHjHDHuX85x1zWoFTGElt8HnoDzwkFZm29g= @@ -373,30 +355,24 @@ github.com/libp2p/go-libp2p-loggables v0.1.0/go.mod h1:EyumB2Y6PrYjr55Q3/tiJ/o3x github.com/libp2p/go-libp2p-mplex v0.2.0/go.mod h1:Ejl9IyjvXJ0T9iqUTE1jpYATQ9NM3g+OtR+EMMODbKo= github.com/libp2p/go-libp2p-mplex v0.2.1 h1:E1xaJBQnbSiTHGI1gaBKmKhu1TUKkErKJnE8iGvirYI= github.com/libp2p/go-libp2p-mplex v0.2.1/go.mod h1:SC99Rxs8Vuzrf/6WhmH41kNn13TiYdAWNYHrwImKLnE= -github.com/libp2p/go-libp2p-nat v0.0.4 h1:+KXK324yaY701On8a0aGjTnw8467kW3ExKcqW2wwmyw= github.com/libp2p/go-libp2p-nat v0.0.4/go.mod h1:N9Js/zVtAXqaeT99cXgTV9e75KpnWCvVOiGzlcHmBbY= github.com/libp2p/go-libp2p-nat v0.0.5 h1:/mH8pXFVKleflDL1YwqMg27W9GD8kjEx7NY0P6eGc98= github.com/libp2p/go-libp2p-nat v0.0.5/go.mod h1:1qubaE5bTZMJE+E/uu2URroMbzdubFz1ChgiN79yKPE= github.com/libp2p/go-libp2p-netutil v0.1.0/go.mod h1:3Qv/aDqtMLTUyQeundkKsA+YCThNdbQD54k3TqjpbFU= -github.com/libp2p/go-libp2p-peer v0.2.0 h1:EQ8kMjaCUwt/Y5uLgjT8iY2qg0mGUT0N1zUjer50DsY= github.com/libp2p/go-libp2p-peer v0.2.0/go.mod h1:RCffaCvUyW2CJmG2gAWVqwePwW7JMgxjsHm7+J5kjWY= -github.com/libp2p/go-libp2p-peerstore v0.1.0 h1:MKh7pRNPHSh1fLPj8u/M/s/napdmeNpoi9BRy9lPN0E= github.com/libp2p/go-libp2p-peerstore v0.1.0/go.mod h1:2CeHkQsr8svp4fZ+Oi9ykN1HBb6u0MOvdJ7YIsmcwtY= github.com/libp2p/go-libp2p-peerstore v0.1.3/go.mod h1:BJ9sHlm59/80oSkpWgr1MyY1ciXAXV397W6h1GH/uKI= github.com/libp2p/go-libp2p-peerstore v0.1.4 h1:d23fvq5oYMJ/lkkbO4oTwBp/JP+I/1m5gZJobNXCE/k= github.com/libp2p/go-libp2p-peerstore v0.1.4/go.mod h1:+4BDbDiiKf4PzpANZDAT+knVdLxvqh7hXOujessqdzs= -github.com/libp2p/go-libp2p-secio v0.1.0 h1:NNP5KLxuP97sE5Bu3iuwOWyT/dKEGMN5zSLMWdB7GTQ= github.com/libp2p/go-libp2p-secio v0.1.0/go.mod h1:tMJo2w7h3+wN4pgU2LSYeiKPrfqBgkOsdiKK77hE7c8= github.com/libp2p/go-libp2p-secio v0.2.0/go.mod h1:2JdZepB8J5V9mBp79BmwsaPQhRPNN2NrnB2lKQcdy6g= github.com/libp2p/go-libp2p-secio v0.2.1 h1:eNWbJTdyPA7NxhP7J3c5lT97DC5d+u+IldkgCYFTPVA= github.com/libp2p/go-libp2p-secio v0.2.1/go.mod h1:cWtZpILJqkqrSkiYcDBh5lA3wbT2Q+hz3rJQq3iftD8= -github.com/libp2p/go-libp2p-swarm v0.1.0 h1:HrFk2p0awrGEgch9JXK/qp/hfjqQfgNxpLWnCiWPg5s= github.com/libp2p/go-libp2p-swarm v0.1.0/go.mod h1:wQVsCdjsuZoc730CgOvh5ox6K8evllckjebkdiY5ta4= github.com/libp2p/go-libp2p-swarm v0.2.2 h1:T4hUpgEs2r371PweU3DuH7EOmBIdTBCwWs+FLcgx3bQ= github.com/libp2p/go-libp2p-swarm v0.2.2/go.mod h1:fvmtQ0T1nErXym1/aa1uJEyN7JzaTNyBcHImCxRpPKU= github.com/libp2p/go-libp2p-testing v0.0.2/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E= github.com/libp2p/go-libp2p-testing v0.0.3/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E= -github.com/libp2p/go-libp2p-testing v0.0.4 h1:Qev57UR47GcLPXWjrunv5aLIQGO4n9mhI/8/EIrEEFc= github.com/libp2p/go-libp2p-testing v0.0.4/go.mod h1:gvchhf3FQOtBdr+eFUABet5a4MBLK8jM3V4Zghvmi+E= github.com/libp2p/go-libp2p-testing v0.1.0 h1:WaFRj/t3HdMZGNZqnU2pS7pDRBmMeoDx7/HDNpeyT9U= github.com/libp2p/go-libp2p-testing v0.1.0/go.mod h1:xaZWMJrPUM5GlDBxCeGUi7kI4eqnjVyavGroI2nxEM0= @@ -405,7 +381,6 @@ github.com/libp2p/go-libp2p-transport-upgrader v0.1.1/go.mod h1:IEtA6or8JUbsV07q github.com/libp2p/go-libp2p-yamux v0.2.0/go.mod h1:Db2gU+XfLpm6E4rG5uGCFX6uXA8MEXOxFcRoXUODaK8= github.com/libp2p/go-libp2p-yamux v0.2.1 h1:Q3XYNiKCC2vIxrvUJL+Jg1kiyeEaIDNKLjgEjo3VQdI= github.com/libp2p/go-libp2p-yamux v0.2.1/go.mod h1:1FBXiHDk1VyRM1C0aez2bCfHQ4vMZKkAQzZbkSQt5fI= -github.com/libp2p/go-maddr-filter v0.0.4 h1:hx8HIuuwk34KePddrp2mM5ivgPkZ09JH4AvsALRbFUs= github.com/libp2p/go-maddr-filter v0.0.4/go.mod h1:6eT12kSQMA9x2pvFQa+xesMKUBlj9VImZbj3B9FBH/Q= github.com/libp2p/go-maddr-filter v0.0.5 h1:CW3AgbMO6vUvT4kf87y4N+0P8KUl2aqLYhrGyDUbLSg= github.com/libp2p/go-maddr-filter v0.0.5/go.mod h1:Jk+36PMfIqCJhAnaASRH83bdAvfDRp/w6ENFaC9bG+M= @@ -415,11 +390,9 @@ github.com/libp2p/go-mplex v0.1.0/go.mod h1:SXgmdki2kwCUlCCbfGLEgHjC4pFqhTp0ZoV6 github.com/libp2p/go-msgio v0.0.2/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ= github.com/libp2p/go-msgio v0.0.4 h1:agEFehY3zWJFUHK6SEMR7UYmk2z6kC3oeCM7ybLhguA= github.com/libp2p/go-msgio v0.0.4/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ= -github.com/libp2p/go-nat v0.0.3 h1:l6fKV+p0Xa354EqQOQP+d8CivdLM4kl5GxC1hSc/UeI= github.com/libp2p/go-nat v0.0.3/go.mod h1:88nUEt0k0JD45Bk93NIwDqjlhiOwOoV36GchpcVc1yI= github.com/libp2p/go-nat v0.0.4 h1:KbizNnq8YIf7+Hn7+VFL/xE0eDrkPru2zIO9NMwL8UQ= github.com/libp2p/go-nat v0.0.4/go.mod h1:Nmw50VAvKuk38jUBcmNh6p9lUJLoODbJRvYAa/+KSDo= -github.com/libp2p/go-openssl v0.0.2 h1:9pP2d3Ubaxkv7ZisLjx9BFwgOGnQdQYnfcH29HNY3ls= github.com/libp2p/go-openssl v0.0.2/go.mod h1:v8Zw2ijCSWBQi8Pq5GAixw6DbFfa9u6VIYDXnvOXkc0= github.com/libp2p/go-openssl v0.0.3 h1:wjlG7HvQkt4Fq4cfH33Ivpwp0omaElYEi9z26qaIkIk= github.com/libp2p/go-openssl v0.0.3/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc= @@ -430,11 +403,9 @@ github.com/libp2p/go-reuseport-transport v0.0.2/go.mod h1:YkbSDrvjUVDL6b8XqriyA2 github.com/libp2p/go-stream-muxer v0.0.1/go.mod h1:bAo8x7YkSpadMTbtTaxGVHWUQsR/l5MEaHbKaliuT14= github.com/libp2p/go-stream-muxer-multistream v0.2.0 h1:714bRJ4Zy9mdhyTLJ+ZKiROmAFwUHpeRidG+q7LTQOg= github.com/libp2p/go-stream-muxer-multistream v0.2.0/go.mod h1:j9eyPol/LLRqT+GPLSxvimPhNph4sfYfMoDPd7HkzIc= -github.com/libp2p/go-tcp-transport v0.1.0 h1:IGhowvEqyMFknOar4FWCKSWE0zL36UFKQtiRQD60/8o= github.com/libp2p/go-tcp-transport v0.1.0/go.mod h1:oJ8I5VXryj493DEJ7OsBieu8fcg2nHGctwtInJVpipc= github.com/libp2p/go-tcp-transport v0.1.1 h1:yGlqURmqgNA2fvzjSgZNlHcsd/IulAnKM8Ncu+vlqnw= github.com/libp2p/go-tcp-transport v0.1.1/go.mod h1:3HzGvLbx6etZjnFlERyakbaYPdfjg2pWP97dFZworkY= -github.com/libp2p/go-ws-transport v0.1.0 h1:F+0OvvdmPTDsVc4AjPHjV7L7Pk1B7D5QwtDcKE2oag4= github.com/libp2p/go-ws-transport v0.1.0/go.mod h1:rjw1MG1LU9YDC6gzmwObkPd/Sqwhw7yT74kj3raBFuo= github.com/libp2p/go-ws-transport v0.1.2 h1:VnxQcLfSGtqupqPpBNu8fUiCv+IN1RJ2BcVqQEM+z8E= github.com/libp2p/go-ws-transport v0.1.2/go.mod h1:dsh2Ld8F+XNmzpkaAijmg5Is+e9l6/1tK/6VFOdN69Y= @@ -452,13 +423,11 @@ github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0X github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-isatty v0.0.0-20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f h1:QTRRO+ozoYgT3CQRIzNVYJRU3DB8HRnkZv6mr4ISmMA= github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= -github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -499,15 +468,12 @@ github.com/multiformats/go-multiaddr v0.1.0/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lg github.com/multiformats/go-multiaddr v0.1.1 h1:rVAztJYMhCQ7vEFr8FvxW3mS+HF2eY/oPbOMeS0ZDnE= github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= github.com/multiformats/go-multiaddr-dns v0.0.1/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q= -github.com/multiformats/go-multiaddr-dns v0.0.2 h1:/Bbsgsy3R6e3jf2qBahzNHzww6usYaZ0NhNH3sqdFS8= github.com/multiformats/go-multiaddr-dns v0.0.2/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q= github.com/multiformats/go-multiaddr-dns v0.2.0 h1:YWJoIDwLePniH7OU5hBnDZV6SWuvJqJ0YtN6pLeH9zA= github.com/multiformats/go-multiaddr-dns v0.2.0/go.mod h1:TJ5pr5bBO7Y1B18djPuRsVkduhQH2YqYSbxWJzYGdK0= -github.com/multiformats/go-multiaddr-fmt v0.0.1 h1:5YjeOIzbX8OTKVaN72aOzGIYW7PnrZrnkDyOfAWRSMA= github.com/multiformats/go-multiaddr-fmt v0.0.1/go.mod h1:aBYjqL4T/7j4Qx+R73XSv/8JsgnRFlf0w2KGLCmXl3Q= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= -github.com/multiformats/go-multiaddr-net v0.0.1 h1:76O59E3FavvHqNg7jvzWzsPSW5JSi/ek0E4eiDVbg9g= github.com/multiformats/go-multiaddr-net v0.0.1/go.mod h1:nw6HSxNmCIQH27XPGBuX+d1tnvM7ihcFwHMSstNAVUU= github.com/multiformats/go-multiaddr-net v0.1.0/go.mod h1:5JNbcfBOP4dnhoZOv10JJVkJO0pCCEf8mTnipAo2UZQ= github.com/multiformats/go-multiaddr-net v0.1.1 h1:jFFKUuXTXv+3ARyHZi3XUqQO+YWMKgBdhEvuGRfnL6s= @@ -529,18 +495,15 @@ github.com/nsf/termbox-go v0.0.0-20170211012700-3540b76b9c77/go.mod h1:IuKpRQcYE github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd h1:+iAPaTbi1gZpcpDwe/BW1fx7Xoesv69hLNGPheoyhBs= github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd/go.mod h1:4soZNh0zW0LtYGdQ416i0jO0EIqMGcbtaspRS4BDvRQ= -github.com/olekukonko/tablewriter v0.0.0-20170128050532-febf2d34b54a h1:m6hB6GkmZ/suOSKZM7yx3Yt+7iZ9HNfzacCykJqgXA8= github.com/olekukonko/tablewriter v0.0.0-20170128050532-febf2d34b54a/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -551,7 +514,6 @@ github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQ github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opentracing/opentracing-go v0.0.0-20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -600,7 +562,6 @@ github.com/prometheus/prometheus v0.0.0-20170814170113-3101606756c5/go.mod h1:oA github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= @@ -608,7 +569,6 @@ github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C github.com/robertkrimen/otto v0.0.0-20170205013659-6a77b7cbc37d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= @@ -663,7 +623,6 @@ github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3 h1:njlZPzLwU639 github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3/go.mod h1:hpGUWaI9xL8pRQCTXQgocU38Qw1g0Us7n5PxxTwTCYU= github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -698,7 +657,6 @@ github.com/wealdtech/go-string2eth v1.0.0/go.mod h1:UZA/snEybGcD6n+Pl+yoDjmexlEJ github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc h1:9lDbC6Rz4bwmou+oE6Dt4Cb2BGMur5eR/GYptkKUVHo= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= -github.com/whyrusleeping/go-notifier v0.0.0-20170827234753-097c5d47330f h1:M/lL30eFZTKnomXY6huvM6G0+gVquFNf6mxghaWlFUg= github.com/whyrusleeping/go-notifier v0.0.0-20170827234753-097c5d47330f/go.mod h1:cZNvX9cFybI01GriPRMXDtczuvUhgbcYr9iCGaNlRv8= github.com/whyrusleeping/mafmt v1.2.8 h1:TCghSl5kkwEE0j+sU/gudyhVMRlpBin8fMBBHg59EbA= github.com/whyrusleeping/mafmt v1.2.8/go.mod h1:faQJFPbLSxzD9xpA02ttW/tS9vZykNvXwGvqIpk20FA= @@ -724,13 +682,11 @@ go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= @@ -750,7 +706,6 @@ golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba h1:9bFeDpN3gTqNanMVqNcoR/pJQuP5uroC3t1D7eXozTE= golang.org/x/crypto v0.0.0-20191119213627-4f8c1d86b1ba/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c h1:/nJuwDLoL/zrqY6gf57vxC+Pi+pZ8bfhpPkicO5H7W4= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -762,6 +717,7 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -779,7 +735,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2 h1:4dVFTC832rPn4pomLSz1vA+are2+dU19w1H8OngV7nc= golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -791,7 +746,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -844,7 +798,10 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101 h1:LCmXVkvpQCDj724eX6irUTPCJP5GelFHxqGSWL2D1R0= golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200116062425-473961ec044c h1:D0OxfnjPaEGt7AluXNompYUYGhoY3u6+bValgqfd1vE= +golang.org/x/tools v0.0.0-20200116062425-473961ec044c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -860,11 +817,9 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb h1:i1Ppqkc3WQXikh8 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -888,7 +843,6 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXL gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= -gopkg.in/olebedev/go-duktape.v3 v3.0.0-20180302121509-abf0ba0be5d5 h1:VWXVtmkY4YFVuF1FokZ0PUsuvtx3Di6z/m47daSP5f0= gopkg.in/olebedev/go-duktape.v3 v3.0.0-20180302121509-abf0ba0be5d5/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190709231704-1e4459ed25ff h1:uuol9OUzSvZntY1v963NAbVd7A+PHLMz1FlCe3Lorcs= gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190709231704-1e4459ed25ff/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= diff --git a/mobile/status.go b/mobile/status.go index 0563dead4e..7433c0ba14 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -27,8 +27,6 @@ import ( "github.com/status-im/status-go/transactions" ) -var statusBackend = api.NewGethStatusBackend() - // OpenAccounts opens database and returns accounts list. func OpenAccounts(datadir string) string { statusBackend.UpdateRootDataDir(datadir) @@ -205,49 +203,51 @@ func CallPrivateRPC(inputJSON string) string { // CreateAccount is equivalent to creating an account from the command line, // just modified to handle the function arg passing. func CreateAccount(password string) string { - info, mnemonic, err := statusBackend.AccountManager().CreateAccount(password) - errString := "" - if err != nil { - fmt.Fprintln(os.Stderr, err) - errString = err.Error() - } - - out := AccountInfo{ - Address: info.WalletAddress, - PubKey: info.WalletPubKey, - WalletAddress: info.WalletAddress, - WalletPubKey: info.WalletPubKey, - ChatAddress: info.ChatAddress, - ChatPubKey: info.ChatPubKey, - Mnemonic: mnemonic, - Error: errString, - } - outBytes, _ := json.Marshal(out) - return string(outBytes) + panic("CreateAccount") + // info, mnemonic, err := statusBackend.AccountManager().CreateAccount(password) + // errString := "" + // if err != nil { + // fmt.Fprintln(os.Stderr, err) + // errString = err.Error() + // } + + // out := AccountInfo{ + // Address: info.WalletAddress, + // PubKey: info.WalletPubKey, + // WalletAddress: info.WalletAddress, + // WalletPubKey: info.WalletPubKey, + // ChatAddress: info.ChatAddress, + // ChatPubKey: info.ChatPubKey, + // Mnemonic: mnemonic, + // Error: errString, + // } + // outBytes, _ := json.Marshal(out) + // return string(outBytes) } // RecoverAccount re-creates master key using given details. func RecoverAccount(password, mnemonic string) string { - info, err := statusBackend.AccountManager().RecoverAccount(password, mnemonic) - - errString := "" - if err != nil { - fmt.Fprintln(os.Stderr, err) - errString = err.Error() - } - - out := AccountInfo{ - Address: info.WalletAddress, - PubKey: info.WalletPubKey, - WalletAddress: info.WalletAddress, - WalletPubKey: info.WalletPubKey, - ChatAddress: info.ChatAddress, - ChatPubKey: info.ChatPubKey, - Mnemonic: mnemonic, - Error: errString, - } - outBytes, _ := json.Marshal(out) - return string(outBytes) + panic("RecoverAccount") + // info, err := statusBackend.AccountManager().RecoverAccount(password, mnemonic) + + // errString := "" + // if err != nil { + // fmt.Fprintln(os.Stderr, err) + // errString = err.Error() + // } + + // out := AccountInfo{ + // Address: info.WalletAddress, + // PubKey: info.WalletPubKey, + // WalletAddress: info.WalletAddress, + // WalletPubKey: info.WalletPubKey, + // ChatAddress: info.ChatAddress, + // ChatPubKey: info.ChatPubKey, + // Mnemonic: mnemonic, + // Error: errString, + // } + // outBytes, _ := json.Marshal(out) + // return string(outBytes) } // StartOnboarding initialize the onboarding with n random accounts diff --git a/mobile/status_geth.go b/mobile/status_geth.go new file mode 100644 index 0000000000..0cac474788 --- /dev/null +++ b/mobile/status_geth.go @@ -0,0 +1,9 @@ +// +build !nimbus + +package statusgo + +import ( + "github.com/status-im/status-go/api" +) + +var statusBackend = api.NewGethStatusBackend() diff --git a/mobile/status_nimbus.go b/mobile/status_nimbus.go new file mode 100644 index 0000000000..58d1557113 --- /dev/null +++ b/mobile/status_nimbus.go @@ -0,0 +1,9 @@ +// +build nimbus + +package statusgo + +import ( + "github.com/status-im/status-go/api" +) + +var statusBackend = api.NewNimbusStatusBackend() diff --git a/node/status_node.go b/node/get_status_node.go similarity index 99% rename from node/status_node.go rename to node/get_status_node.go index d55ba4d0f2..a946a3aa01 100644 --- a/node/status_node.go +++ b/node/get_status_node.go @@ -1,3 +1,5 @@ +// +build !nimbus + package node import ( diff --git a/node/node.go b/node/geth_node.go similarity index 99% rename from node/node.go rename to node/geth_node.go index b1d742d425..f334bf880e 100644 --- a/node/node.go +++ b/node/geth_node.go @@ -1,3 +1,5 @@ +// +build !nimbus + package node import ( @@ -134,7 +136,7 @@ func activateNodeServices(stack *node.Node, config *params.NodeConfig, db *level return &nodebridge.NodeService{Node: gethbridge.NewNodeBridge(stack)}, nil }) if err != nil { - return fmt.Errorf("failed to register NodeBridhe: %v", err) + return fmt.Errorf("failed to register NodeBridge: %v", err) } // start Whisper service. diff --git a/node/node_api_test.go b/node/geth_node_api_test.go similarity index 100% rename from node/node_api_test.go rename to node/geth_node_api_test.go diff --git a/node/node_test.go b/node/geth_node_test.go similarity index 99% rename from node/node_test.go rename to node/geth_node_test.go index 9dce87e17b..617d1d8c0d 100644 --- a/node/node_test.go +++ b/node/geth_node_test.go @@ -1,3 +1,5 @@ +// +build !nimbus + package node import ( diff --git a/node/status_node_test.go b/node/geth_status_node_test.go similarity index 100% rename from node/status_node_test.go rename to node/geth_status_node_test.go diff --git a/node/nimbus_node.go b/node/nimbus_node.go new file mode 100644 index 0000000000..3b4e61a5db --- /dev/null +++ b/node/nimbus_node.go @@ -0,0 +1,392 @@ +// +build nimbus + +package node + +import ( + "errors" + "fmt" + "reflect" + + "github.com/syndtr/goleveldb/leveldb" + + gethrpc "github.com/ethereum/go-ethereum/rpc" + + "github.com/status-im/status-go/params" + nimbussvc "github.com/status-im/status-go/services/nimbus" + "github.com/status-im/status-go/services/nodebridge" + "github.com/status-im/status-go/services/shhext" + "github.com/status-im/status-go/services/status" + "github.com/status-im/status-go/timesource" +) + +// Errors related to node and services creation. +var ( + // ErrNodeMakeFailureFormat = "error creating p2p node: %s" + ErrWhisperServiceRegistrationFailure = errors.New("failed to register the Whisper service") + // ErrLightEthRegistrationFailure = errors.New("failed to register the LES service") + ErrLightEthRegistrationFailureUpstreamEnabled = errors.New("failed to register the LES service, upstream is also configured") + // ErrPersonalServiceRegistrationFailure = errors.New("failed to register the personal api service") + ErrStatusServiceRegistrationFailure = errors.New("failed to register the Status service") + // ErrPeerServiceRegistrationFailure = errors.New("failed to register the Peer service") + // ErrIncentivisationServiceRegistrationFailure = errors.New("failed to register the Incentivisation service") +) + +func (n *NimbusStatusNode) activateServices(config *params.NodeConfig, db *leveldb.DB) error { + // start Ethereum service if we are not expected to use an upstream server + if !config.UpstreamConfig.Enabled { + } else { + if config.LightEthConfig.Enabled { + return ErrLightEthRegistrationFailureUpstreamEnabled + } + + n.log.Info("LES protocol is disabled") + + // `personal_sign` and `personal_ecRecover` methods are important to + // keep DApps working. + // Usually, they are provided by an ETH or a LES service, but when using + // upstream, we don't start any of these, so we need to start our own + // implementation. + // if err := n.activatePersonalService(accs, config); err != nil { + // return fmt.Errorf("%v: %v", ErrPersonalServiceRegistrationFailure, err) + // } + } + + if err := n.activateNodeServices(config, db); err != nil { + return err + } + + return nil +} + +func (n *NimbusStatusNode) activateNodeServices(config *params.NodeConfig, db *leveldb.DB) error { + // start Whisper service. + if err := n.activateShhService(config, db); err != nil { + return fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err) + } + + // // start Waku service + // if err := activateWakuService(stack, config, db); err != nil { + // return fmt.Errorf("%v: %v", ErrWakuServiceRegistrationFailure, err) + // } + + // start incentivisation service + // if err := n.activateIncentivisationService(config); err != nil { + // return fmt.Errorf("%v: %v", ErrIncentivisationServiceRegistrationFailure, err) + // } + + // start status service. + if err := n.activateStatusService(config); err != nil { + return fmt.Errorf("%v: %v", ErrStatusServiceRegistrationFailure, err) + } + + // start peer service + // if err := activatePeerService(n); err != nil { + // return fmt.Errorf("%v: %v", ErrPeerServiceRegistrationFailure, err) + // } + return nil +} + +// // activateLightEthService configures and registers the eth.Ethereum service with a given node. +// func activateLightEthService(stack *node.Node, accs *accounts.Manager, config *params.NodeConfig) error { +// if !config.LightEthConfig.Enabled { +// logger.Info("LES protocol is disabled") +// return nil +// } + +// genesis, err := calculateGenesis(config.NetworkID) +// if err != nil { +// return err +// } + +// ethConf := eth.DefaultConfig +// ethConf.Genesis = genesis +// ethConf.SyncMode = downloader.LightSync +// ethConf.NetworkId = config.NetworkID +// ethConf.DatabaseCache = config.LightEthConfig.DatabaseCache +// return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { +// // NOTE(dshulyak) here we set our instance of the accounts manager. +// // without sharing same instance selected account won't be visible for personal_* methods. +// nctx := &node.ServiceContext{} +// *nctx = *ctx +// nctx.AccountManager = accs +// return les.New(nctx, ðConf) +// }) +// } + +// func activatePersonalService(stack *node.Node, accs *accounts.Manager, config *params.NodeConfig) error { +// return stack.Register(func(*node.ServiceContext) (node.Service, error) { +// svc := personal.New(accs) +// return svc, nil +// }) +// } + +// func (n *NimbusStatusNode) activatePersonalService(accs *accounts.Manager, config *params.NodeConfig) error { +// return n.Register(func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { +// svc := personal.New(accs) +// return svc, nil +// }) +// } + +func (n *NimbusStatusNode) activateStatusService(config *params.NodeConfig) error { + if !config.EnableStatusService { + n.log.Info("Status service api is disabled") + return nil + } + + return n.Register(func(ctx *nimbussvc.ServiceContext) (nimbussvc.Service, error) { + var service *nodebridge.WhisperService + if err := ctx.Service(&service); err != nil { + return nil, err + } + svc := status.New(service.Whisper) + return svc, nil + }) +} + +// func (n *NimbusStatusNode) activatePeerService() error { +// return n.Register(func(ctx *nimbussvc.ServiceContext) (nimbussvc.Service, error) { +// svc := peer.New() +// return svc, nil +// }) +// } + +// func registerWhisperMailServer(whisperService *whisper.Whisper, config *params.WhisperConfig) (err error) { +// var mailServer mailserver.WhisperMailServer +// whisperService.RegisterMailServer(&mailServer) + +// return mailServer.Init(whisperService, config) +// } + +// func registerWakuMailServer(wakuService *waku.Waku, config *params.WakuConfig) (err error) { +// var mailServer mailserver.WakuMailServer +// wakuService.RegisterMailServer(&mailServer) + +// return mailServer.Init(wakuService, config) +// } + +// activateShhService configures Whisper and adds it to the given node. +func (n *NimbusStatusNode) activateShhService(config *params.NodeConfig, db *leveldb.DB) (err error) { + if !config.WhisperConfig.Enabled { + n.log.Info("SHH protocol is disabled") + return nil + } + if config.EnableNTPSync { + if err = n.Register(func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { + return timesource.Default(), nil + }); err != nil { + return + } + } + + // err = n.Register(func(ctx *nimbussvc.ServiceContext) (nimbussvc.Service, error) { + // return n.createShhService(ctx, &config.WhisperConfig, &config.ClusterConfig) + // }) + // if err != nil { + // return + // } + + // Register eth-node node bridge + err = n.Register(func(ctx *nimbussvc.ServiceContext) (nimbussvc.Service, error) { + return &nodebridge.NodeService{Node: n.node}, nil + }) + if err != nil { + return + } + + // Register Whisper eth-node bridge + err = n.Register(func(ctx *nimbussvc.ServiceContext) (nimbussvc.Service, error) { + n.log.Info("Creating WhisperService") + + var ethnode *nodebridge.NodeService + if err := ctx.Service(ðnode); err != nil { + return nil, err + } + + w, err := ethnode.Node.GetWhisper(ctx) + if err != nil { + n.log.Error("GetWhisper returned error", "err", err) + return nil, err + } + + return &nodebridge.WhisperService{Whisper: w}, nil + }) + if err != nil { + return + } + + // TODO(dshulyak) add a config option to enable it by default, but disable if app is started from statusd + return n.Register(func(ctx *nimbussvc.ServiceContext) (nimbussvc.Service, error) { + var ethnode *nodebridge.NodeService + if err := ctx.Service(ðnode); err != nil { + return nil, err + } + return shhext.NewNimbus(ethnode.Node, ctx, "shhext", db, config.ShhextConfig), nil + }) +} + +// activateWakuService configures Waku and adds it to the given node. +func (n *NimbusStatusNode) activateWakuService(config *params.NodeConfig, db *leveldb.DB) (err error) { + if !config.WakuConfig.Enabled { + n.log.Info("Waku protocol is disabled") + return nil + } + + panic("not implemented") + // err = n.Register(func(ctx *nimbussvc.ServiceContext) (nimbussvc.Service, error) { + // return createWakuService(ctx, &config.WakuConfig, &config.ClusterConfig) + // }) + // if err != nil { + // return + // } + + // // TODO(dshulyak) add a config option to enable it by default, but disable if app is started from statusd + // return n.Register(func(ctx *nimbussvc.ServiceContext) (nimbussvc.Service, error) { + // var ethnode *nodebridge.NodeService + // if err := ctx.Service(ðnode); err != nil { + // return nil, err + // } + // return shhext.New(ethnode.Node, ctx, "wakuext", shhext.EnvelopeSignalHandler{}, db, config.ShhextConfig), nil + // }) +} + +// Register injects a new service into the node's stack. The service created by +// the passed constructor must be unique in its type with regard to sibling ones. +func (n *NimbusStatusNode) Register(constructor nimbussvc.ServiceConstructor) error { + n.lock.Lock() + defer n.lock.Unlock() + + if n.isRunning() { + return ErrNodeRunning + } + n.serviceFuncs = append(n.serviceFuncs, constructor) + return nil +} + +func (n *NimbusStatusNode) startServices() error { + services := make(map[reflect.Type]nimbussvc.Service) + for _, constructor := range n.serviceFuncs { + // Create a new context for the particular service + ctxServices := make(map[reflect.Type]nimbussvc.Service) + for kind, s := range services { // copy needed for threaded access + ctxServices[kind] = s + } + ctx := nimbussvc.NewServiceContext(n.config, ctxServices) + //EventMux: n.eventmux, + //AccountManager: n.accman, + // Construct and save the service + service, err := constructor(ctx) + if err != nil { + n.log.Info("Service constructor returned error", "err", err) + return err + } + kind := reflect.TypeOf(service) + if _, exists := services[kind]; exists { + return &nimbussvc.DuplicateServiceError{Kind: kind} + } + services[kind] = service + } + // Start each of the services + var started []reflect.Type + for kind, service := range services { + // Start the next service, stopping all previous upon failure + if err := service.StartService(); err != nil { + for _, kind := range started { + services[kind].Stop() + } + + return err + } + // Mark the service started for potential cleanup + started = append(started, kind) + } + // Lastly start the configured RPC interfaces + if err := n.startRPC(services); err != nil { + for _, service := range services { + service.Stop() + } + return err + } + // Finish initializing the startup + n.services = services + + return nil +} + +// startRPC is a helper method to start all the various RPC endpoint during node +// startup. It's not meant to be called at any time afterwards as it makes certain +// assumptions about the state of the node. +func (n *NimbusStatusNode) startRPC(services map[reflect.Type]nimbussvc.Service) error { + // Gather all the possible APIs to surface + apis := []gethrpc.API{} + for _, service := range services { + apis = append(apis, service.APIs()...) + } + + // Start the various API endpoints, terminating all in case of errors + if err := n.startInProc(apis); err != nil { + return err + } + if err := n.startPublicInProc(apis, n.config.FormatAPIModules()); err != nil { + n.stopInProc() + return err + } + // All API endpoints started successfully + n.rpcAPIs = apis + return nil +} + +// startInProc initializes an in-process RPC endpoint. +func (n *NimbusStatusNode) startInProc(apis []gethrpc.API) error { + n.log.Debug("startInProc", "apis", apis) + // Register all the APIs exposed by the services + handler := gethrpc.NewServer() + for _, api := range apis { + n.log.Debug("Registering InProc", "namespace", api.Namespace) + if err := handler.RegisterName(api.Namespace, api.Service); err != nil { + return err + } + n.log.Debug("InProc registered", "namespace", api.Namespace) + } + n.inprocHandler = handler + return nil +} + +// stopInProc terminates the in-process RPC endpoint. +func (n *NimbusStatusNode) stopInProc() { + if n.inprocHandler != nil { + n.inprocHandler.Stop() + n.inprocHandler = nil + } +} + +// startPublicInProc initializes an in-process RPC endpoint for public APIs. +func (n *NimbusStatusNode) startPublicInProc(apis []gethrpc.API, modules []string) error { + n.log.Debug("startPublicInProc", "apis", apis, "modules", modules) + // Generate the whitelist based on the allowed modules + whitelist := make(map[string]bool) + for _, module := range modules { + whitelist[module] = true + } + + // Register all the public APIs exposed by the services + handler := gethrpc.NewServer() + for _, api := range apis { + if whitelist[api.Namespace] || (len(whitelist) == 0 && api.Public) { + n.log.Debug("Registering InProc public", "service", api.Service, "namespace", api.Namespace) + if err := handler.RegisterName(api.Namespace, api.Service); err != nil { + return err + } + n.log.Debug("InProc public registered", "service", api.Service, "namespace", api.Namespace) + } + } + n.inprocPublicHandler = handler + return nil +} + +// stopPublicInProc terminates the in-process RPC endpoint for public APIs. +func (n *NimbusStatusNode) stopPublicInProc() { + if n.inprocPublicHandler != nil { + n.inprocPublicHandler.Stop() + n.inprocPublicHandler = nil + } +} diff --git a/node/nimbus_status_node.go b/node/nimbus_status_node.go new file mode 100644 index 0000000000..4f1f95bed8 --- /dev/null +++ b/node/nimbus_status_node.go @@ -0,0 +1,798 @@ +// +build nimbus + +package node + +import ( + "context" + "crypto/ecdsa" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "sync" + + "github.com/syndtr/goleveldb/leveldb" + + "github.com/ethereum/go-ethereum/log" + gethrpc "github.com/ethereum/go-ethereum/rpc" + + "github.com/status-im/status-go/db" + nimbusbridge "github.com/status-im/status-go/eth-node/bridge/nimbus" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/rpc" + nimbussvc "github.com/status-im/status-go/services/nimbus" + "github.com/status-im/status-go/services/nodebridge" + "github.com/status-im/status-go/services/shhext" + "github.com/status-im/status-go/services/status" +) + +// // tickerResolution is the delta to check blockchain sync progress. +// const tickerResolution = time.Second + +// errors +var ( + ErrNodeRunning = errors.New("node is already running") + ErrNodeStopped = errors.New("node not started") + ErrNoRunningNode = errors.New("there is no running node") + ErrServiceUnknown = errors.New("service unknown") +) + +// NimbusStatusNode abstracts contained geth node and provides helper methods to +// interact with it. +type NimbusStatusNode struct { + mu sync.RWMutex + + //eventmux *event.TypeMux // Event multiplexer used between the services of a stack + + config *params.NodeConfig // Status node configuration + privateKey *ecdsa.PrivateKey + node nimbusbridge.Node + nodeRunning bool + rpcClient *rpc.Client // reference to public RPC client + rpcPrivateClient *rpc.Client // reference to private RPC client (can call private APIs) + + rpcAPIs []gethrpc.API // List of APIs currently provided by the node + inprocHandler *gethrpc.Server // In-process RPC request handler to process the API requests + inprocPublicHandler *gethrpc.Server // In-process RPC request handler to process the public API requests + + serviceFuncs []nimbussvc.ServiceConstructor // Service constructors (in dependency order) + services map[reflect.Type]nimbussvc.Service // Currently running services + + // discovery discovery.Discovery + // register *peers.Register + // peerPool *peers.PeerPool + db *leveldb.DB // used as a cache for PeerPool + + //stop chan struct{} // Channel to wait for termination notifications + lock sync.RWMutex + + log log.Logger +} + +// NewNimbus makes new instance of NimbusStatusNode. +func NewNimbus() *NimbusStatusNode { + return &NimbusStatusNode{ + //eventmux: new(event.TypeMux), + log: log.New("package", "status-go/node.NimbusStatusNode"), + } +} + +// Config exposes reference to running node's configuration +func (n *NimbusStatusNode) Config() *params.NodeConfig { + n.mu.RLock() + defer n.mu.RUnlock() + + return n.config +} + +// GethNode returns underlying geth node. +// func (n *NimbusStatusNode) GethNode() *node.Node { +// n.mu.RLock() +// defer n.mu.RUnlock() + +// return n.gethNode +// } + +// Server retrieves the currently running P2P network layer. +// func (n *NimbusStatusNode) Server() *p2p.Server { +// n.mu.RLock() +// defer n.mu.RUnlock() + +// if n.gethNode == nil { +// return nil +// } + +// return n.gethNode.Server() +// } + +// Start starts current NimbusStatusNode, failing if it's already started. +// It accepts a list of services that should be added to the node. +func (n *NimbusStatusNode) Start(config *params.NodeConfig, services ...nimbussvc.ServiceConstructor) error { + panic("Start") + return n.StartWithOptions(config, NimbusStartOptions{ + Services: services, + StartDiscovery: true, + // AccountsManager: accs, + }) +} + +// NimbusStartOptions allows to control some parameters of Start() method. +type NimbusStartOptions struct { + Node types.Node + Services []nimbussvc.ServiceConstructor + StartDiscovery bool + // AccountsManager *accounts.Manager +} + +// StartWithOptions starts current NimbusStatusNode, failing if it's already started. +// It takes some options that allows to further configure starting process. +func (n *NimbusStatusNode) StartWithOptions(config *params.NodeConfig, options NimbusStartOptions) error { + n.mu.Lock() + defer n.mu.Unlock() + + if n.isRunning() { + n.log.Debug("cannot start, node already running") + return ErrNodeRunning + } + + n.log.Debug("starting with NodeConfig", "ClusterConfig", config.ClusterConfig) + + db, err := db.Create(config.DataDir, params.StatusDatabase) + if err != nil { + return fmt.Errorf("failed to create database at %s: %v", config.DataDir, err) + } + + n.db = db + + err = n.startWithDB(config, db, options.Services) + + // continue only if there was no error when starting node with a db + if err == nil && options.StartDiscovery && n.discoveryEnabled() { + // err = n.startDiscovery() + } + + if err != nil { + if dberr := db.Close(); dberr != nil { + n.log.Error("error while closing leveldb after node crash", "error", dberr) + } + n.db = nil + return err + } + + return nil +} + +func (n *NimbusStatusNode) startWithDB(config *params.NodeConfig, db *leveldb.DB, services []nimbussvc.ServiceConstructor) error { + if err := n.createNode(config, services, db); err != nil { + return err + } + + if err := n.setupRPCClient(); err != nil { + return err + } + + return nil +} + +func (n *NimbusStatusNode) createNode(config *params.NodeConfig, services []nimbussvc.ServiceConstructor, db *leveldb.DB) error { + var privateKey *ecdsa.PrivateKey + if config.NodeKey != "" { + var err error + privateKey, err = crypto.HexToECDSA(config.NodeKey) + if err != nil { + return err + } + } + + n.privateKey = privateKey + n.node = nimbusbridge.NewNodeBridge() + + err := n.activateServices(config, db) + if err != nil { + return err + } + + if err = n.start(config, services); err != nil { + return err + } + + return nil +} + +// start starts current NimbusStatusNode, will fail if it's already started. +func (n *NimbusStatusNode) start(config *params.NodeConfig, services []nimbussvc.ServiceConstructor) error { + for _, service := range services { + if err := n.Register(service); err != nil { + return err + } + } + + n.config = config + n.startServices() + + err := n.node.StartNimbus(n.privateKey, config.ListenAddr, true) + n.nodeRunning = err == nil + return err +} + +func (n *NimbusStatusNode) setupRPCClient() (err error) { + // setup public RPC client + gethNodeClient := gethrpc.DialInProc(n.inprocPublicHandler) + n.rpcClient, err = rpc.NewClient(gethNodeClient, n.config.UpstreamConfig) + if err != nil { + return + } + + // setup private RPC client + gethNodePrivateClient := gethrpc.DialInProc(n.inprocHandler) + n.rpcPrivateClient, err = rpc.NewClient(gethNodePrivateClient, n.config.UpstreamConfig) + + return +} + +func (n *NimbusStatusNode) discoveryEnabled() bool { + return n.config != nil && (!n.config.NoDiscovery || n.config.Rendezvous) && n.config.ClusterConfig.Enabled +} + +// func (n *NimbusStatusNode) discoverNode() (*enode.Node, error) { +// if !n.isRunning() { +// return nil, nil +// } + +// server := n.gethNode.Server() +// discNode := server.Self() + +// if n.config.AdvertiseAddr == "" { +// return discNode, nil +// } + +// n.log.Info("Using AdvertiseAddr for rendezvous", "addr", n.config.AdvertiseAddr) + +// r := discNode.Record() +// r.Set(enr.IP(net.ParseIP(n.config.AdvertiseAddr))) +// if err := enode.SignV4(r, server.PrivateKey); err != nil { +// return nil, err +// } +// return enode.New(enode.ValidSchemes[r.IdentityScheme()], r) +// } + +// func (n *NimbusStatusNode) startRendezvous() (discovery.Discovery, error) { +// if !n.config.Rendezvous { +// return nil, errors.New("rendezvous is not enabled") +// } +// if len(n.config.ClusterConfig.RendezvousNodes) == 0 { +// return nil, errors.New("rendezvous node must be provided if rendezvous discovery is enabled") +// } +// maddrs := make([]ma.Multiaddr, len(n.config.ClusterConfig.RendezvousNodes)) +// for i, addr := range n.config.ClusterConfig.RendezvousNodes { +// var err error +// maddrs[i], err = ma.NewMultiaddr(addr) +// if err != nil { +// return nil, fmt.Errorf("failed to parse rendezvous node %s: %v", n.config.ClusterConfig.RendezvousNodes[0], err) +// } +// } +// node, err := n.discoverNode() +// if err != nil { +// return nil, fmt.Errorf("failed to get a discover node: %v", err) +// } + +// return discovery.NewRendezvous(maddrs, n.gethNode.Server().PrivateKey, node) +// } + +// StartDiscovery starts the peers discovery protocols depending on the node config. +func (n *NimbusStatusNode) StartDiscovery() error { + n.mu.Lock() + defer n.mu.Unlock() + + if n.discoveryEnabled() { + // return n.startDiscovery() + } + + return nil +} + +// func (n *NimbusStatusNode) startDiscovery() error { +// if n.isDiscoveryRunning() { +// return ErrDiscoveryRunning +// } + +// discoveries := []discovery.Discovery{} +// if !n.config.NoDiscovery { +// discoveries = append(discoveries, discovery.NewDiscV5( +// n.gethNode.Server().PrivateKey, +// n.config.ListenAddr, +// parseNodesV5(n.config.ClusterConfig.BootNodes))) +// } +// if n.config.Rendezvous { +// d, err := n.startRendezvous() +// if err != nil { +// return err +// } +// discoveries = append(discoveries, d) +// } +// if len(discoveries) == 0 { +// return errors.New("wasn't able to register any discovery") +// } else if len(discoveries) > 1 { +// n.discovery = discovery.NewMultiplexer(discoveries) +// } else { +// n.discovery = discoveries[0] +// } +// log.Debug( +// "using discovery", +// "instance", reflect.TypeOf(n.discovery), +// "registerTopics", n.config.RegisterTopics, +// "requireTopics", n.config.RequireTopics, +// ) +// n.register = peers.NewRegister(n.discovery, n.config.RegisterTopics...) +// options := peers.NewDefaultOptions() +// // TODO(dshulyak) consider adding a flag to define this behaviour +// options.AllowStop = len(n.config.RegisterTopics) == 0 +// options.TrustedMailServers = parseNodesToNodeID(n.config.ClusterConfig.TrustedMailServers) + +// options.MailServerRegistryAddress = n.config.MailServerRegistryAddress + +// n.peerPool = peers.NewPeerPool( +// n.discovery, +// n.config.RequireTopics, +// peers.NewCache(n.db), +// options, +// ) +// if err := n.discovery.Start(); err != nil { +// return err +// } +// if err := n.register.Start(); err != nil { +// return err +// } +// return n.peerPool.Start(n.gethNode.Server(), n.rpcClient) +// } + +// Stop will stop current NimbusStatusNode. A stopped node cannot be resumed. +func (n *NimbusStatusNode) Stop() error { + n.mu.Lock() + defer n.mu.Unlock() + + if !n.isRunning() { + return ErrNoRunningNode + } + + var errs []error + + // Terminate all subsystems and collect any errors + if err := n.stop(); err != nil && err != ErrNodeStopped { + errs = append(errs, err) + } + // Report any errors that might have occurred + switch len(errs) { + case 0: + return nil + case 1: + return errs[0] + default: + return fmt.Errorf("%v", errs) + } +} + +// StopError is returned if a Node fails to stop either any of its registered +// services or itself. +type StopError struct { + Server error + Services map[reflect.Type]error +} + +// Error generates a textual representation of the stop error. +func (e *StopError) Error() string { + return fmt.Sprintf("server: %v, services: %v", e.Server, e.Services) +} + +// stop will stop current NimbusStatusNode. A stopped node cannot be resumed. +func (n *NimbusStatusNode) stop() error { + // if n.isDiscoveryRunning() { + // if err := n.stopDiscovery(); err != nil { + // n.log.Error("Error stopping the discovery components", "error", err) + // } + // n.register = nil + // n.peerPool = nil + // n.discovery = nil + // } + + // if err := n.gethNode.Stop(); err != nil { + // return err + // } + + // Terminate the API, services and the p2p server. + n.stopPublicInProc() + n.stopInProc() + n.rpcClient = nil + n.rpcPrivateClient = nil + n.rpcAPIs = nil + + failure := &StopError{ + Services: make(map[reflect.Type]error), + } + for kind, service := range n.services { + if err := service.Stop(); err != nil { + failure.Services[kind] = err + } + } + n.services = nil + // We need to clear `node` because config is passed to `Start()` + // and may be completely different. Similarly with `config`. + if n.node != nil { + n.node.Stop() + n.node = nil + } + n.nodeRunning = false + n.config = nil + + if n.db != nil { + err := n.db.Close() + + n.db = nil + + return err + } + + if len(failure.Services) > 0 { + return failure + } + return nil +} + +func (n *NimbusStatusNode) isDiscoveryRunning() bool { + return false //n.register != nil || n.peerPool != nil || n.discovery != nil +} + +// func (n *NimbusStatusNode) stopDiscovery() error { +// n.register.Stop() +// n.peerPool.Stop() +// return n.discovery.Stop() +// } + +// ResetChainData removes chain data if node is not running. +func (n *NimbusStatusNode) ResetChainData(config *params.NodeConfig) error { + n.mu.Lock() + defer n.mu.Unlock() + + if n.isRunning() { + return ErrNodeRunning + } + + chainDataDir := filepath.Join(config.DataDir, config.Name, "lightchaindata") + if _, err := os.Stat(chainDataDir); os.IsNotExist(err) { + return err + } + err := os.RemoveAll(chainDataDir) + if err == nil { + n.log.Info("Chain data has been removed", "dir", chainDataDir) + } + return err +} + +// IsRunning confirm that node is running. +func (n *NimbusStatusNode) IsRunning() bool { + n.mu.RLock() + defer n.mu.RUnlock() + + return n.isRunning() +} + +func (n *NimbusStatusNode) isRunning() bool { + return n.node != nil && n.nodeRunning // && n.gethNode.Server() != nil +} + +// populateStaticPeers connects current node with our publicly available LES/SHH/Swarm cluster +func (n *NimbusStatusNode) populateStaticPeers() error { + if !n.config.ClusterConfig.Enabled { + n.log.Info("Static peers are disabled") + return nil + } + + for _, enode := range n.config.ClusterConfig.StaticNodes { + if err := n.addPeer(enode); err != nil { + n.log.Error("Static peer addition failed", "error", err) + return err + } + n.log.Info("Static peer added", "enode", enode) + } + + return nil +} + +func (n *NimbusStatusNode) removeStaticPeers() error { + if !n.config.ClusterConfig.Enabled { + n.log.Info("Static peers are disabled") + return nil + } + + for _, enode := range n.config.ClusterConfig.StaticNodes { + if err := n.removePeer(enode); err != nil { + n.log.Error("Static peer deletion failed", "error", err) + return err + } + n.log.Info("Static peer deleted", "enode", enode) + } + return nil +} + +// ReconnectStaticPeers removes and adds static peers to a server. +func (n *NimbusStatusNode) ReconnectStaticPeers() error { + n.mu.Lock() + defer n.mu.Unlock() + + if !n.isRunning() { + return ErrNoRunningNode + } + + if err := n.removeStaticPeers(); err != nil { + return err + } + + return n.populateStaticPeers() +} + +// AddPeer adds new static peer node +func (n *NimbusStatusNode) AddPeer(url string) error { + n.mu.RLock() + defer n.mu.RUnlock() + + return n.addPeer(url) +} + +// addPeer adds new static peer node +func (n *NimbusStatusNode) addPeer(url string) error { + if !n.isRunning() { + return ErrNoRunningNode + } + + n.node.AddPeer(url) + + return nil +} + +func (n *NimbusStatusNode) removePeer(url string) error { + if !n.isRunning() { + return ErrNoRunningNode + } + + n.node.RemovePeer(url) + + return nil +} + +// PeerCount returns the number of connected peers. +func (n *NimbusStatusNode) PeerCount() int { + n.mu.RLock() + defer n.mu.RUnlock() + + if !n.isRunning() { + return 0 + } + + return 1 + //return n.gethNode.Server().PeerCount() +} + +// Service retrieves a currently running service registered of a specific type. +func (n *NimbusStatusNode) Service(service interface{}) error { + n.lock.RLock() + defer n.lock.RUnlock() + + // Short circuit if the node's not running + if !n.isRunning() { + return ErrNodeStopped + } + // Otherwise try to find the service to return + element := reflect.ValueOf(service).Elem() + if running, ok := n.services[element.Type()]; ok { + element.Set(reflect.ValueOf(running)) + return nil + } + return ErrServiceUnknown +} + +// // LightEthereumService exposes reference to LES service running on top of the node +// func (n *NimbusStatusNode) LightEthereumService() (l *les.LightEthereum, err error) { +// n.mu.RLock() +// defer n.mu.RUnlock() + +// err = n.Service(&l) + +// return +// } + +// StatusService exposes reference to status service running on top of the node +func (n *NimbusStatusNode) StatusService() (st *status.Service, err error) { + n.mu.RLock() + defer n.mu.RUnlock() + + err = n.Service(&st) + + return +} + +// // PeerService exposes reference to peer service running on top of the node. +// func (n *NimbusStatusNode) PeerService() (st *peer.Service, err error) { +// n.mu.RLock() +// defer n.mu.RUnlock() + +// err = n.Service(&st) + +// return +// } + +// WhisperService exposes reference to Whisper service running on top of the node +func (n *NimbusStatusNode) WhisperService() (w *nodebridge.WhisperService, err error) { + n.mu.RLock() + defer n.mu.RUnlock() + + err = n.Service(&w) + + return +} + +// ShhExtService exposes reference to shh extension service running on top of the node +func (n *NimbusStatusNode) ShhExtService() (s *shhext.NimbusService, err error) { + n.mu.RLock() + defer n.mu.RUnlock() + + err = n.Service(&s) + + return +} + +// // WalletService returns wallet.Service instance if it was started. +// func (n *NimbusStatusNode) WalletService() (s *wallet.Service, err error) { +// n.mu.RLock() +// defer n.mu.RUnlock() +// err = n.Service(&s) +// return +// } + +// // BrowsersService returns browsers.Service instance if it was started. +// func (n *NimbusStatusNode) BrowsersService() (s *browsers.Service, err error) { +// n.mu.RLock() +// defer n.mu.RUnlock() +// err = n.Service(&s) +// return +// } + +// // PermissionsService returns browsers.Service instance if it was started. +// func (n *NimbusStatusNode) PermissionsService() (s *permissions.Service, err error) { +// n.mu.RLock() +// defer n.mu.RUnlock() +// err = n.Service(&s) +// return +// } + +// // AccountManager exposes reference to node's accounts manager +// func (n *NimbusStatusNode) AccountManager() (*accounts.Manager, error) { +// n.mu.RLock() +// defer n.mu.RUnlock() + +// if n.gethNode == nil { +// return nil, ErrNoGethNode +// } + +// return n.gethNode.AccountManager(), nil +// } + +// RPCClient exposes reference to RPC client connected to the running node. +func (n *NimbusStatusNode) RPCClient() *rpc.Client { + n.mu.RLock() + defer n.mu.RUnlock() + return n.rpcClient +} + +// RPCPrivateClient exposes reference to RPC client connected to the running node +// that can call both public and private APIs. +func (n *NimbusStatusNode) RPCPrivateClient() *rpc.Client { + n.mu.Lock() + defer n.mu.Unlock() + return n.rpcPrivateClient +} + +// ChaosModeCheckRPCClientsUpstreamURL updates RPCClient and RPCPrivateClient upstream URLs, +// if defined, without restarting the node. This is required for the Chaos Unicorn Day. +// Additionally, if the passed URL is Infura, it changes it to httpstat.us/500. +func (n *NimbusStatusNode) ChaosModeCheckRPCClientsUpstreamURL(on bool) error { + url := n.config.UpstreamConfig.URL + + if on { + if strings.Contains(url, "infura.io") { + url = "https://httpstat.us/500" + } + } + + publicClient := n.RPCClient() + if publicClient != nil { + if err := publicClient.UpdateUpstreamURL(url); err != nil { + return err + } + } + + privateClient := n.RPCPrivateClient() + if privateClient != nil { + if err := privateClient.UpdateUpstreamURL(url); err != nil { + return err + } + } + + return nil +} + +// EnsureSync waits until blockchain synchronization +// is complete and returns. +func (n *NimbusStatusNode) EnsureSync(ctx context.Context) error { + // Don't wait for any blockchain sync for the + // local private chain as blocks are never mined. + if n.config.NetworkID == 0 || n.config.NetworkID == params.StatusChainNetworkID { + return nil + } + + return n.ensureSync(ctx) +} + +func (n *NimbusStatusNode) ensureSync(ctx context.Context) error { + return errors.New("Sync not implemented") + // les, err := n.LightEthereumService() + // if err != nil { + // return fmt.Errorf("failed to get LES service: %v", err) + // } + + // downloader := les.Downloader() + // if downloader == nil { + // return errors.New("LightEthereumService downloader is nil") + // } + + // progress := downloader.Progress() + // if n.PeerCount() > 0 && progress.CurrentBlock >= progress.HighestBlock { + // n.log.Debug("Synchronization completed", "current block", progress.CurrentBlock, "highest block", progress.HighestBlock) + // return nil + // } + + // ticker := time.NewTicker(tickerResolution) + // defer ticker.Stop() + + // progressTicker := time.NewTicker(time.Minute) + // defer progressTicker.Stop() + + // for { + // select { + // case <-ctx.Done(): + // return errors.New("timeout during node synchronization") + // case <-ticker.C: + // if n.PeerCount() == 0 { + // n.log.Debug("No established connections with any peers, continue waiting for a sync") + // continue + // } + // if downloader.Synchronising() { + // n.log.Debug("Synchronization is in progress") + // continue + // } + // progress = downloader.Progress() + // if progress.CurrentBlock >= progress.HighestBlock { + // n.log.Info("Synchronization completed", "current block", progress.CurrentBlock, "highest block", progress.HighestBlock) + // return nil + // } + // n.log.Debug("Synchronization is not finished", "current", progress.CurrentBlock, "highest", progress.HighestBlock) + // case <-progressTicker.C: + // progress = downloader.Progress() + // n.log.Warn("Synchronization is not finished", "current", progress.CurrentBlock, "highest", progress.HighestBlock) + // } + // } +} + +// // Discover sets up the discovery for a specific topic. +// func (n *NimbusStatusNode) Discover(topic string, max, min int) (err error) { +// if n.peerPool == nil { +// return errors.New("peerPool not running") +// } +// return n.peerPool.UpdateTopic(topic, params.Limits{ +// Max: max, +// Min: min, +// }) +// } diff --git a/rpc/client.go b/rpc/client.go index a53fa26e75..803116252b 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -155,6 +155,10 @@ func (c *Client) CallContextIgnoringLocalHandlers(ctx context.Context, result in return client.CallContext(ctx, result, method, args...) } + if c.local == nil { + c.log.Warn("Local JSON-RPC endpoint missing", "method", method) + return errors.New("missing local JSON-RPC endpoint") + } return c.local.CallContext(ctx, result, method, args...) } diff --git a/services/accounts/service_nimbus.go b/services/accounts/service_nimbus.go new file mode 100644 index 0000000000..8f888d3e36 --- /dev/null +++ b/services/accounts/service_nimbus.go @@ -0,0 +1,15 @@ +// +build nimbus + +package accounts + +import ( + nimbussvc "github.com/status-im/status-go/services/nimbus" +) + +// Make sure that Service implements nimbussvc.Service interface. +var _ nimbussvc.Service = (*Service)(nil) + +// Start a service. +func (s *Service) StartService() error { + return nil +} diff --git a/services/nimbus/service.go b/services/nimbus/service.go new file mode 100644 index 0000000000..1d63365adb --- /dev/null +++ b/services/nimbus/service.go @@ -0,0 +1,84 @@ +// +build nimbus + +package nimbus + +import ( + "errors" + "fmt" + "reflect" + + gethrpc "github.com/ethereum/go-ethereum/rpc" + + "github.com/status-im/status-go/params" +) + +// errors +var ( + ErrNodeStopped = errors.New("node not started") + ErrServiceUnknown = errors.New("service unknown") +) + +// DuplicateServiceError is returned during Node startup if a registered service +// constructor returns a service of the same type that was already started. +type DuplicateServiceError struct { + Kind reflect.Type +} + +// Error generates a textual representation of the duplicate service error. +func (e *DuplicateServiceError) Error() string { + return fmt.Sprintf("duplicate service: %v", e.Kind) +} + +// ServiceContext is a collection of service independent options inherited from +// the protocol stack, that is passed to all constructors to be optionally used; +// as well as utility methods to operate on the service environment. +type ServiceContext struct { + config *params.NodeConfig + services map[reflect.Type]Service // Index of the already constructed services + // EventMux *event.TypeMux // Event multiplexer used for decoupled notifications + // AccountManager *accounts.Manager // Account manager created by the node. +} + +func NewServiceContext(config *params.NodeConfig, services map[reflect.Type]Service) *ServiceContext { + return &ServiceContext{ + config: config, + services: services, + } +} + +// Service retrieves a currently running service registered of a specific type. +func (ctx *ServiceContext) Service(service interface{}) error { + element := reflect.ValueOf(service).Elem() + if running, ok := ctx.services[element.Type()]; ok { + element.Set(reflect.ValueOf(running)) + return nil + } + return ErrServiceUnknown +} + +// ServiceConstructor is the function signature of the constructors needed to be +// registered for service instantiation. +type ServiceConstructor func(ctx *ServiceContext) (Service, error) + +// Service is an individual protocol that can be registered into a node. +// +// Notes: +// +// • Service life-cycle management is delegated to the node. The service is allowed to +// initialize itself upon creation, but no goroutines should be spun up outside of the +// Start method. +// +// • Restart logic is not required as the node will create a fresh instance +// every time a service is started. +type Service interface { + // APIs retrieves the list of RPC descriptors the service provides + APIs() []gethrpc.API + + // StartService is called after all services have been constructed and the networking + // layer was also initialized to spawn any goroutines required by the service. + StartService() error + + // Stop terminates all goroutines belonging to the service, blocking until they + // are all terminated. + Stop() error +} diff --git a/services/nodebridge/node_service_nimbus.go b/services/nodebridge/node_service_nimbus.go new file mode 100644 index 0000000000..a321a247a4 --- /dev/null +++ b/services/nodebridge/node_service_nimbus.go @@ -0,0 +1,14 @@ +// +build nimbus + +package nodebridge + +import ( + nimbussvc "github.com/status-im/status-go/services/nimbus" +) + +// Make sure that NodeService implements nimbussvc.Service interface. +var _ nimbussvc.Service = (*NodeService)(nil) + +func (w *NodeService) StartService() error { + return nil +} diff --git a/services/nodebridge/whisper_service_nimbus.go b/services/nodebridge/whisper_service_nimbus.go new file mode 100644 index 0000000000..4abef5041d --- /dev/null +++ b/services/nodebridge/whisper_service_nimbus.go @@ -0,0 +1,14 @@ +// +build nimbus + +package nodebridge + +import ( + nimbussvc "github.com/status-im/status-go/services/nimbus" +) + +// Make sure that WhisperService implements nimbussvc.Service interface. +var _ nimbussvc.Service = (*WhisperService)(nil) + +func (w *WhisperService) StartService() error { + return nil +} diff --git a/services/rpcfilters/service_nimbus.go b/services/rpcfilters/service_nimbus.go new file mode 100644 index 0000000000..e61f8c1735 --- /dev/null +++ b/services/rpcfilters/service_nimbus.go @@ -0,0 +1,15 @@ +// +build nimbus + +package rpcfilters + +import ( + nimbussvc "github.com/status-im/status-go/services/nimbus" +) + +// Make sure that Service implements nimbussvc.Service interface. +var _ nimbussvc.Service = (*Service)(nil) + +// StartService is run when a service is started. +func (s *Service) StartService() error { + return s.Start(nil) +} diff --git a/services/shhext/api.go b/services/shhext/api.go index 2744f68ee0..645f8eea6b 100644 --- a/services/shhext/api.go +++ b/services/shhext/api.go @@ -1,29 +1,10 @@ package shhext import ( - "context" - "crypto/ecdsa" - "encoding/hex" "errors" - "fmt" - "math/big" "time" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rlp" - - "github.com/status-im/status-go/db" - "github.com/status-im/status-go/mailserver" - "github.com/status-im/status-go/services/shhext/mailservers" - "github.com/status-im/status-go/whisper/v6" - - gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" "github.com/status-im/status-go/eth-node/types" - enstypes "github.com/status-im/status-go/eth-node/types/ens" - "github.com/status-im/status-go/protocol" - "github.com/status-im/status-go/protocol/encryption/multidevice" - "github.com/status-im/status-go/protocol/transport" ) const ( @@ -151,18 +132,6 @@ type SyncMessagesRequest struct { Topics []types.TopicType `json:"topics"` } -// SyncMessagesResponse is a response from the mail server -// to which SyncMessagesRequest was sent. -type SyncMessagesResponse struct { - // Cursor from the response can be used to retrieve more messages - // for the previous request. - Cursor string `json:"cursor"` - - // Error indicates that something wrong happened when sending messages - // to the requester. - Error string `json:"error"` -} - // InitiateHistoryRequestParams type for initiating history requests from a peer. type InitiateHistoryRequestParams struct { Peer string @@ -172,233 +141,16 @@ type InitiateHistoryRequestParams struct { Timeout time.Duration } -// ----- -// PUBLIC API -// ----- - -// PublicAPI extends whisper public API. -type PublicAPI struct { - service *Service - publicAPI types.PublicWhisperAPI - log log.Logger -} - -// NewPublicAPI returns instance of the public API. -func NewPublicAPI(s *Service) *PublicAPI { - return &PublicAPI{ - service: s, - publicAPI: s.w.PublicWhisperAPI(), - log: log.New("package", "status-go/services/sshext.PublicAPI"), - } -} - -func (api *PublicAPI) getPeer(rawurl string) (*enode.Node, error) { - if len(rawurl) == 0 { - return mailservers.GetFirstConnected(api.service.server, api.service.peerStore) - } - return enode.ParseV4(rawurl) -} - -// RetryConfig specifies configuration for retries with timeout and max amount of retries. -type RetryConfig struct { - BaseTimeout time.Duration - // StepTimeout defines duration increase per each retry. - StepTimeout time.Duration - MaxRetries int -} - -// RequestMessagesSync repeats MessagesRequest using configuration in retry conf. -func (api *PublicAPI) RequestMessagesSync(conf RetryConfig, r MessagesRequest) (MessagesResponse, error) { - var resp MessagesResponse - - shh := api.service.w - events := make(chan types.EnvelopeEvent, 10) - var ( - requestID types.HexBytes - err error - retries int - ) - for retries <= conf.MaxRetries { - sub := shh.SubscribeEnvelopeEvents(events) - r.Timeout = conf.BaseTimeout + conf.StepTimeout*time.Duration(retries) - timeout := r.Timeout - // FIXME this weird conversion is required because MessagesRequest expects seconds but defines time.Duration - r.Timeout = time.Duration(int(r.Timeout.Seconds())) - requestID, err = api.RequestMessages(context.Background(), r) - if err != nil { - sub.Unsubscribe() - return resp, err - } - mailServerResp, err := waitForExpiredOrCompleted(types.BytesToHash(requestID), events, timeout) - sub.Unsubscribe() - if err == nil { - resp.Cursor = hex.EncodeToString(mailServerResp.Cursor) - resp.Error = mailServerResp.Error - return resp, nil - } - retries++ - api.log.Error("[RequestMessagesSync] failed", "err", err, "retries", retries) - } - return resp, fmt.Errorf("failed to request messages after %d retries", retries) -} - -func waitForExpiredOrCompleted(requestID types.Hash, events chan types.EnvelopeEvent, timeout time.Duration) (*types.MailServerResponse, error) { - expired := fmt.Errorf("request %x expired", requestID) - after := time.NewTimer(timeout) - defer after.Stop() - for { - var ev types.EnvelopeEvent - select { - case ev = <-events: - case <-after.C: - return nil, expired - } - if ev.Hash != requestID { - continue - } - switch ev.Event { - case types.EventMailServerRequestCompleted: - data, ok := ev.Data.(*types.MailServerResponse) - if ok { - return data, nil - } - return nil, errors.New("invalid event data type") - case types.EventMailServerRequestExpired: - return nil, expired - } - } -} - -// RequestMessages sends a request for historic messages to a MailServer. -func (api *PublicAPI) RequestMessages(_ context.Context, r MessagesRequest) (types.HexBytes, error) { - api.log.Info("RequestMessages", "request", r) - shh := api.service.w - now := api.service.w.GetCurrentTime() - r.setDefaults(now) - - if r.From > r.To { - return nil, fmt.Errorf("Query range is invalid: from > to (%d > %d)", r.From, r.To) - } - - mailServerNode, err := api.getPeer(r.MailServerPeer) - if err != nil { - return nil, fmt.Errorf("%v: %v", ErrInvalidMailServerPeer, err) - } - - var ( - symKey []byte - publicKey *ecdsa.PublicKey - ) - - if r.SymKeyID != "" { - symKey, err = shh.GetSymKey(r.SymKeyID) - if err != nil { - return nil, fmt.Errorf("%v: %v", ErrInvalidSymKeyID, err) - } - } else { - publicKey = mailServerNode.Pubkey() - } - - payload, err := makeMessagesRequestPayload(r) - if err != nil { - return nil, err - } - - envelope, err := makeEnvelop( - payload, - symKey, - publicKey, - api.service.nodeID, - shh.MinPow(), - now, - ) - if err != nil { - return nil, err - } - hash := envelope.Hash() - - if !r.Force { - err = api.service.requestsRegistry.Register(hash, r.Topics) - if err != nil { - return nil, err - } - } - - if err := shh.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, r.Timeout*time.Second); err != nil { - if !r.Force { - api.service.requestsRegistry.Unregister(hash) - } - return nil, err - } - - return hash[:], nil -} - -// createSyncMailRequest creates SyncMailRequest. It uses a full bloom filter -// if no topics are given. -func createSyncMailRequest(r SyncMessagesRequest) (types.SyncMailRequest, error) { - var bloom []byte - if len(r.Topics) > 0 { - bloom = topicsToBloom(r.Topics...) - } else { - bloom = types.MakeFullNodeBloom() - } - - cursor, err := hex.DecodeString(r.Cursor) - if err != nil { - return types.SyncMailRequest{}, err - } - - return types.SyncMailRequest{ - Lower: r.From, - Upper: r.To, - Bloom: bloom, - Limit: r.Limit, - Cursor: cursor, - }, nil -} - -func createSyncMessagesResponse(r types.SyncEventResponse) SyncMessagesResponse { - return SyncMessagesResponse{ - Cursor: hex.EncodeToString(r.Cursor), - Error: r.Error, - } -} - -// SyncMessages sends a request to a given MailServerPeer to sync historic messages. -// MailServerPeers needs to be added as a trusted peer first. -func (api *PublicAPI) SyncMessages(ctx context.Context, r SyncMessagesRequest) (SyncMessagesResponse, error) { - log.Info("SyncMessages start", "request", r) - - var response SyncMessagesResponse - - mailServerEnode, err := enode.ParseV4(r.MailServerPeer) - if err != nil { - return response, fmt.Errorf("invalid MailServerPeer: %v", err) - } - mailServerID := mailServerEnode.ID().Bytes() - - request, err := createSyncMailRequest(r) - if err != nil { - return response, fmt.Errorf("failed to create a sync mail request: %v", err) - } - - for { - log.Info("Sending a request to sync messages", "request", request) - - resp, err := api.service.syncMessages(ctx, mailServerID, request) - if err != nil { - return response, err - } - - log.Info("Syncing messages response", "error", resp.Error, "cursor", fmt.Sprintf("%#x", resp.Cursor)) - - if resp.Error != "" || len(resp.Cursor) == 0 || !r.FollowCursor { - return createSyncMessagesResponse(resp), nil - } +// SyncMessagesResponse is a response from the mail server +// to which SyncMessagesRequest was sent. +type SyncMessagesResponse struct { + // Cursor from the response can be used to retrieve more messages + // for the previous request. + Cursor string `json:"cursor"` - request.Cursor = resp.Cursor - } + // Error indicates that something wrong happened when sending messages + // to the requester. + Error string `json:"error"` } type Author struct { @@ -413,421 +165,3 @@ type Metadata struct { MessageID types.HexBytes `json:"messageId"` Author Author `json:"author"` } - -// ConfirmMessagesProcessedByID is a method to confirm that messages was consumed by -// the client side. -// TODO: this is broken now as it requires dedup ID while a message hash should be used. -func (api *PublicAPI) ConfirmMessagesProcessedByID(messageConfirmations []*Metadata) error { - confirmationCount := len(messageConfirmations) - dedupIDs := make([][]byte, confirmationCount) - encryptionIDs := make([][]byte, confirmationCount) - for i, confirmation := range messageConfirmations { - dedupIDs[i] = confirmation.DedupID - encryptionIDs[i] = confirmation.EncryptionID - } - return api.service.ConfirmMessagesProcessed(encryptionIDs) -} - -// Post is used to send one-to-one for those who did not enabled device-to-device sync, -// in other words don't use PFS-enabled messages. Otherwise, SendDirectMessage is used. -// It's important to call PublicAPI.afterSend() so that the client receives a signal -// with confirmation that the message left the device. -func (api *PublicAPI) Post(ctx context.Context, newMessage types.NewMessage) (types.HexBytes, error) { - return api.publicAPI.Post(ctx, newMessage) -} - -// SendPublicMessage sends a public chat message to the underlying transport. -// Message's payload is a transit encoded message. -// It's important to call PublicAPI.afterSend() so that the client receives a signal -// with confirmation that the message left the device. -func (api *PublicAPI) SendPublicMessage(ctx context.Context, msg SendPublicMessageRPC) (types.HexBytes, error) { - chat := protocol.Chat{ - Name: msg.Chat, - } - return api.service.messenger.SendRaw(ctx, chat, msg.Payload) -} - -// SendDirectMessage sends a 1:1 chat message to the underlying transport -// Message's payload is a transit encoded message. -// It's important to call PublicAPI.afterSend() so that the client receives a signal -// with confirmation that the message left the device. -func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg SendDirectMessageRPC) (types.HexBytes, error) { - chat := protocol.Chat{ - ChatType: protocol.ChatTypeOneToOne, - ID: types.EncodeHex(msg.PubKey), - } - - return api.service.messenger.SendRaw(ctx, chat, msg.Payload) -} - -func (api *PublicAPI) Join(chat protocol.Chat) error { - return api.service.messenger.Join(chat) -} - -func (api *PublicAPI) Leave(chat protocol.Chat) error { - return api.service.messenger.Leave(chat) -} - -func (api *PublicAPI) LeaveGroupChat(ctx Context, chatID string) (*protocol.MessengerResponse, error) { - return api.service.messenger.LeaveGroupChat(ctx, chatID) -} - -func (api *PublicAPI) CreateGroupChatWithMembers(ctx Context, name string, members []string) (*protocol.MessengerResponse, error) { - return api.service.messenger.CreateGroupChatWithMembers(ctx, name, members) -} - -func (api *PublicAPI) AddMembersToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { - return api.service.messenger.AddMembersToGroupChat(ctx, chatID, members) -} - -func (api *PublicAPI) RemoveMemberFromGroupChat(ctx Context, chatID string, member string) (*protocol.MessengerResponse, error) { - return api.service.messenger.RemoveMemberFromGroupChat(ctx, chatID, member) -} - -func (api *PublicAPI) AddAdminsToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { - return api.service.messenger.AddAdminsToGroupChat(ctx, chatID, members) -} - -func (api *PublicAPI) ConfirmJoiningGroup(ctx context.Context, chatID string) (*protocol.MessengerResponse, error) { - return api.service.messenger.ConfirmJoiningGroup(ctx, chatID) -} - -func (api *PublicAPI) requestMessagesUsingPayload(request db.HistoryRequest, peer, symkeyID string, payload []byte, force bool, timeout time.Duration, topics []types.TopicType) (hash types.Hash, err error) { - shh := api.service.w - now := api.service.w.GetCurrentTime() - - mailServerNode, err := api.getPeer(peer) - if err != nil { - return hash, fmt.Errorf("%v: %v", ErrInvalidMailServerPeer, err) - } - - var ( - symKey []byte - publicKey *ecdsa.PublicKey - ) - - if symkeyID != "" { - symKey, err = shh.GetSymKey(symkeyID) - if err != nil { - return hash, fmt.Errorf("%v: %v", ErrInvalidSymKeyID, err) - } - } else { - publicKey = mailServerNode.Pubkey() - } - - envelope, err := makeEnvelop( - payload, - symKey, - publicKey, - api.service.nodeID, - shh.MinPow(), - now, - ) - if err != nil { - return hash, err - } - hash = envelope.Hash() - - err = request.Replace(hash) - if err != nil { - return hash, err - } - - if !force { - err = api.service.requestsRegistry.Register(hash, topics) - if err != nil { - return hash, err - } - } - - if err := shh.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, timeout); err != nil { - if !force { - api.service.requestsRegistry.Unregister(hash) - } - return hash, err - } - - return hash, nil - -} - -// InitiateHistoryRequests is a stateful API for initiating history request for each topic. -// Caller of this method needs to define only two parameters per each TopicRequest: -// - Topic -// - Duration in nanoseconds. Will be used to determine starting time for history request. -// After that status-go will guarantee that request for this topic and date will be performed. -func (api *PublicAPI) InitiateHistoryRequests(parent context.Context, request InitiateHistoryRequestParams) (rst []types.HexBytes, err error) { - tx := api.service.storage.NewTx() - defer func() { - if err == nil { - err = tx.Commit() - } - }() - ctx := NewContextFromService(parent, api.service, tx) - requests, err := api.service.historyUpdates.CreateRequests(ctx, request.Requests) - if err != nil { - return nil, err - } - var ( - payload []byte - hash types.Hash - ) - for i := range requests { - req := requests[i] - options := CreateTopicOptionsFromRequest(req) - bloom := options.ToBloomFilterOption() - payload, err = bloom.ToMessagesRequestPayload() - if err != nil { - return rst, err - } - hash, err = api.requestMessagesUsingPayload(req, request.Peer, request.SymKeyID, payload, request.Force, request.Timeout, options.Topics()) - if err != nil { - return rst, err - } - rst = append(rst, hash.Bytes()) - } - return rst, err -} - -// CompleteRequest client must mark request completed when all envelopes were processed. -func (api *PublicAPI) CompleteRequest(parent context.Context, hex string) (err error) { - tx := api.service.storage.NewTx() - ctx := NewContextFromService(parent, api.service, tx) - err = api.service.historyUpdates.UpdateFinishedRequest(ctx, types.HexToHash(hex)) - if err == nil { - return tx.Commit() - } - return err -} - -func (api *PublicAPI) LoadFilters(parent context.Context, chats []*transport.Filter) ([]*transport.Filter, error) { - return api.service.messenger.LoadFilters(chats) -} - -func (api *PublicAPI) SaveChat(parent context.Context, chat *protocol.Chat) error { - api.log.Info("saving chat", "chat", chat) - return api.service.messenger.SaveChat(chat) -} - -func (api *PublicAPI) Chats(parent context.Context) []*protocol.Chat { - return api.service.messenger.Chats() -} - -func (api *PublicAPI) DeleteChat(parent context.Context, chatID string) error { - return api.service.messenger.DeleteChat(chatID) -} - -func (api *PublicAPI) SaveContact(parent context.Context, contact *protocol.Contact) error { - return api.service.messenger.SaveContact(contact) -} - -func (api *PublicAPI) BlockContact(parent context.Context, contact *protocol.Contact) ([]*protocol.Chat, error) { - api.log.Info("blocking contact", "contact", contact.ID) - return api.service.messenger.BlockContact(contact) -} - -func (api *PublicAPI) Contacts(parent context.Context) []*protocol.Contact { - return api.service.messenger.Contacts() -} - -func (api *PublicAPI) RemoveFilters(parent context.Context, chats []*transport.Filter) error { - return api.service.messenger.RemoveFilters(chats) -} - -// EnableInstallation enables an installation for multi-device sync. -func (api *PublicAPI) EnableInstallation(installationID string) error { - return api.service.messenger.EnableInstallation(installationID) -} - -// DisableInstallation disables an installation for multi-device sync. -func (api *PublicAPI) DisableInstallation(installationID string) error { - return api.service.messenger.DisableInstallation(installationID) -} - -// GetOurInstallations returns all the installations available given an identity -func (api *PublicAPI) GetOurInstallations() []*multidevice.Installation { - return api.service.messenger.Installations() -} - -// SetInstallationMetadata sets the metadata for our own installation -func (api *PublicAPI) SetInstallationMetadata(installationID string, data *multidevice.InstallationMetadata) error { - return api.service.messenger.SetInstallationMetadata(installationID, data) -} - -// VerifyENSNames takes a list of ensdetails and returns whether they match the public key specified -func (api *PublicAPI) VerifyENSNames(details []enstypes.ENSDetails) (map[string]enstypes.ENSResponse, error) { - return api.service.messenger.VerifyENSNames(api.service.config.VerifyENSURL, ensContractAddress, details) -} - -type ApplicationMessagesResponse struct { - Messages []*protocol.Message `json:"messages"` - Cursor string `json:"cursor"` -} - -func (api *PublicAPI) ChatMessages(chatID, cursor string, limit int) (*ApplicationMessagesResponse, error) { - messages, cursor, err := api.service.messenger.MessageByChatID(chatID, cursor, limit) - if err != nil { - return nil, err - } - - return &ApplicationMessagesResponse{ - Messages: messages, - Cursor: cursor, - }, nil -} - -func (api *PublicAPI) DeleteMessage(id string) error { - return api.service.messenger.DeleteMessage(id) -} - -func (api *PublicAPI) DeleteMessagesByChatID(id string) error { - return api.service.messenger.DeleteMessagesByChatID(id) -} - -func (api *PublicAPI) MarkMessagesSeen(chatID string, ids []string) error { - return api.service.messenger.MarkMessagesSeen(chatID, ids) -} - -func (api *PublicAPI) UpdateMessageOutgoingStatus(id, newOutgoingStatus string) error { - return api.service.messenger.UpdateMessageOutgoingStatus(id, newOutgoingStatus) -} - -func (api *PublicAPI) SendChatMessage(ctx context.Context, message *protocol.Message) (*protocol.MessengerResponse, error) { - return api.service.messenger.SendChatMessage(ctx, message) -} - -func (api *PublicAPI) ReSendChatMessage(ctx context.Context, messageID string) error { - return api.service.messenger.ReSendChatMessage(ctx, messageID) -} - -func (api *PublicAPI) RequestTransaction(ctx context.Context, chatID, value, contract, address string) (*protocol.MessengerResponse, error) { - return api.service.messenger.RequestTransaction(ctx, chatID, value, contract, address) -} - -func (api *PublicAPI) RequestAddressForTransaction(ctx context.Context, chatID, from, value, contract string) (*protocol.MessengerResponse, error) { - return api.service.messenger.RequestAddressForTransaction(ctx, chatID, from, value, contract) -} - -func (api *PublicAPI) DeclineRequestAddressForTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { - return api.service.messenger.DeclineRequestAddressForTransaction(ctx, messageID) -} - -func (api *PublicAPI) DeclineRequestTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { - return api.service.messenger.DeclineRequestTransaction(ctx, messageID) -} - -func (api *PublicAPI) AcceptRequestAddressForTransaction(ctx context.Context, messageID, address string) (*protocol.MessengerResponse, error) { - return api.service.messenger.AcceptRequestAddressForTransaction(ctx, messageID, address) -} - -func (api *PublicAPI) SendTransaction(ctx context.Context, chatID, value, contract, transactionHash string, signature types.HexBytes) (*protocol.MessengerResponse, error) { - return api.service.messenger.SendTransaction(ctx, chatID, value, contract, transactionHash, signature) -} - -func (api *PublicAPI) AcceptRequestTransaction(ctx context.Context, transactionHash, messageID string, signature types.HexBytes) (*protocol.MessengerResponse, error) { - return api.service.messenger.AcceptRequestTransaction(ctx, transactionHash, messageID, signature) -} - -func (api *PublicAPI) SendContactUpdates(ctx context.Context, name, picture string) error { - return api.service.messenger.SendContactUpdates(ctx, name, picture) -} - -func (api *PublicAPI) SendContactUpdate(ctx context.Context, contactID, name, picture string) (*protocol.MessengerResponse, error) { - return api.service.messenger.SendContactUpdate(ctx, contactID, name, picture) -} - -func (api *PublicAPI) SendPairInstallation(ctx context.Context) (*protocol.MessengerResponse, error) { - return api.service.messenger.SendPairInstallation(ctx) -} - -func (api *PublicAPI) SyncDevices(ctx context.Context, name, picture string) error { - return api.service.messenger.SyncDevices(ctx, name, picture) -} - -// ----- -// HELPER -// ----- - -// makeEnvelop makes an envelop for a historic messages request. -// Symmetric key is used to authenticate to MailServer. -// PK is the current node ID. -func makeEnvelop( - payload []byte, - symKey []byte, - publicKey *ecdsa.PublicKey, - nodeID *ecdsa.PrivateKey, - pow float64, - now time.Time, -) (types.Envelope, error) { - // TODO: replace with an types.Envelope creator passed to the API struct - params := whisper.MessageParams{ - PoW: pow, - Payload: payload, - WorkTime: defaultWorkTime, - Src: nodeID, - } - // Either symKey or public key is required. - // This condition is verified in `message.Wrap()` method. - if len(symKey) > 0 { - params.KeySym = symKey - } else if publicKey != nil { - params.Dst = publicKey - } - message, err := whisper.NewSentMessage(¶ms) - if err != nil { - return nil, err - } - envelope, err := message.Wrap(¶ms, now) - if err != nil { - return nil, err - } - return gethbridge.NewWhisperEnvelope(envelope), nil -} - -// makeMessagesRequestPayload makes a specific payload for MailServer -// to request historic messages. -func makeMessagesRequestPayload(r MessagesRequest) ([]byte, error) { - cursor, err := hex.DecodeString(r.Cursor) - if err != nil { - return nil, fmt.Errorf("invalid cursor: %v", err) - } - - if len(cursor) > 0 && len(cursor) != mailserver.CursorLength { - return nil, fmt.Errorf("invalid cursor size: expected %d but got %d", mailserver.CursorLength, len(cursor)) - } - - payload := mailserver.MessagesRequestPayload{ - Lower: r.From, - Upper: r.To, - Bloom: createBloomFilter(r), - Limit: r.Limit, - Cursor: cursor, - // Client must tell the MailServer if it supports batch responses. - // This can be removed in the future. - Batch: true, - } - - return rlp.EncodeToBytes(payload) -} - -func createBloomFilter(r MessagesRequest) []byte { - if len(r.Topics) > 0 { - return topicsToBloom(r.Topics...) - } - - return types.TopicToBloom(r.Topic) -} - -func topicsToBloom(topics ...types.TopicType) []byte { - i := new(big.Int) - for _, topic := range topics { - bloom := types.TopicToBloom(topic) - i.Or(i, new(big.Int).SetBytes(bloom[:])) - } - - combined := make([]byte, types.BloomFilterSize) - data := i.Bytes() - copy(combined[types.BloomFilterSize-len(data):], data[:]) - - return combined -} diff --git a/services/shhext/api_geth.go b/services/shhext/api_geth.go new file mode 100644 index 0000000000..2318749e32 --- /dev/null +++ b/services/shhext/api_geth.go @@ -0,0 +1,676 @@ +// +build !nimbus + +package shhext + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "errors" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/rlp" + + "github.com/status-im/status-go/db" + "github.com/status-im/status-go/mailserver" + "github.com/status-im/status-go/services/shhext/mailservers" + "github.com/status-im/status-go/whisper/v6" + + gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + "github.com/status-im/status-go/eth-node/types" + enstypes "github.com/status-im/status-go/eth-node/types/ens" + "github.com/status-im/status-go/protocol" + "github.com/status-im/status-go/protocol/encryption/multidevice" + "github.com/status-im/status-go/protocol/transport" +) + +// ----- +// PUBLIC API +// ----- + +// PublicAPI extends whisper public API. +type PublicAPI struct { + service *Service + publicAPI types.PublicWhisperAPI + log log.Logger +} + +// NewPublicAPI returns instance of the public API. +func NewPublicAPI(s *Service) *PublicAPI { + return &PublicAPI{ + service: s, + publicAPI: s.w.PublicWhisperAPI(), + log: log.New("package", "status-go/services/sshext.PublicAPI"), + } +} + +func (api *PublicAPI) getPeer(rawurl string) (*enode.Node, error) { + if len(rawurl) == 0 { + return mailservers.GetFirstConnected(api.service.server, api.service.peerStore) + } + return enode.ParseV4(rawurl) +} + +// RetryConfig specifies configuration for retries with timeout and max amount of retries. +type RetryConfig struct { + BaseTimeout time.Duration + // StepTimeout defines duration increase per each retry. + StepTimeout time.Duration + MaxRetries int +} + +// RequestMessagesSync repeats MessagesRequest using configuration in retry conf. +func (api *PublicAPI) RequestMessagesSync(conf RetryConfig, r MessagesRequest) (MessagesResponse, error) { + var resp MessagesResponse + + shh := api.service.w + events := make(chan types.EnvelopeEvent, 10) + var ( + requestID types.HexBytes + err error + retries int + ) + for retries <= conf.MaxRetries { + sub := shh.SubscribeEnvelopeEvents(events) + r.Timeout = conf.BaseTimeout + conf.StepTimeout*time.Duration(retries) + timeout := r.Timeout + // FIXME this weird conversion is required because MessagesRequest expects seconds but defines time.Duration + r.Timeout = time.Duration(int(r.Timeout.Seconds())) + requestID, err = api.RequestMessages(context.Background(), r) + if err != nil { + sub.Unsubscribe() + return resp, err + } + mailServerResp, err := waitForExpiredOrCompleted(types.BytesToHash(requestID), events, timeout) + sub.Unsubscribe() + if err == nil { + resp.Cursor = hex.EncodeToString(mailServerResp.Cursor) + resp.Error = mailServerResp.Error + return resp, nil + } + retries++ + api.log.Error("[RequestMessagesSync] failed", "err", err, "retries", retries) + } + return resp, fmt.Errorf("failed to request messages after %d retries", retries) +} + +func waitForExpiredOrCompleted(requestID types.Hash, events chan types.EnvelopeEvent, timeout time.Duration) (*types.MailServerResponse, error) { + expired := fmt.Errorf("request %x expired", requestID) + after := time.NewTimer(timeout) + defer after.Stop() + for { + var ev types.EnvelopeEvent + select { + case ev = <-events: + case <-after.C: + return nil, expired + } + if ev.Hash != requestID { + continue + } + switch ev.Event { + case types.EventMailServerRequestCompleted: + data, ok := ev.Data.(*types.MailServerResponse) + if ok { + return data, nil + } + return nil, errors.New("invalid event data type") + case types.EventMailServerRequestExpired: + return nil, expired + } + } +} + +// RequestMessages sends a request for historic messages to a MailServer. +func (api *PublicAPI) RequestMessages(_ context.Context, r MessagesRequest) (types.HexBytes, error) { + api.log.Info("RequestMessages", "request", r) + shh := api.service.w + now := api.service.w.GetCurrentTime() + r.setDefaults(now) + + if r.From > r.To { + return nil, fmt.Errorf("Query range is invalid: from > to (%d > %d)", r.From, r.To) + } + + mailServerNode, err := api.getPeer(r.MailServerPeer) + if err != nil { + return nil, fmt.Errorf("%v: %v", ErrInvalidMailServerPeer, err) + } + + var ( + symKey []byte + publicKey *ecdsa.PublicKey + ) + + if r.SymKeyID != "" { + symKey, err = shh.GetSymKey(r.SymKeyID) + if err != nil { + return nil, fmt.Errorf("%v: %v", ErrInvalidSymKeyID, err) + } + } else { + publicKey = mailServerNode.Pubkey() + } + + payload, err := makeMessagesRequestPayload(r) + if err != nil { + return nil, err + } + + envelope, err := makeEnvelop( + payload, + symKey, + publicKey, + api.service.nodeID, + shh.MinPow(), + now, + ) + if err != nil { + return nil, err + } + hash := envelope.Hash() + + if !r.Force { + err = api.service.requestsRegistry.Register(hash, r.Topics) + if err != nil { + return nil, err + } + } + + if err := shh.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, r.Timeout*time.Second); err != nil { + if !r.Force { + api.service.requestsRegistry.Unregister(hash) + } + return nil, err + } + + return hash[:], nil +} + +// createSyncMailRequest creates SyncMailRequest. It uses a full bloom filter +// if no topics are given. +func createSyncMailRequest(r SyncMessagesRequest) (types.SyncMailRequest, error) { + var bloom []byte + if len(r.Topics) > 0 { + bloom = topicsToBloom(r.Topics...) + } else { + bloom = types.MakeFullNodeBloom() + } + + cursor, err := hex.DecodeString(r.Cursor) + if err != nil { + return types.SyncMailRequest{}, err + } + + return types.SyncMailRequest{ + Lower: r.From, + Upper: r.To, + Bloom: bloom, + Limit: r.Limit, + Cursor: cursor, + }, nil +} + +func createSyncMessagesResponse(r types.SyncEventResponse) SyncMessagesResponse { + return SyncMessagesResponse{ + Cursor: hex.EncodeToString(r.Cursor), + Error: r.Error, + } +} + +// SyncMessages sends a request to a given MailServerPeer to sync historic messages. +// MailServerPeers needs to be added as a trusted peer first. +func (api *PublicAPI) SyncMessages(ctx context.Context, r SyncMessagesRequest) (SyncMessagesResponse, error) { + log.Info("SyncMessages start", "request", r) + + var response SyncMessagesResponse + + mailServerEnode, err := enode.ParseV4(r.MailServerPeer) + if err != nil { + return response, fmt.Errorf("invalid MailServerPeer: %v", err) + } + mailServerID := mailServerEnode.ID().Bytes() + + request, err := createSyncMailRequest(r) + if err != nil { + return response, fmt.Errorf("failed to create a sync mail request: %v", err) + } + + for { + log.Info("Sending a request to sync messages", "request", request) + + resp, err := api.service.syncMessages(ctx, mailServerID, request) + if err != nil { + return response, err + } + + log.Info("Syncing messages response", "error", resp.Error, "cursor", fmt.Sprintf("%#x", resp.Cursor)) + + if resp.Error != "" || len(resp.Cursor) == 0 || !r.FollowCursor { + return createSyncMessagesResponse(resp), nil + } + + request.Cursor = resp.Cursor + } +} + +// ConfirmMessagesProcessedByID is a method to confirm that messages was consumed by +// the client side. +// TODO: this is broken now as it requires dedup ID while a message hash should be used. +func (api *PublicAPI) ConfirmMessagesProcessedByID(messageConfirmations []*Metadata) error { + confirmationCount := len(messageConfirmations) + dedupIDs := make([][]byte, confirmationCount) + encryptionIDs := make([][]byte, confirmationCount) + for i, confirmation := range messageConfirmations { + dedupIDs[i] = confirmation.DedupID + encryptionIDs[i] = confirmation.EncryptionID + } + return api.service.ConfirmMessagesProcessed(encryptionIDs) +} + +// Post is used to send one-to-one for those who did not enabled device-to-device sync, +// in other words don't use PFS-enabled messages. Otherwise, SendDirectMessage is used. +// It's important to call PublicAPI.afterSend() so that the client receives a signal +// with confirmation that the message left the device. +func (api *PublicAPI) Post(ctx context.Context, newMessage types.NewMessage) (types.HexBytes, error) { + return api.publicAPI.Post(ctx, newMessage) +} + +// SendPublicMessage sends a public chat message to the underlying transport. +// Message's payload is a transit encoded message. +// It's important to call PublicAPI.afterSend() so that the client receives a signal +// with confirmation that the message left the device. +func (api *PublicAPI) SendPublicMessage(ctx context.Context, msg SendPublicMessageRPC) (types.HexBytes, error) { + chat := protocol.Chat{ + Name: msg.Chat, + } + return api.service.messenger.SendRaw(ctx, chat, msg.Payload) +} + +// SendDirectMessage sends a 1:1 chat message to the underlying transport +// Message's payload is a transit encoded message. +// It's important to call PublicAPI.afterSend() so that the client receives a signal +// with confirmation that the message left the device. +func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg SendDirectMessageRPC) (types.HexBytes, error) { + chat := protocol.Chat{ + ChatType: protocol.ChatTypeOneToOne, + ID: types.EncodeHex(msg.PubKey), + } + + return api.service.messenger.SendRaw(ctx, chat, msg.Payload) +} + +func (api *PublicAPI) Join(chat protocol.Chat) error { + return api.service.messenger.Join(chat) +} + +func (api *PublicAPI) Leave(chat protocol.Chat) error { + return api.service.messenger.Leave(chat) +} + +func (api *PublicAPI) LeaveGroupChat(ctx Context, chatID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.LeaveGroupChat(ctx, chatID) +} + +func (api *PublicAPI) CreateGroupChatWithMembers(ctx Context, name string, members []string) (*protocol.MessengerResponse, error) { + return api.service.messenger.CreateGroupChatWithMembers(ctx, name, members) +} + +func (api *PublicAPI) AddMembersToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AddMembersToGroupChat(ctx, chatID, members) +} + +func (api *PublicAPI) RemoveMemberFromGroupChat(ctx Context, chatID string, member string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RemoveMemberFromGroupChat(ctx, chatID, member) +} + +func (api *PublicAPI) AddAdminsToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AddAdminsToGroupChat(ctx, chatID, members) +} + +func (api *PublicAPI) ConfirmJoiningGroup(ctx context.Context, chatID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.ConfirmJoiningGroup(ctx, chatID) +} + +func (api *PublicAPI) requestMessagesUsingPayload(request db.HistoryRequest, peer, symkeyID string, payload []byte, force bool, timeout time.Duration, topics []types.TopicType) (hash types.Hash, err error) { + shh := api.service.w + now := api.service.w.GetCurrentTime() + + mailServerNode, err := api.getPeer(peer) + if err != nil { + return hash, fmt.Errorf("%v: %v", ErrInvalidMailServerPeer, err) + } + + var ( + symKey []byte + publicKey *ecdsa.PublicKey + ) + + if symkeyID != "" { + symKey, err = shh.GetSymKey(symkeyID) + if err != nil { + return hash, fmt.Errorf("%v: %v", ErrInvalidSymKeyID, err) + } + } else { + publicKey = mailServerNode.Pubkey() + } + + envelope, err := makeEnvelop( + payload, + symKey, + publicKey, + api.service.nodeID, + shh.MinPow(), + now, + ) + if err != nil { + return hash, err + } + hash = envelope.Hash() + + err = request.Replace(hash) + if err != nil { + return hash, err + } + + if !force { + err = api.service.requestsRegistry.Register(hash, topics) + if err != nil { + return hash, err + } + } + + if err := shh.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, timeout); err != nil { + if !force { + api.service.requestsRegistry.Unregister(hash) + } + return hash, err + } + + return hash, nil + +} + +// InitiateHistoryRequests is a stateful API for initiating history request for each topic. +// Caller of this method needs to define only two parameters per each TopicRequest: +// - Topic +// - Duration in nanoseconds. Will be used to determine starting time for history request. +// After that status-go will guarantee that request for this topic and date will be performed. +func (api *PublicAPI) InitiateHistoryRequests(parent context.Context, request InitiateHistoryRequestParams) (rst []types.HexBytes, err error) { + tx := api.service.storage.NewTx() + defer func() { + if err == nil { + err = tx.Commit() + } + }() + ctx := NewContextFromService(parent, api.service, tx) + requests, err := api.service.historyUpdates.CreateRequests(ctx, request.Requests) + if err != nil { + return nil, err + } + var ( + payload []byte + hash types.Hash + ) + for i := range requests { + req := requests[i] + options := CreateTopicOptionsFromRequest(req) + bloom := options.ToBloomFilterOption() + payload, err = bloom.ToMessagesRequestPayload() + if err != nil { + return rst, err + } + hash, err = api.requestMessagesUsingPayload(req, request.Peer, request.SymKeyID, payload, request.Force, request.Timeout, options.Topics()) + if err != nil { + return rst, err + } + rst = append(rst, hash.Bytes()) + } + return rst, err +} + +// CompleteRequest client must mark request completed when all envelopes were processed. +func (api *PublicAPI) CompleteRequest(parent context.Context, hex string) (err error) { + tx := api.service.storage.NewTx() + ctx := NewContextFromService(parent, api.service, tx) + err = api.service.historyUpdates.UpdateFinishedRequest(ctx, types.HexToHash(hex)) + if err == nil { + return tx.Commit() + } + return err +} + +func (api *PublicAPI) LoadFilters(parent context.Context, chats []*transport.Filter) ([]*transport.Filter, error) { + return api.service.messenger.LoadFilters(chats) +} + +func (api *PublicAPI) SaveChat(parent context.Context, chat *protocol.Chat) error { + api.log.Info("saving chat", "chat", chat) + return api.service.messenger.SaveChat(chat) +} + +func (api *PublicAPI) Chats(parent context.Context) []*protocol.Chat { + return api.service.messenger.Chats() +} + +func (api *PublicAPI) DeleteChat(parent context.Context, chatID string) error { + return api.service.messenger.DeleteChat(chatID) +} + +func (api *PublicAPI) SaveContact(parent context.Context, contact *protocol.Contact) error { + return api.service.messenger.SaveContact(contact) +} + +func (api *PublicAPI) BlockContact(parent context.Context, contact *protocol.Contact) ([]*protocol.Chat, error) { + api.log.Info("blocking contact", "contact", contact.ID) + return api.service.messenger.BlockContact(contact) +} + +func (api *PublicAPI) Contacts(parent context.Context) []*protocol.Contact { + return api.service.messenger.Contacts() +} + +func (api *PublicAPI) RemoveFilters(parent context.Context, chats []*transport.Filter) error { + return api.service.messenger.RemoveFilters(chats) +} + +// EnableInstallation enables an installation for multi-device sync. +func (api *PublicAPI) EnableInstallation(installationID string) error { + return api.service.messenger.EnableInstallation(installationID) +} + +// DisableInstallation disables an installation for multi-device sync. +func (api *PublicAPI) DisableInstallation(installationID string) error { + return api.service.messenger.DisableInstallation(installationID) +} + +// GetOurInstallations returns all the installations available given an identity +func (api *PublicAPI) GetOurInstallations() []*multidevice.Installation { + return api.service.messenger.Installations() +} + +// SetInstallationMetadata sets the metadata for our own installation +func (api *PublicAPI) SetInstallationMetadata(installationID string, data *multidevice.InstallationMetadata) error { + return api.service.messenger.SetInstallationMetadata(installationID, data) +} + +// VerifyENSNames takes a list of ensdetails and returns whether they match the public key specified +func (api *PublicAPI) VerifyENSNames(details []enstypes.ENSDetails) (map[string]enstypes.ENSResponse, error) { + return api.service.messenger.VerifyENSNames(api.service.config.VerifyENSURL, ensContractAddress, details) +} + +type ApplicationMessagesResponse struct { + Messages []*protocol.Message `json:"messages"` + Cursor string `json:"cursor"` +} + +func (api *PublicAPI) ChatMessages(chatID, cursor string, limit int) (*ApplicationMessagesResponse, error) { + messages, cursor, err := api.service.messenger.MessageByChatID(chatID, cursor, limit) + if err != nil { + return nil, err + } + + return &ApplicationMessagesResponse{ + Messages: messages, + Cursor: cursor, + }, nil +} + +func (api *PublicAPI) DeleteMessage(id string) error { + return api.service.messenger.DeleteMessage(id) +} + +func (api *PublicAPI) DeleteMessagesByChatID(id string) error { + return api.service.messenger.DeleteMessagesByChatID(id) +} + +func (api *PublicAPI) MarkMessagesSeen(chatID string, ids []string) error { + return api.service.messenger.MarkMessagesSeen(chatID, ids) +} + +func (api *PublicAPI) UpdateMessageOutgoingStatus(id, newOutgoingStatus string) error { + return api.service.messenger.UpdateMessageOutgoingStatus(id, newOutgoingStatus) +} + +func (api *PublicAPI) SendChatMessage(ctx context.Context, message *protocol.Message) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendChatMessage(ctx, message) +} + +func (api *PublicAPI) ReSendChatMessage(ctx context.Context, messageID string) error { + return api.service.messenger.ReSendChatMessage(ctx, messageID) +} + +func (api *PublicAPI) RequestTransaction(ctx context.Context, chatID, value, contract, address string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RequestTransaction(ctx, chatID, value, contract, address) +} + +func (api *PublicAPI) RequestAddressForTransaction(ctx context.Context, chatID, from, value, contract string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RequestAddressForTransaction(ctx, chatID, from, value, contract) +} + +func (api *PublicAPI) DeclineRequestAddressForTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.DeclineRequestAddressForTransaction(ctx, messageID) +} + +func (api *PublicAPI) DeclineRequestTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.DeclineRequestTransaction(ctx, messageID) +} + +func (api *PublicAPI) AcceptRequestAddressForTransaction(ctx context.Context, messageID, address string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AcceptRequestAddressForTransaction(ctx, messageID, address) +} + +func (api *PublicAPI) SendTransaction(ctx context.Context, chatID, value, contract, transactionHash string, signature types.HexBytes) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendTransaction(ctx, chatID, value, contract, transactionHash, signature) +} + +func (api *PublicAPI) AcceptRequestTransaction(ctx context.Context, transactionHash, messageID string, signature types.HexBytes) (*protocol.MessengerResponse, error) { + return api.service.messenger.AcceptRequestTransaction(ctx, transactionHash, messageID, signature) +} + +func (api *PublicAPI) SendContactUpdates(ctx context.Context, name, picture string) error { + return api.service.messenger.SendContactUpdates(ctx, name, picture) +} + +func (api *PublicAPI) SendContactUpdate(ctx context.Context, contactID, name, picture string) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendContactUpdate(ctx, contactID, name, picture) +} + +func (api *PublicAPI) SendPairInstallation(ctx context.Context) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendPairInstallation(ctx) +} + +func (api *PublicAPI) SyncDevices(ctx context.Context, name, picture string) error { + return api.service.messenger.SyncDevices(ctx, name, picture) +} + +// ----- +// HELPER +// ----- + +// makeEnvelop makes an envelop for a historic messages request. +// Symmetric key is used to authenticate to MailServer. +// PK is the current node ID. +func makeEnvelop( + payload []byte, + symKey []byte, + publicKey *ecdsa.PublicKey, + nodeID *ecdsa.PrivateKey, + pow float64, + now time.Time, +) (types.Envelope, error) { + // TODO: replace with an types.Envelope creator passed to the API struct + params := whisper.MessageParams{ + PoW: pow, + Payload: payload, + WorkTime: defaultWorkTime, + Src: nodeID, + } + // Either symKey or public key is required. + // This condition is verified in `message.Wrap()` method. + if len(symKey) > 0 { + params.KeySym = symKey + } else if publicKey != nil { + params.Dst = publicKey + } + message, err := whisper.NewSentMessage(¶ms) + if err != nil { + return nil, err + } + envelope, err := message.Wrap(¶ms, now) + if err != nil { + return nil, err + } + return gethbridge.NewWhisperEnvelope(envelope), nil +} + +// makeMessagesRequestPayload makes a specific payload for MailServer +// to request historic messages. +func makeMessagesRequestPayload(r MessagesRequest) ([]byte, error) { + cursor, err := hex.DecodeString(r.Cursor) + if err != nil { + return nil, fmt.Errorf("invalid cursor: %v", err) + } + + if len(cursor) > 0 && len(cursor) != mailserver.CursorLength { + return nil, fmt.Errorf("invalid cursor size: expected %d but got %d", mailserver.CursorLength, len(cursor)) + } + + payload := mailserver.MessagesRequestPayload{ + Lower: r.From, + Upper: r.To, + Bloom: createBloomFilter(r), + Limit: r.Limit, + Cursor: cursor, + // Client must tell the MailServer if it supports batch responses. + // This can be removed in the future. + Batch: true, + } + + return rlp.EncodeToBytes(payload) +} + +func createBloomFilter(r MessagesRequest) []byte { + if len(r.Topics) > 0 { + return topicsToBloom(r.Topics...) + } + + return types.TopicToBloom(r.Topic) +} + +func topicsToBloom(topics ...types.TopicType) []byte { + i := new(big.Int) + for _, topic := range topics { + bloom := types.TopicToBloom(topic) + i.Or(i, new(big.Int).SetBytes(bloom[:])) + } + + combined := make([]byte, types.BloomFilterSize) + data := i.Bytes() + copy(combined[types.BloomFilterSize-len(data):], data[:]) + + return combined +} diff --git a/services/shhext/api_test.go b/services/shhext/api_geth_test.go similarity index 99% rename from services/shhext/api_test.go rename to services/shhext/api_geth_test.go index 3b5879fcba..86e9c207cc 100644 --- a/services/shhext/api_test.go +++ b/services/shhext/api_geth_test.go @@ -1,3 +1,5 @@ +// +build !nimbus + package shhext import ( diff --git a/services/shhext/api_nimbus.go b/services/shhext/api_nimbus.go new file mode 100644 index 0000000000..4ec231749d --- /dev/null +++ b/services/shhext/api_nimbus.go @@ -0,0 +1,661 @@ +// +build nimbus + +package shhext + +import ( + "context" + + "github.com/ethereum/go-ethereum/log" + + "github.com/status-im/status-go/eth-node/types" + enstypes "github.com/status-im/status-go/eth-node/types/ens" + "github.com/status-im/status-go/protocol" + "github.com/status-im/status-go/protocol/encryption/multidevice" + "github.com/status-im/status-go/protocol/transport" +) + +// ----- +// PUBLIC API +// ----- + +// NimbusPublicAPI extends whisper public API. +type NimbusPublicAPI struct { + service *NimbusService + publicAPI types.PublicWhisperAPI + log log.Logger +} + +// NewPublicAPI returns instance of the public API. +func NewNimbusPublicAPI(s *NimbusService) *NimbusPublicAPI { + return &NimbusPublicAPI{ + service: s, + publicAPI: s.w.PublicWhisperAPI(), + log: log.New("package", "status-go/services/sshext.NimbusPublicAPI"), + } +} + +// func (api *NimbusPublicAPI) getPeer(rawurl string) (*enode.Node, error) { +// if len(rawurl) == 0 { +// return mailservers.GetFirstConnected(api.service.server, api.service.peerStore) +// } +// return enode.ParseV4(rawurl) +// } + +// // RetryConfig specifies configuration for retries with timeout and max amount of retries. +// type RetryConfig struct { +// BaseTimeout time.Duration +// // StepTimeout defines duration increase per each retry. +// StepTimeout time.Duration +// MaxRetries int +// } + +// RequestMessagesSync repeats MessagesRequest using configuration in retry conf. +// func (api *NimbusPublicAPI) RequestMessagesSync(conf RetryConfig, r MessagesRequest) (MessagesResponse, error) { +// var resp MessagesResponse + +// shh := api.service.w +// events := make(chan types.EnvelopeEvent, 10) +// var ( +// requestID types.HexBytes +// err error +// retries int +// ) +// for retries <= conf.MaxRetries { +// sub := shh.SubscribeEnvelopeEvents(events) +// r.Timeout = conf.BaseTimeout + conf.StepTimeout*time.Duration(retries) +// timeout := r.Timeout +// // FIXME this weird conversion is required because MessagesRequest expects seconds but defines time.Duration +// r.Timeout = time.Duration(int(r.Timeout.Seconds())) +// requestID, err = api.RequestMessages(context.Background(), r) +// if err != nil { +// sub.Unsubscribe() +// return resp, err +// } +// mailServerResp, err := waitForExpiredOrCompleted(types.BytesToHash(requestID), events, timeout) +// sub.Unsubscribe() +// if err == nil { +// resp.Cursor = hex.EncodeToString(mailServerResp.Cursor) +// resp.Error = mailServerResp.Error +// return resp, nil +// } +// retries++ +// api.log.Error("[RequestMessagesSync] failed", "err", err, "retries", retries) +// } +// return resp, fmt.Errorf("failed to request messages after %d retries", retries) +// } + +// func waitForExpiredOrCompleted(requestID types.Hash, events chan types.EnvelopeEvent, timeout time.Duration) (*types.MailServerResponse, error) { +// expired := fmt.Errorf("request %x expired", requestID) +// after := time.NewTimer(timeout) +// defer after.Stop() +// for { +// var ev types.EnvelopeEvent +// select { +// case ev = <-events: +// case <-after.C: +// return nil, expired +// } +// if ev.Hash != requestID { +// continue +// } +// switch ev.Event { +// case types.EventMailServerRequestCompleted: +// data, ok := ev.Data.(*types.MailServerResponse) +// if ok { +// return data, nil +// } +// return nil, errors.New("invalid event data type") +// case types.EventMailServerRequestExpired: +// return nil, expired +// } +// } +// } + +// // RequestMessages sends a request for historic messages to a MailServer. +// func (api *NimbusPublicAPI) RequestMessages(_ context.Context, r MessagesRequest) (types.HexBytes, error) { +// api.log.Info("RequestMessages", "request", r) +// shh := api.service.w +// now := api.service.w.GetCurrentTime() +// r.setDefaults(now) + +// if r.From > r.To { +// return nil, fmt.Errorf("Query range is invalid: from > to (%d > %d)", r.From, r.To) +// } + +// mailServerNode, err := api.getPeer(r.MailServerPeer) +// if err != nil { +// return nil, fmt.Errorf("%v: %v", ErrInvalidMailServerPeer, err) +// } + +// var ( +// symKey []byte +// publicKey *ecdsa.PublicKey +// ) + +// if r.SymKeyID != "" { +// symKey, err = shh.GetSymKey(r.SymKeyID) +// if err != nil { +// return nil, fmt.Errorf("%v: %v", ErrInvalidSymKeyID, err) +// } +// } else { +// publicKey = mailServerNode.Pubkey() +// } + +// payload, err := makeMessagesRequestPayload(r) +// if err != nil { +// return nil, err +// } + +// envelope, err := makeEnvelop( +// payload, +// symKey, +// publicKey, +// api.service.nodeID, +// shh.MinPow(), +// now, +// ) +// if err != nil { +// return nil, err +// } +// hash := envelope.Hash() + +// if !r.Force { +// err = api.service.requestsRegistry.Register(hash, r.Topics) +// if err != nil { +// return nil, err +// } +// } + +// if err := shh.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, r.Timeout*time.Second); err != nil { +// if !r.Force { +// api.service.requestsRegistry.Unregister(hash) +// } +// return nil, err +// } + +// return hash[:], nil +// } + +// // createSyncMailRequest creates SyncMailRequest. It uses a full bloom filter +// // if no topics are given. +// func createSyncMailRequest(r SyncMessagesRequest) (types.SyncMailRequest, error) { +// var bloom []byte +// if len(r.Topics) > 0 { +// bloom = topicsToBloom(r.Topics...) +// } else { +// bloom = types.MakeFullNodeBloom() +// } + +// cursor, err := hex.DecodeString(r.Cursor) +// if err != nil { +// return types.SyncMailRequest{}, err +// } + +// return types.SyncMailRequest{ +// Lower: r.From, +// Upper: r.To, +// Bloom: bloom, +// Limit: r.Limit, +// Cursor: cursor, +// }, nil +// } + +// func createSyncMessagesResponse(r types.SyncEventResponse) SyncMessagesResponse { +// return SyncMessagesResponse{ +// Cursor: hex.EncodeToString(r.Cursor), +// Error: r.Error, +// } +// } + +// // SyncMessages sends a request to a given MailServerPeer to sync historic messages. +// // MailServerPeers needs to be added as a trusted peer first. +// func (api *NimbusPublicAPI) SyncMessages(ctx context.Context, r SyncMessagesRequest) (SyncMessagesResponse, error) { +// log.Info("SyncMessages start", "request", r) + +// var response SyncMessagesResponse + +// mailServerEnode, err := enode.ParseV4(r.MailServerPeer) +// if err != nil { +// return response, fmt.Errorf("invalid MailServerPeer: %v", err) +// } +// mailServerID := mailServerEnode.ID().Bytes() + +// request, err := createSyncMailRequest(r) +// if err != nil { +// return response, fmt.Errorf("failed to create a sync mail request: %v", err) +// } + +// for { +// log.Info("Sending a request to sync messages", "request", request) + +// resp, err := api.service.syncMessages(ctx, mailServerID, request) +// if err != nil { +// return response, err +// } + +// log.Info("Syncing messages response", "error", resp.Error, "cursor", fmt.Sprintf("%#x", resp.Cursor)) + +// if resp.Error != "" || len(resp.Cursor) == 0 || !r.FollowCursor { +// return createSyncMessagesResponse(resp), nil +// } + +// request.Cursor = resp.Cursor +// } +// } + +// ConfirmMessagesProcessedByID is a method to confirm that messages was consumed by +// the client side. +// TODO: this is broken now as it requires dedup ID while a message hash should be used. +func (api *NimbusPublicAPI) ConfirmMessagesProcessedByID(messageConfirmations []*Metadata) error { + confirmationCount := len(messageConfirmations) + dedupIDs := make([][]byte, confirmationCount) + encryptionIDs := make([][]byte, confirmationCount) + for i, confirmation := range messageConfirmations { + dedupIDs[i] = confirmation.DedupID + encryptionIDs[i] = confirmation.EncryptionID + } + return api.service.ConfirmMessagesProcessed(encryptionIDs) +} + +// Post is used to send one-to-one for those who did not enabled device-to-device sync, +// in other words don't use PFS-enabled messages. Otherwise, SendDirectMessage is used. +// It's important to call NimbusPublicAPI.afterSend() so that the client receives a signal +// with confirmation that the message left the device. +func (api *NimbusPublicAPI) Post(ctx context.Context, newMessage types.NewMessage) (types.HexBytes, error) { + return api.publicAPI.Post(ctx, newMessage) +} + +// SendPublicMessage sends a public chat message to the underlying transport. +// Message's payload is a transit encoded message. +// It's important to call NimbusPublicAPI.afterSend() so that the client receives a signal +// with confirmation that the message left the device. +func (api *NimbusPublicAPI) SendPublicMessage(ctx context.Context, msg SendPublicMessageRPC) (types.HexBytes, error) { + chat := protocol.Chat{ + Name: msg.Chat, + } + return api.service.messenger.SendRaw(ctx, chat, msg.Payload) +} + +// SendDirectMessage sends a 1:1 chat message to the underlying transport +// Message's payload is a transit encoded message. +// It's important to call NimbusPublicAPI.afterSend() so that the client receives a signal +// with confirmation that the message left the device. +func (api *NimbusPublicAPI) SendDirectMessage(ctx context.Context, msg SendDirectMessageRPC) (types.HexBytes, error) { + chat := protocol.Chat{ + ChatType: protocol.ChatTypeOneToOne, + ID: types.EncodeHex(msg.PubKey), + } + + return api.service.messenger.SendRaw(ctx, chat, msg.Payload) +} + +func (api *NimbusPublicAPI) Join(chat protocol.Chat) error { + return api.service.messenger.Join(chat) +} + +func (api *NimbusPublicAPI) Leave(chat protocol.Chat) error { + return api.service.messenger.Leave(chat) +} + +func (api *NimbusPublicAPI) LeaveGroupChat(ctx Context, chatID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.LeaveGroupChat(ctx, chatID) +} + +func (api *NimbusPublicAPI) CreateGroupChatWithMembers(ctx Context, name string, members []string) (*protocol.MessengerResponse, error) { + return api.service.messenger.CreateGroupChatWithMembers(ctx, name, members) +} + +func (api *NimbusPublicAPI) AddMembersToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AddMembersToGroupChat(ctx, chatID, members) +} + +func (api *NimbusPublicAPI) RemoveMemberFromGroupChat(ctx Context, chatID string, member string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RemoveMemberFromGroupChat(ctx, chatID, member) +} + +func (api *NimbusPublicAPI) AddAdminsToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AddAdminsToGroupChat(ctx, chatID, members) +} + +func (api *NimbusPublicAPI) ConfirmJoiningGroup(ctx context.Context, chatID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.ConfirmJoiningGroup(ctx, chatID) +} + +// func (api *NimbusPublicAPI) requestMessagesUsingPayload(request db.HistoryRequest, peer, symkeyID string, payload []byte, force bool, timeout time.Duration, topics []types.TopicType) (hash types.Hash, err error) { +// shh := api.service.w +// now := api.service.w.GetCurrentTime() + +// mailServerNode, err := api.getPeer(peer) +// if err != nil { +// return hash, fmt.Errorf("%v: %v", ErrInvalidMailServerPeer, err) +// } + +// var ( +// symKey []byte +// publicKey *ecdsa.PublicKey +// ) + +// if symkeyID != "" { +// symKey, err = shh.GetSymKey(symkeyID) +// if err != nil { +// return hash, fmt.Errorf("%v: %v", ErrInvalidSymKeyID, err) +// } +// } else { +// publicKey = mailServerNode.Pubkey() +// } + +// envelope, err := makeEnvelop( +// payload, +// symKey, +// publicKey, +// api.service.nodeID, +// shh.MinPow(), +// now, +// ) +// if err != nil { +// return hash, err +// } +// hash = envelope.Hash() + +// err = request.Replace(hash) +// if err != nil { +// return hash, err +// } + +// if !force { +// err = api.service.requestsRegistry.Register(hash, topics) +// if err != nil { +// return hash, err +// } +// } + +// if err := shh.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, timeout); err != nil { +// if !force { +// api.service.requestsRegistry.Unregister(hash) +// } +// return hash, err +// } + +// return hash, nil + +// } + +// // InitiateHistoryRequests is a stateful API for initiating history request for each topic. +// // Caller of this method needs to define only two parameters per each TopicRequest: +// // - Topic +// // - Duration in nanoseconds. Will be used to determine starting time for history request. +// // After that status-go will guarantee that request for this topic and date will be performed. +// func (api *NimbusPublicAPI) InitiateHistoryRequests(parent context.Context, request InitiateHistoryRequestParams) (rst []types.HexBytes, err error) { +// tx := api.service.storage.NewTx() +// defer func() { +// if err == nil { +// err = tx.Commit() +// } +// }() +// ctx := NewContextFromService(parent, api.service, tx) +// requests, err := api.service.historyUpdates.CreateRequests(ctx, request.Requests) +// if err != nil { +// return nil, err +// } +// var ( +// payload []byte +// hash types.Hash +// ) +// for i := range requests { +// req := requests[i] +// options := CreateTopicOptionsFromRequest(req) +// bloom := options.ToBloomFilterOption() +// payload, err = bloom.ToMessagesRequestPayload() +// if err != nil { +// return rst, err +// } +// hash, err = api.requestMessagesUsingPayload(req, request.Peer, request.SymKeyID, payload, request.Force, request.Timeout, options.Topics()) +// if err != nil { +// return rst, err +// } +// rst = append(rst, hash.Bytes()) +// } +// return rst, err +// } + +// // CompleteRequest client must mark request completed when all envelopes were processed. +// func (api *NimbusPublicAPI) CompleteRequest(parent context.Context, hex string) (err error) { +// tx := api.service.storage.NewTx() +// ctx := NewContextFromService(parent, api.service, tx) +// err = api.service.historyUpdates.UpdateFinishedRequest(ctx, types.HexToHash(hex)) +// if err == nil { +// return tx.Commit() +// } +// return err +// } + +func (api *NimbusPublicAPI) LoadFilters(parent context.Context, chats []*transport.Filter) ([]*transport.Filter, error) { + return api.service.messenger.LoadFilters(chats) +} + +func (api *NimbusPublicAPI) SaveChat(parent context.Context, chat *protocol.Chat) error { + api.log.Info("saving chat", "chat", chat) + return api.service.messenger.SaveChat(chat) +} + +func (api *NimbusPublicAPI) Chats(parent context.Context) []*protocol.Chat { + return api.service.messenger.Chats() +} + +func (api *NimbusPublicAPI) DeleteChat(parent context.Context, chatID string) error { + return api.service.messenger.DeleteChat(chatID) +} + +func (api *NimbusPublicAPI) SaveContact(parent context.Context, contact *protocol.Contact) error { + return api.service.messenger.SaveContact(contact) +} + +func (api *NimbusPublicAPI) BlockContact(parent context.Context, contact *protocol.Contact) ([]*protocol.Chat, error) { + api.log.Info("blocking contact", "contact", contact.ID) + return api.service.messenger.BlockContact(contact) +} + +func (api *NimbusPublicAPI) Contacts(parent context.Context) []*protocol.Contact { + return api.service.messenger.Contacts() +} + +func (api *NimbusPublicAPI) RemoveFilters(parent context.Context, chats []*transport.Filter) error { + return api.service.messenger.RemoveFilters(chats) +} + +// EnableInstallation enables an installation for multi-device sync. +func (api *NimbusPublicAPI) EnableInstallation(installationID string) error { + return api.service.messenger.EnableInstallation(installationID) +} + +// DisableInstallation disables an installation for multi-device sync. +func (api *NimbusPublicAPI) DisableInstallation(installationID string) error { + return api.service.messenger.DisableInstallation(installationID) +} + +// GetOurInstallations returns all the installations available given an identity +func (api *NimbusPublicAPI) GetOurInstallations() []*multidevice.Installation { + return api.service.messenger.Installations() +} + +// SetInstallationMetadata sets the metadata for our own installation +func (api *NimbusPublicAPI) SetInstallationMetadata(installationID string, data *multidevice.InstallationMetadata) error { + return api.service.messenger.SetInstallationMetadata(installationID, data) +} + +// VerifyENSNames takes a list of ensdetails and returns whether they match the public key specified +func (api *NimbusPublicAPI) VerifyENSNames(details []enstypes.ENSDetails) (map[string]enstypes.ENSResponse, error) { + return api.service.messenger.VerifyENSNames(api.service.config.VerifyENSURL, ensContractAddress, details) +} + +type ApplicationMessagesResponse struct { + Messages []*protocol.Message `json:"messages"` + Cursor string `json:"cursor"` +} + +func (api *NimbusPublicAPI) ChatMessages(chatID, cursor string, limit int) (*ApplicationMessagesResponse, error) { + messages, cursor, err := api.service.messenger.MessageByChatID(chatID, cursor, limit) + if err != nil { + return nil, err + } + + return &ApplicationMessagesResponse{ + Messages: messages, + Cursor: cursor, + }, nil +} + +func (api *NimbusPublicAPI) DeleteMessage(id string) error { + return api.service.messenger.DeleteMessage(id) +} + +func (api *NimbusPublicAPI) DeleteMessagesByChatID(id string) error { + return api.service.messenger.DeleteMessagesByChatID(id) +} + +func (api *NimbusPublicAPI) MarkMessagesSeen(chatID string, ids []string) error { + return api.service.messenger.MarkMessagesSeen(chatID, ids) +} + +func (api *NimbusPublicAPI) UpdateMessageOutgoingStatus(id, newOutgoingStatus string) error { + return api.service.messenger.UpdateMessageOutgoingStatus(id, newOutgoingStatus) +} + +func (api *NimbusPublicAPI) SendChatMessage(ctx context.Context, message *protocol.Message) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendChatMessage(ctx, message) +} + +func (api *NimbusPublicAPI) ReSendChatMessage(ctx context.Context, messageID string) error { + return api.service.messenger.ReSendChatMessage(ctx, messageID) +} + +func (api *NimbusPublicAPI) RequestTransaction(ctx context.Context, chatID, value, contract, address string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RequestTransaction(ctx, chatID, value, contract, address) +} + +func (api *NimbusPublicAPI) RequestAddressForTransaction(ctx context.Context, chatID, from, value, contract string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RequestAddressForTransaction(ctx, chatID, from, value, contract) +} + +func (api *NimbusPublicAPI) DeclineRequestAddressForTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.DeclineRequestAddressForTransaction(ctx, messageID) +} + +func (api *NimbusPublicAPI) DeclineRequestTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.DeclineRequestTransaction(ctx, messageID) +} + +func (api *NimbusPublicAPI) AcceptRequestAddressForTransaction(ctx context.Context, messageID, address string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AcceptRequestAddressForTransaction(ctx, messageID, address) +} + +func (api *NimbusPublicAPI) SendTransaction(ctx context.Context, chatID, value, contract, transactionHash string, signature types.HexBytes) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendTransaction(ctx, chatID, value, contract, transactionHash, signature) +} + +func (api *NimbusPublicAPI) AcceptRequestTransaction(ctx context.Context, transactionHash, messageID string, signature types.HexBytes) (*protocol.MessengerResponse, error) { + return api.service.messenger.AcceptRequestTransaction(ctx, transactionHash, messageID, signature) +} + +func (api *NimbusPublicAPI) SendContactUpdates(ctx context.Context, name, picture string) error { + return api.service.messenger.SendContactUpdates(ctx, name, picture) +} + +func (api *NimbusPublicAPI) SendContactUpdate(ctx context.Context, contactID, name, picture string) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendContactUpdate(ctx, contactID, name, picture) +} + +func (api *NimbusPublicAPI) SendPairInstallation(ctx context.Context) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendPairInstallation(ctx) +} + +func (api *NimbusPublicAPI) SyncDevices(ctx context.Context, name, picture string) error { + return api.service.messenger.SyncDevices(ctx, name, picture) +} + +// ----- +// HELPER +// ----- + +// makeEnvelop makes an envelop for a historic messages request. +// Symmetric key is used to authenticate to MailServer. +// PK is the current node ID. +// func makeEnvelop( +// payload []byte, +// symKey []byte, +// publicKey *ecdsa.PublicKey, +// nodeID *ecdsa.PrivateKey, +// pow float64, +// now time.Time, +// ) (types.Envelope, error) { +// params := whisper.MessageParams{ +// PoW: pow, +// Payload: payload, +// WorkTime: defaultWorkTime, +// Src: nodeID, +// } +// // Either symKey or public key is required. +// // This condition is verified in `message.Wrap()` method. +// if len(symKey) > 0 { +// params.KeySym = symKey +// } else if publicKey != nil { +// params.Dst = publicKey +// } +// message, err := whisper.NewSentMessage(¶ms) +// if err != nil { +// return nil, err +// } +// envelope, err := message.Wrap(¶ms, now) +// if err != nil { +// return nil, err +// } +// return nimbusbridge.NewNimbusEnvelopeWrapper(envelope), nil +// } + +// // makeMessagesRequestPayload makes a specific payload for MailServer +// // to request historic messages. +// func makeMessagesRequestPayload(r MessagesRequest) ([]byte, error) { +// cursor, err := hex.DecodeString(r.Cursor) +// if err != nil { +// return nil, fmt.Errorf("invalid cursor: %v", err) +// } + +// if len(cursor) > 0 && len(cursor) != mailserver.CursorLength { +// return nil, fmt.Errorf("invalid cursor size: expected %d but got %d", mailserver.CursorLength, len(cursor)) +// } + +// payload := mailserver.MessagesRequestPayload{ +// Lower: r.From, +// Upper: r.To, +// Bloom: createBloomFilter(r), +// Limit: r.Limit, +// Cursor: cursor, +// // Client must tell the MailServer if it supports batch responses. +// // This can be removed in the future. +// Batch: true, +// } + +// return rlp.EncodeToBytes(payload) +// } + +// func createBloomFilter(r MessagesRequest) []byte { +// if len(r.Topics) > 0 { +// return topicsToBloom(r.Topics...) +// } + +// return types.TopicToBloom(r.Topic) +// } + +// func topicsToBloom(topics ...types.TopicType) []byte { +// i := new(big.Int) +// for _, topic := range topics { +// bloom := types.TopicToBloom(topic) +// i.Or(i, new(big.Int).SetBytes(bloom[:])) +// } + +// combined := make([]byte, types.BloomFilterSize) +// data := i.Bytes() +// copy(combined[types.BloomFilterSize-len(data):], data[:]) + +// return combined +// } diff --git a/services/shhext/context.go b/services/shhext/context.go index 107506ed67..037857f681 100644 --- a/services/shhext/context.go +++ b/services/shhext/context.go @@ -23,11 +23,6 @@ var ( timeKey = NewContextKey("time") ) -// NewContextFromService creates new context instance using Service fileds directly and Storage. -func NewContextFromService(ctx context.Context, service *Service, storage db.Storage) Context { - return NewContext(ctx, service.w.GetCurrentTime, service.requestsRegistry, storage) -} - // NewContext creates Context with all required fields. func NewContext(ctx context.Context, source TimeSource, registry *RequestsRegistry, storage db.Storage) Context { ctx = context.WithValue(ctx, historyDBKey, db.NewHistoryStore(storage)) diff --git a/services/shhext/context_geth.go b/services/shhext/context_geth.go new file mode 100644 index 0000000000..e67c887ead --- /dev/null +++ b/services/shhext/context_geth.go @@ -0,0 +1,14 @@ +// +build !nimbus + +package shhext + +import ( + "context" + + "github.com/status-im/status-go/db" +) + +// NewContextFromService creates new context instance using Service fileds directly and Storage. +func NewContextFromService(ctx context.Context, service *Service, storage db.Storage) Context { + return NewContext(ctx, service.w.GetCurrentTime, service.requestsRegistry, storage) +} diff --git a/services/shhext/history.go b/services/shhext/history.go index 60ce72af53..f28665385e 100644 --- a/services/shhext/history.go +++ b/services/shhext/history.go @@ -1,17 +1,9 @@ package shhext import ( - "errors" - "fmt" - "sort" - "sync" "time" - "github.com/ethereum/go-ethereum/rlp" - - "github.com/status-im/status-go/db" "github.com/status-im/status-go/eth-node/types" - "github.com/status-im/status-go/mailserver" ) const ( @@ -20,331 +12,8 @@ const ( WhisperTimeAllowance = 20 * time.Second ) -// NewHistoryUpdateReactor creates HistoryUpdateReactor instance. -func NewHistoryUpdateReactor() *HistoryUpdateReactor { - return &HistoryUpdateReactor{} -} - -// HistoryUpdateReactor responsible for tracking progress for all history requests. -// It listens for 2 events: -// - when envelope from mail server is received we will update appropriate topic on disk -// - when confirmation for request completion is received - we will set last envelope timestamp as the last timestamp -// for all TopicLists in current request. -type HistoryUpdateReactor struct { - mu sync.Mutex -} - -// UpdateFinishedRequest removes successfully finished request and updates every topic -// attached to the request. -func (reactor *HistoryUpdateReactor) UpdateFinishedRequest(ctx Context, id types.Hash) error { - reactor.mu.Lock() - defer reactor.mu.Unlock() - req, err := ctx.HistoryStore().GetRequest(id) - if err != nil { - return err - } - for i := range req.Histories() { - th := &req.Histories()[i] - th.RequestID = types.Hash{} - th.Current = th.End - th.End = time.Time{} - if err := th.Save(); err != nil { - return err - } - } - return req.Delete() -} - -// UpdateTopicHistory updates Current timestamp for the TopicHistory with a given timestamp. -func (reactor *HistoryUpdateReactor) UpdateTopicHistory(ctx Context, topic types.TopicType, timestamp time.Time) error { - reactor.mu.Lock() - defer reactor.mu.Unlock() - histories, err := ctx.HistoryStore().GetHistoriesByTopic(topic) - if err != nil { - return err - } - if len(histories) == 0 { - return fmt.Errorf("no histories for topic 0x%x", topic) - } - for i := range histories { - th := &histories[i] - // this case could happen only iff envelopes were delivered out of order - // last envelope received, request completed, then others envelopes received - // request completed, last envelope received, and then all others envelopes received - if !th.Pending() { - continue - } - if timestamp.Before(th.End) && timestamp.After(th.Current) { - th.Current = timestamp - } - err := th.Save() - if err != nil { - return err - } - } - return nil -} - // TopicRequest defines what user has to provide. type TopicRequest struct { Topic types.TopicType Duration time.Duration } - -// CreateRequests receives list of topic with desired timestamps and initiates both pending requests and requests -// that cover new topics. -func (reactor *HistoryUpdateReactor) CreateRequests(ctx Context, topicRequests []TopicRequest) ([]db.HistoryRequest, error) { - reactor.mu.Lock() - defer reactor.mu.Unlock() - seen := map[types.TopicType]struct{}{} - for i := range topicRequests { - if _, exist := seen[topicRequests[i].Topic]; exist { - return nil, errors.New("only one duration per topic is allowed") - } - seen[topicRequests[i].Topic] = struct{}{} - } - histories := map[types.TopicType]db.TopicHistory{} - for i := range topicRequests { - th, err := ctx.HistoryStore().GetHistory(topicRequests[i].Topic, topicRequests[i].Duration) - if err != nil { - return nil, err - } - histories[th.Topic] = th - } - requests, err := ctx.HistoryStore().GetAllRequests() - if err != nil { - return nil, err - } - filtered := []db.HistoryRequest{} - for i := range requests { - req := requests[i] - for _, th := range histories { - if th.Pending() { - delete(histories, th.Topic) - } - } - if !ctx.RequestRegistry().Has(req.ID) { - filtered = append(filtered, req) - } - } - adjusted, err := adjustRequestedHistories(ctx.HistoryStore(), mapToList(histories)) - if err != nil { - return nil, err - } - filtered = append(filtered, - GroupHistoriesByRequestTimespan(ctx.HistoryStore(), adjusted)...) - return RenewRequests(filtered, ctx.Time()), nil -} - -// for every history that is not included in any request check if there are other ranges with such topic in db -// if so check if they can be merged -// if not then adjust second part so that End of it will be equal to First of previous -func adjustRequestedHistories(store db.HistoryStore, histories []db.TopicHistory) ([]db.TopicHistory, error) { - adjusted := []db.TopicHistory{} - for i := range histories { - all, err := store.GetHistoriesByTopic(histories[i].Topic) - if err != nil { - return nil, err - } - th, err := adjustRequestedHistory(&histories[i], all...) - if err != nil { - return nil, err - } - if th != nil { - adjusted = append(adjusted, *th) - } - } - return adjusted, nil -} - -func adjustRequestedHistory(th *db.TopicHistory, others ...db.TopicHistory) (*db.TopicHistory, error) { - sort.Slice(others, func(i, j int) bool { - return others[i].Duration > others[j].Duration - }) - if len(others) == 1 && others[0].Duration == th.Duration { - return th, nil - } - for j := range others { - if others[j].Duration == th.Duration { - // skip instance with same duration - continue - } else if th.Duration > others[j].Duration { - if th.Current.Equal(others[j].First) { - // this condition will be reached when query for new index successfully finished - th.Current = others[j].Current - // FIXME next two db operations must be completed atomically - err := th.Save() - if err != nil { - return nil, err - } - err = others[j].Delete() - if err != nil { - return nil, err - } - } else if (others[j].First != time.Time{}) { - // select First timestamp with lowest value. if there are multiple indexes that cover such ranges: - // 6:00 - 7:00 Duration: 3h - // 7:00 - 8:00 2h - // 8:00 - 9:00 1h - // and client created new index with Duration 4h - // 4h index must have End value set to 6:00 - if (others[j].First.Before(th.End) || th.End == time.Time{}) { - th.End = others[j].First - } - } else { - // remove previous if it is covered by new one - // client created multiple indexes without any succsefully executed query - err := others[j].Delete() - if err != nil { - return nil, err - } - } - } else if th.Duration < others[j].Duration { - if !others[j].Pending() { - th = &others[j] - } else { - return nil, nil - } - } - } - return th, nil -} - -// RenewRequests re-sets current, first and end timestamps. -// Changes should not be persisted on disk in this method. -func RenewRequests(requests []db.HistoryRequest, now time.Time) []db.HistoryRequest { - zero := time.Time{} - for i := range requests { - req := requests[i] - histories := req.Histories() - for j := range histories { - history := &histories[j] - if history.Current == zero { - history.Current = now.Add(-(history.Duration)) - } - if history.First == zero { - history.First = history.Current - } - if history.End == zero { - history.End = now - } - } - } - return requests -} - -// CreateTopicOptionsFromRequest transforms histories attached to a single request to a simpler format - TopicOptions. -func CreateTopicOptionsFromRequest(req db.HistoryRequest) TopicOptions { - histories := req.Histories() - rst := make(TopicOptions, len(histories)) - for i := range histories { - history := histories[i] - rst[i] = TopicOption{ - Topic: history.Topic, - Range: Range{ - Start: uint64(history.Current.Add(-(WhisperTimeAllowance)).Unix()), - End: uint64(history.End.Unix()), - }, - } - } - return rst -} - -func mapToList(topics map[types.TopicType]db.TopicHistory) []db.TopicHistory { - rst := make([]db.TopicHistory, 0, len(topics)) - for key := range topics { - rst = append(rst, topics[key]) - } - return rst -} - -// GroupHistoriesByRequestTimespan creates requests from provided histories. -// Multiple histories will be included into the same request only if they share timespan. -func GroupHistoriesByRequestTimespan(store db.HistoryStore, histories []db.TopicHistory) []db.HistoryRequest { - requests := []db.HistoryRequest{} - for _, th := range histories { - var added bool - for i := range requests { - req := &requests[i] - histories := req.Histories() - if histories[0].SameRange(th) { - req.AddHistory(th) - added = true - } - } - if !added { - req := store.NewRequest() - req.AddHistory(th) - requests = append(requests, req) - } - } - return requests -} - -// Range of the request. -type Range struct { - Start uint64 - End uint64 -} - -// TopicOption request for a single topic. -type TopicOption struct { - Topic types.TopicType - Range Range -} - -// TopicOptions is a list of topic-based requsts. -type TopicOptions []TopicOption - -// ToBloomFilterOption creates bloom filter request from a list of topics. -func (options TopicOptions) ToBloomFilterOption() BloomFilterOption { - topics := make([]types.TopicType, len(options)) - var start, end uint64 - for i := range options { - opt := options[i] - topics[i] = opt.Topic - if opt.Range.Start > start { - start = opt.Range.Start - } - if opt.Range.End > end { - end = opt.Range.End - } - } - - return BloomFilterOption{ - Range: Range{Start: start, End: end}, - Filter: topicsToBloom(topics...), - } -} - -// Topics returns list of whisper TopicType attached to each TopicOption. -func (options TopicOptions) Topics() []types.TopicType { - rst := make([]types.TopicType, len(options)) - for i := range options { - rst[i] = options[i].Topic - } - return rst -} - -// BloomFilterOption is a request based on bloom filter. -type BloomFilterOption struct { - Range Range - Filter []byte -} - -// ToMessagesRequestPayload creates mailserver.MessagesRequestPayload and encodes it to bytes using rlp. -func (filter BloomFilterOption) ToMessagesRequestPayload() ([]byte, error) { - // TODO fix this conversion. - // we start from time.Duration which is int64, then convert to uint64 for rlp-serilizability - // why uint32 here? max uint32 is smaller than max int64 - payload := mailserver.MessagesRequestPayload{ - Lower: uint32(filter.Range.Start), - Upper: uint32(filter.Range.End), - Bloom: filter.Filter, - // Client must tell the MailServer if it supports batch responses. - // This can be removed in the future. - Batch: true, - Limit: 1000, - } - return rlp.EncodeToBytes(payload) -} diff --git a/services/shhext/history_geth.go b/services/shhext/history_geth.go new file mode 100644 index 0000000000..0a21d81523 --- /dev/null +++ b/services/shhext/history_geth.go @@ -0,0 +1,340 @@ +// +build !nimbus + +package shhext + +import ( + "errors" + "fmt" + "sort" + "sync" + "time" + + "github.com/ethereum/go-ethereum/rlp" + + "github.com/status-im/status-go/db" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/mailserver" +) + +// NewHistoryUpdateReactor creates HistoryUpdateReactor instance. +func NewHistoryUpdateReactor() *HistoryUpdateReactor { + return &HistoryUpdateReactor{} +} + +// HistoryUpdateReactor responsible for tracking progress for all history requests. +// It listens for 2 events: +// - when envelope from mail server is received we will update appropriate topic on disk +// - when confirmation for request completion is received - we will set last envelope timestamp as the last timestamp +// for all TopicLists in current request. +type HistoryUpdateReactor struct { + mu sync.Mutex +} + +// UpdateFinishedRequest removes successfully finished request and updates every topic +// attached to the request. +func (reactor *HistoryUpdateReactor) UpdateFinishedRequest(ctx Context, id types.Hash) error { + reactor.mu.Lock() + defer reactor.mu.Unlock() + req, err := ctx.HistoryStore().GetRequest(id) + if err != nil { + return err + } + for i := range req.Histories() { + th := &req.Histories()[i] + th.RequestID = types.Hash{} + th.Current = th.End + th.End = time.Time{} + if err := th.Save(); err != nil { + return err + } + } + return req.Delete() +} + +// UpdateTopicHistory updates Current timestamp for the TopicHistory with a given timestamp. +func (reactor *HistoryUpdateReactor) UpdateTopicHistory(ctx Context, topic types.TopicType, timestamp time.Time) error { + reactor.mu.Lock() + defer reactor.mu.Unlock() + histories, err := ctx.HistoryStore().GetHistoriesByTopic(topic) + if err != nil { + return err + } + if len(histories) == 0 { + return fmt.Errorf("no histories for topic 0x%x", topic) + } + for i := range histories { + th := &histories[i] + // this case could happen only iff envelopes were delivered out of order + // last envelope received, request completed, then others envelopes received + // request completed, last envelope received, and then all others envelopes received + if !th.Pending() { + continue + } + if timestamp.Before(th.End) && timestamp.After(th.Current) { + th.Current = timestamp + } + err := th.Save() + if err != nil { + return err + } + } + return nil +} + +// CreateRequests receives list of topic with desired timestamps and initiates both pending requests and requests +// that cover new topics. +func (reactor *HistoryUpdateReactor) CreateRequests(ctx Context, topicRequests []TopicRequest) ([]db.HistoryRequest, error) { + reactor.mu.Lock() + defer reactor.mu.Unlock() + seen := map[types.TopicType]struct{}{} + for i := range topicRequests { + if _, exist := seen[topicRequests[i].Topic]; exist { + return nil, errors.New("only one duration per topic is allowed") + } + seen[topicRequests[i].Topic] = struct{}{} + } + histories := map[types.TopicType]db.TopicHistory{} + for i := range topicRequests { + th, err := ctx.HistoryStore().GetHistory(topicRequests[i].Topic, topicRequests[i].Duration) + if err != nil { + return nil, err + } + histories[th.Topic] = th + } + requests, err := ctx.HistoryStore().GetAllRequests() + if err != nil { + return nil, err + } + filtered := []db.HistoryRequest{} + for i := range requests { + req := requests[i] + for _, th := range histories { + if th.Pending() { + delete(histories, th.Topic) + } + } + if !ctx.RequestRegistry().Has(req.ID) { + filtered = append(filtered, req) + } + } + adjusted, err := adjustRequestedHistories(ctx.HistoryStore(), mapToList(histories)) + if err != nil { + return nil, err + } + filtered = append(filtered, + GroupHistoriesByRequestTimespan(ctx.HistoryStore(), adjusted)...) + return RenewRequests(filtered, ctx.Time()), nil +} + +// for every history that is not included in any request check if there are other ranges with such topic in db +// if so check if they can be merged +// if not then adjust second part so that End of it will be equal to First of previous +func adjustRequestedHistories(store db.HistoryStore, histories []db.TopicHistory) ([]db.TopicHistory, error) { + adjusted := []db.TopicHistory{} + for i := range histories { + all, err := store.GetHistoriesByTopic(histories[i].Topic) + if err != nil { + return nil, err + } + th, err := adjustRequestedHistory(&histories[i], all...) + if err != nil { + return nil, err + } + if th != nil { + adjusted = append(adjusted, *th) + } + } + return adjusted, nil +} + +func adjustRequestedHistory(th *db.TopicHistory, others ...db.TopicHistory) (*db.TopicHistory, error) { + sort.Slice(others, func(i, j int) bool { + return others[i].Duration > others[j].Duration + }) + if len(others) == 1 && others[0].Duration == th.Duration { + return th, nil + } + for j := range others { + if others[j].Duration == th.Duration { + // skip instance with same duration + continue + } else if th.Duration > others[j].Duration { + if th.Current.Equal(others[j].First) { + // this condition will be reached when query for new index successfully finished + th.Current = others[j].Current + // FIXME next two db operations must be completed atomically + err := th.Save() + if err != nil { + return nil, err + } + err = others[j].Delete() + if err != nil { + return nil, err + } + } else if (others[j].First != time.Time{}) { + // select First timestamp with lowest value. if there are multiple indexes that cover such ranges: + // 6:00 - 7:00 Duration: 3h + // 7:00 - 8:00 2h + // 8:00 - 9:00 1h + // and client created new index with Duration 4h + // 4h index must have End value set to 6:00 + if (others[j].First.Before(th.End) || th.End == time.Time{}) { + th.End = others[j].First + } + } else { + // remove previous if it is covered by new one + // client created multiple indexes without any succsefully executed query + err := others[j].Delete() + if err != nil { + return nil, err + } + } + } else if th.Duration < others[j].Duration { + if !others[j].Pending() { + th = &others[j] + } else { + return nil, nil + } + } + } + return th, nil +} + +// RenewRequests re-sets current, first and end timestamps. +// Changes should not be persisted on disk in this method. +func RenewRequests(requests []db.HistoryRequest, now time.Time) []db.HistoryRequest { + zero := time.Time{} + for i := range requests { + req := requests[i] + histories := req.Histories() + for j := range histories { + history := &histories[j] + if history.Current == zero { + history.Current = now.Add(-(history.Duration)) + } + if history.First == zero { + history.First = history.Current + } + if history.End == zero { + history.End = now + } + } + } + return requests +} + +// CreateTopicOptionsFromRequest transforms histories attached to a single request to a simpler format - TopicOptions. +func CreateTopicOptionsFromRequest(req db.HistoryRequest) TopicOptions { + histories := req.Histories() + rst := make(TopicOptions, len(histories)) + for i := range histories { + history := histories[i] + rst[i] = TopicOption{ + Topic: history.Topic, + Range: Range{ + Start: uint64(history.Current.Add(-(WhisperTimeAllowance)).Unix()), + End: uint64(history.End.Unix()), + }, + } + } + return rst +} + +func mapToList(topics map[types.TopicType]db.TopicHistory) []db.TopicHistory { + rst := make([]db.TopicHistory, 0, len(topics)) + for key := range topics { + rst = append(rst, topics[key]) + } + return rst +} + +// GroupHistoriesByRequestTimespan creates requests from provided histories. +// Multiple histories will be included into the same request only if they share timespan. +func GroupHistoriesByRequestTimespan(store db.HistoryStore, histories []db.TopicHistory) []db.HistoryRequest { + requests := []db.HistoryRequest{} + for _, th := range histories { + var added bool + for i := range requests { + req := &requests[i] + histories := req.Histories() + if histories[0].SameRange(th) { + req.AddHistory(th) + added = true + } + } + if !added { + req := store.NewRequest() + req.AddHistory(th) + requests = append(requests, req) + } + } + return requests +} + +// Range of the request. +type Range struct { + Start uint64 + End uint64 +} + +// TopicOption request for a single topic. +type TopicOption struct { + Topic types.TopicType + Range Range +} + +// TopicOptions is a list of topic-based requsts. +type TopicOptions []TopicOption + +// ToBloomFilterOption creates bloom filter request from a list of topics. +func (options TopicOptions) ToBloomFilterOption() BloomFilterOption { + topics := make([]types.TopicType, len(options)) + var start, end uint64 + for i := range options { + opt := options[i] + topics[i] = opt.Topic + if opt.Range.Start > start { + start = opt.Range.Start + } + if opt.Range.End > end { + end = opt.Range.End + } + } + + return BloomFilterOption{ + Range: Range{Start: start, End: end}, + Filter: topicsToBloom(topics...), + } +} + +// Topics returns list of whisper TopicType attached to each TopicOption. +func (options TopicOptions) Topics() []types.TopicType { + rst := make([]types.TopicType, len(options)) + for i := range options { + rst[i] = options[i].Topic + } + return rst +} + +// BloomFilterOption is a request based on bloom filter. +type BloomFilterOption struct { + Range Range + Filter []byte +} + +// ToMessagesRequestPayload creates mailserver.MessagesRequestPayload and encodes it to bytes using rlp. +func (filter BloomFilterOption) ToMessagesRequestPayload() ([]byte, error) { + // TODO fix this conversion. + // we start from time.Duration which is int64, then convert to uint64 for rlp-serilizability + // why uint32 here? max uint32 is smaller than max int64 + payload := mailserver.MessagesRequestPayload{ + Lower: uint32(filter.Range.Start), + Upper: uint32(filter.Range.End), + Bloom: filter.Filter, + // Client must tell the MailServer if it supports batch responses. + // This can be removed in the future. + Batch: true, + Limit: 1000, + } + return rlp.EncodeToBytes(payload) +} diff --git a/services/shhext/history_test.go b/services/shhext/history_geth_test.go similarity index 99% rename from services/shhext/history_test.go rename to services/shhext/history_geth_test.go index 87b34a6d75..27c6f6a423 100644 --- a/services/shhext/history_test.go +++ b/services/shhext/history_geth_test.go @@ -1,3 +1,5 @@ +// +build !nimbus + package shhext import ( diff --git a/services/shhext/mailrequests.go b/services/shhext/mailrequests.go index 3c06e243fd..c2a8819a91 100644 --- a/services/shhext/mailrequests.go +++ b/services/shhext/mailrequests.go @@ -1,3 +1,5 @@ +// +build !nimbus + package shhext import ( diff --git a/services/shhext/mailrequests_test.go b/services/shhext/mailrequests_test.go index f8ac4cf4b9..b6901b860d 100644 --- a/services/shhext/mailrequests_test.go +++ b/services/shhext/mailrequests_test.go @@ -1,3 +1,5 @@ +// +build !nimbus + package shhext import ( diff --git a/services/shhext/service.go b/services/shhext/service.go index 8a61cf2c09..0449f8c9b5 100644 --- a/services/shhext/service.go +++ b/services/shhext/service.go @@ -1,3 +1,5 @@ +// +build !nimbus + package shhext import ( diff --git a/services/shhext/service_nimbus.go b/services/shhext/service_nimbus.go new file mode 100644 index 0000000000..452dc93ec1 --- /dev/null +++ b/services/shhext/service_nimbus.go @@ -0,0 +1,448 @@ +// +build nimbus + +package shhext + +import ( + "context" + "crypto/ecdsa" + "database/sql" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/status-im/status-go/logutils" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/status-im/status-go/db" + "github.com/status-im/status-go/params" + nimbussvc "github.com/status-im/status-go/services/nimbus" + "github.com/status-im/status-go/signal" + + "github.com/syndtr/goleveldb/leveldb" + "go.uber.org/zap" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol" + "github.com/status-im/status-go/protocol/transport" +) + +const ( + // defaultConnectionsTarget used in Service.Start if configured connection target is 0. + defaultConnectionsTarget = 1 + // defaultTimeoutWaitAdded is a timeout to use to establish initial connections. + defaultTimeoutWaitAdded = 5 * time.Second +) + +// EnvelopeEventsHandler used for two different event types. +type EnvelopeEventsHandler interface { + EnvelopeSent([][]byte) + EnvelopeExpired([][]byte, error) + MailServerRequestCompleted(types.Hash, types.Hash, []byte, error) + MailServerRequestExpired(types.Hash) +} + +// NimbusService is a service that provides some additional Whisper API. +type NimbusService struct { + apiName string + messenger *protocol.Messenger + identity *ecdsa.PrivateKey + cancelMessenger chan struct{} + storage db.TransactionalStorage + n types.Node + w types.Whisper + config params.ShhextConfig + // mailMonitor *MailRequestMonitor + // requestsRegistry *RequestsRegistry + // historyUpdates *HistoryUpdateReactor + // server *p2p.Server + nodeID *ecdsa.PrivateKey + // peerStore *mailservers.PeerStore + // cache *mailservers.Cache + // connManager *mailservers.ConnectionManager + // lastUsedMonitor *mailservers.LastUsedConnectionMonitor + // accountsDB *accounts.Database +} + +// Make sure that NimbusService implements nimbussvc.Service interface. +var _ nimbussvc.Service = (*NimbusService)(nil) + +// NewNimbus returns a new shhext NimbusService. +func NewNimbus(n types.Node, ctx interface{}, apiName string, ldb *leveldb.DB, config params.ShhextConfig) *NimbusService { + w, err := n.GetWhisper(ctx) + if err != nil { + panic(err) + } + // cache := mailservers.NewCache(ldb) + // ps := mailservers.NewPeerStore(cache) + // delay := defaultRequestsDelay + // if config.RequestsDelay != 0 { + // delay = config.RequestsDelay + // } + // requestsRegistry := NewRequestsRegistry(delay) + // historyUpdates := NewHistoryUpdateReactor() + // mailMonitor := &MailRequestMonitor{ + // w: w, + // handler: handler, + // cache: map[types.Hash]EnvelopeState{}, + // requestsRegistry: requestsRegistry, + // } + return &NimbusService{ + apiName: apiName, + storage: db.NewLevelDBStorage(ldb), + n: n, + w: w, + config: config, + // mailMonitor: mailMonitor, + // requestsRegistry: requestsRegistry, + // historyUpdates: historyUpdates, + // peerStore: ps, + // cache: cache, + } +} + +func (s *NimbusService) InitProtocol(identity *ecdsa.PrivateKey, db *sql.DB) error { // nolint: gocyclo + if !s.config.PFSEnabled { + return nil + } + + // If Messenger has been already set up, we need to shut it down + // before we init it again. Otherwise, it will lead to goroutines leakage + // due to not stopped filters. + if s.messenger != nil { + if err := s.messenger.Shutdown(); err != nil { + return err + } + } + + s.identity = identity + + dataDir := filepath.Clean(s.config.BackupDisabledDataDir) + + if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { + return err + } + + // Create a custom zap.Logger which will forward logs from status-go/protocol to status-go logger. + zapLogger, err := logutils.NewZapLoggerWithAdapter(logutils.Logger()) + if err != nil { + return err + } + + // envelopesMonitorConfig := &protocolwhisper.EnvelopesMonitorConfig{ + // MaxAttempts: s.config.MaxMessageDeliveryAttempts, + // MailserverConfirmationsEnabled: s.config.MailServerConfirmations, + // IsMailserver: func(peer types.EnodeID) bool { + // return s.peerStore.Exist(peer) + // }, + // EnvelopeEventsHandler: EnvelopeSignalHandler{}, + // Logger: zapLogger, + // } + options := buildMessengerOptions(s.config, db, nil, zapLogger) + + messenger, err := protocol.NewMessenger( + identity, + s.n, + s.config.InstallationID, + options..., + ) + if err != nil { + return err + } + // s.accountsDB = accounts.NewDB(db) + s.messenger = messenger + // Start a loop that retrieves all messages and propagates them to status-react. + s.cancelMessenger = make(chan struct{}) + go s.retrieveMessagesLoop(time.Second, s.cancelMessenger) + // go s.verifyTransactionLoop(30*time.Second, s.cancelMessenger) + + return s.messenger.Init() +} + +func (s *NimbusService) retrieveMessagesLoop(tick time.Duration, cancel <-chan struct{}) { + ticker := time.NewTicker(tick) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + response, err := s.messenger.RetrieveAll() + if err != nil { + log.Error("failed to retrieve raw messages", "err", err) + continue + } + if !response.IsEmpty() { + PublisherSignalHandler{}.NewMessages(response) + } + case <-cancel: + return + } + } +} + +// type verifyTransactionClient struct { +// chainID *big.Int +// url string +// } + +// func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, bool, error) { +// signer := gethtypes.NewEIP155Signer(c.chainID) +// client, err := ethclient.Dial(c.url) +// if err != nil { +// return coretypes.Message{}, false, err +// } + +// transaction, pending, err := client.TransactionByHash(ctx, commongethtypes.BytesToHash(hash.Bytes())) +// if err != nil { +// return coretypes.Message{}, false, err +// } + +// message, err := transaction.AsMessage(signer) +// if err != nil { +// return coretypes.Message{}, false, err +// } +// from := types.BytesToAddress(message.From().Bytes()) +// to := types.BytesToAddress(message.To().Bytes()) + +// return coretypes.NewMessage( +// from, +// &to, +// message.Nonce(), +// message.Value(), +// message.Gas(), +// message.GasPrice(), +// message.Data(), +// message.CheckNonce(), +// ), pending, nil +// } + +// func (s *Service) verifyTransactionLoop(tick time.Duration, cancel <-chan struct{}) { +// if s.config.VerifyTransactionURL == "" { +// log.Warn("not starting transaction loop") +// return +// } + +// ticker := time.NewTicker(tick) +// defer ticker.Stop() + +// ctx, cancelVerifyTransaction := context.WithCancel(context.Background()) + +// for { +// select { +// case <-ticker.C: +// accounts, err := s.accountsDB.GetAccounts() +// if err != nil { +// log.Error("failed to retrieve accounts", "err", err) +// } +// var wallets []types.Address +// for _, account := range accounts { +// if account.Wallet { +// wallets = append(wallets, types.BytesToAddress(account.Address.Bytes())) +// } +// } + +// response, err := s.messenger.ValidateTransactions(ctx, wallets) +// if err != nil { +// log.Error("failed to validate transactions", "err", err) +// continue +// } +// if !response.IsEmpty() { +// PublisherSignalHandler{}.NewMessages(response) +// } +// case <-cancel: +// cancelVerifyTransaction() +// return +// } +// } +// } + +func (s *NimbusService) ConfirmMessagesProcessed(messageIDs [][]byte) error { + return s.messenger.ConfirmMessagesProcessed(messageIDs) +} + +func (s *NimbusService) EnableInstallation(installationID string) error { + return s.messenger.EnableInstallation(installationID) +} + +// DisableInstallation disables an installation for multi-device sync. +func (s *NimbusService) DisableInstallation(installationID string) error { + return s.messenger.DisableInstallation(installationID) +} + +// UpdateMailservers updates information about selected mail servers. +// func (s *NimbusService) UpdateMailservers(nodes []*enode.Node) error { +// // if err := s.peerStore.Update(nodes); err != nil { +// // return err +// // } +// // if s.connManager != nil { +// // s.connManager.Notify(nodes) +// // } +// return nil +// } + +// APIs returns a list of new APIs. +func (s *NimbusService) APIs() []rpc.API { + apis := []rpc.API{ + { + Namespace: s.apiName, + Version: "1.0", + Service: NewNimbusPublicAPI(s), + Public: true, + }, + } + return apis +} + +// Start is run when a service is started. +// It does nothing in this case but is required by `node.NimbusService` interface. +func (s *NimbusService) StartService() error { + if s.config.EnableConnectionManager { + // connectionsTarget := s.config.ConnectionTarget + // if connectionsTarget == 0 { + // connectionsTarget = defaultConnectionsTarget + // } + // maxFailures := s.config.MaxServerFailures + // // if not defined change server on first expired event + // if maxFailures == 0 { + // maxFailures = 1 + // } + // s.connManager = mailservers.NewConnectionManager(server, s.w, connectionsTarget, maxFailures, defaultTimeoutWaitAdded) + // s.connManager.Start() + // if err := mailservers.EnsureUsedRecordsAddedFirst(s.peerStore, s.connManager); err != nil { + // return err + // } + } + if s.config.EnableLastUsedMonitor { + // s.lastUsedMonitor = mailservers.NewLastUsedConnectionMonitor(s.peerStore, s.cache, s.w) + // s.lastUsedMonitor.Start() + } + // s.mailMonitor.Start() + // s.nodeID = server.PrivateKey + // s.server = server + return nil +} + +// Stop is run when a service is stopped. +func (s *NimbusService) Stop() error { + log.Info("Stopping shhext service") + // if s.config.EnableConnectionManager { + // s.connManager.Stop() + // } + // if s.config.EnableLastUsedMonitor { + // s.lastUsedMonitor.Stop() + // } + // s.requestsRegistry.Clear() + // s.mailMonitor.Stop() + + if s.cancelMessenger != nil { + select { + case <-s.cancelMessenger: + // channel already closed + default: + close(s.cancelMessenger) + s.cancelMessenger = nil + } + } + + if s.messenger != nil { + if err := s.messenger.Shutdown(); err != nil { + return err + } + } + + return nil +} + +func (s *NimbusService) syncMessages(ctx context.Context, mailServerID []byte, r types.SyncMailRequest) (resp types.SyncEventResponse, err error) { + err = s.w.SyncMessages(mailServerID, r) + if err != nil { + return + } + + // Wait for the response which is received asynchronously as a p2p packet. + // This packet handler will send an event which contains the response payload. + events := make(chan types.EnvelopeEvent, 1024) + sub := s.w.SubscribeEnvelopeEvents(events) + defer sub.Unsubscribe() + + // Add explicit timeout context, otherwise the request + // can hang indefinitely if not specified by the sender. + // Sender is usually through netcat or some bash tool + // so it's not really possible to specify the timeout. + timeoutCtx, cancel := context.WithTimeout(ctx, time.Second*30) + defer cancel() + + for { + select { + case event := <-events: + if event.Event != types.EventMailServerSyncFinished { + continue + } + + log.Info("received EventMailServerSyncFinished event", "data", event.Data) + + var ok bool + + resp, ok = event.Data.(types.SyncEventResponse) + if !ok { + err = fmt.Errorf("did not understand the response event data") + return + } + return + case <-timeoutCtx.Done(): + err = timeoutCtx.Err() + return + } + } +} + +func onNegotiatedFilters(filters []*transport.Filter) { + var signalFilters []*signal.Filter + for _, filter := range filters { + + signalFilter := &signal.Filter{ + ChatID: filter.ChatID, + SymKeyID: filter.SymKeyID, + Listen: filter.Listen, + FilterID: filter.FilterID, + Identity: filter.Identity, + Topic: filter.Topic, + } + + signalFilters = append(signalFilters, signalFilter) + } + if len(filters) != 0 { + handler := PublisherSignalHandler{} + handler.WhisperFilterAdded(signalFilters) + } +} + +func buildMessengerOptions( + config params.ShhextConfig, + db *sql.DB, + envelopesMonitorConfig *transport.EnvelopesMonitorConfig, + logger *zap.Logger, +) []protocol.Option { + options := []protocol.Option{ + protocol.WithCustomLogger(logger), + protocol.WithDatabase(db), + //protocol.WithEnvelopesMonitorConfig(envelopesMonitorConfig), + protocol.WithOnNegotiatedFilters(onNegotiatedFilters), + } + + if config.DataSyncEnabled { + options = append(options, protocol.WithDatasync()) + } + + // if config.VerifyTransactionURL != "" { + // client := &verifyTransactionClient{ + // url: config.VerifyTransactionURL, + // chainID: big.NewInt(config.VerifyTransactionChainID), + // } + // options = append(options, protocol.WithVerifyTransactionClient(client)) + // } + + return options +} diff --git a/services/status/service_nimbus.go b/services/status/service_nimbus.go new file mode 100644 index 0000000000..f2a2f27c3b --- /dev/null +++ b/services/status/service_nimbus.go @@ -0,0 +1,16 @@ +// +build nimbus + +package status + +import ( + nimbussvc "github.com/status-im/status-go/services/nimbus" +) + +// Make sure that Service implements nimbussvc.Service interface. +var _ nimbussvc.Service = (*Service)(nil) + +// StartService is run when a service is started. +// It does nothing in this case but is required by `nimbussvc.Service` interface. +func (s *Service) StartService() error { + return nil +} diff --git a/services/subscriptions/service.go b/services/subscriptions/service.go index db9e62469e..25a7927a2d 100644 --- a/services/subscriptions/service.go +++ b/services/subscriptions/service.go @@ -8,7 +8,7 @@ import ( "github.com/status-im/status-go/node" ) -// Make sure that Service implements node.Service interface. +// Make sure that Service implements gethnode.Service interface. var _ gethnode.Service = (*Service)(nil) // Service represents our own implementation of personal sign operations. diff --git a/services/subscriptions/service_nimbus.go b/services/subscriptions/service_nimbus.go new file mode 100644 index 0000000000..2abaab6dad --- /dev/null +++ b/services/subscriptions/service_nimbus.go @@ -0,0 +1,15 @@ +// +build nimbus + +package subscriptions + +import ( + nimbussvc "github.com/status-im/status-go/services/nimbus" +) + +// Make sure that Service implements nimbussvc.Service interface. +var _ nimbussvc.Service = (*Service)(nil) + +// StartService is run when a service is started. +func (s *Service) StartService() error { + return nil +} diff --git a/timesource/timesource.go b/timesource/timesource.go index 50195db9b4..639999272c 100644 --- a/timesource/timesource.go +++ b/timesource/timesource.go @@ -189,6 +189,10 @@ func (s *NTPTimeSource) runPeriodically(fn func() error) error { return nil } +func (s *NTPTimeSource) StartService() error { + return s.runPeriodically(s.updateOffset) +} + // Start runs a goroutine that updates local offset every updatePeriod. func (s *NTPTimeSource) Start(*p2p.Server) error { return s.runPeriodically(s.updateOffset) diff --git a/vendor/github.com/mattn/go-pointer/LICENSE b/vendor/github.com/mattn/go-pointer/LICENSE new file mode 100644 index 0000000000..5794eddcd2 --- /dev/null +++ b/vendor/github.com/mattn/go-pointer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Yasuhiro Matsumoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/mattn/go-pointer/README.md b/vendor/github.com/mattn/go-pointer/README.md new file mode 100644 index 0000000000..c74eee22ad --- /dev/null +++ b/vendor/github.com/mattn/go-pointer/README.md @@ -0,0 +1,29 @@ +# go-pointer + +Utility for cgo + +## Usage + +https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md + +In go 1.6, cgo argument can't be passed Go pointer. + +``` +var s string +C.pass_pointer(pointer.Save(&s)) +v := *(pointer.Restore(C.get_from_pointer()).(*string)) +``` + +## Installation + +``` +go get github.com/mattn/go-pointer +``` + +## License + +MIT + +## Author + +Yasuhiro Matsumoto (a.k.a mattn) diff --git a/vendor/github.com/mattn/go-pointer/_example/callback.h b/vendor/github.com/mattn/go-pointer/_example/callback.h new file mode 100644 index 0000000000..4dbed45b33 --- /dev/null +++ b/vendor/github.com/mattn/go-pointer/_example/callback.h @@ -0,0 +1,9 @@ +#include + +typedef void (*callback)(void*); + +static void call_later(int delay, callback cb, void* data) { + sleep(delay); + cb(data); +} + diff --git a/vendor/github.com/mattn/go-pointer/doc.go b/vendor/github.com/mattn/go-pointer/doc.go new file mode 100644 index 0000000000..c27bd8c059 --- /dev/null +++ b/vendor/github.com/mattn/go-pointer/doc.go @@ -0,0 +1 @@ +package pointer diff --git a/vendor/github.com/mattn/go-pointer/pointer.go b/vendor/github.com/mattn/go-pointer/pointer.go new file mode 100644 index 0000000000..6cdfae2aca --- /dev/null +++ b/vendor/github.com/mattn/go-pointer/pointer.go @@ -0,0 +1,57 @@ +package pointer + +// #include +import "C" +import ( + "sync" + "unsafe" +) + +var ( + mutex sync.Mutex + store = map[unsafe.Pointer]interface{}{} +) + +func Save(v interface{}) unsafe.Pointer { + if v == nil { + return nil + } + + // Generate real fake C pointer. + // This pointer will not store any data, but will bi used for indexing purposes. + // Since Go doest allow to cast dangling pointer to unsafe.Pointer, we do rally allocate one byte. + // Why we need indexing, because Go doest allow C code to store pointers to Go data. + var ptr unsafe.Pointer = C.malloc(C.size_t(1)) + if ptr == nil { + panic("can't allocate 'cgo-pointer hack index pointer': ptr == nil") + } + + mutex.Lock() + store[ptr] = v + mutex.Unlock() + + return ptr +} + +func Restore(ptr unsafe.Pointer) (v interface{}) { + if ptr == nil { + return nil + } + + mutex.Lock() + v = store[ptr] + mutex.Unlock() + return +} + +func Unref(ptr unsafe.Pointer) { + if ptr == nil { + return + } + + mutex.Lock() + delete(store, ptr) + mutex.Unlock() + + C.free(ptr) +} diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/geth/node.go b/vendor/github.com/status-im/status-go/eth-node/bridge/geth/node.go index c4666fe5f4..a629ee835a 100644 --- a/vendor/github.com/status-im/status-go/eth-node/bridge/geth/node.go +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/geth/node.go @@ -24,6 +24,10 @@ func NewNodeBridge(stack *node.Node) types.Node { return &gethNodeWrapper{stack: stack} } +func (w *gethNodeWrapper) Poll() { + // noop +} + func (w *gethNodeWrapper) NewENSVerifier(logger *zap.Logger) enstypes.ENSVerifier { return gethens.NewVerifier(logger) } diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/geth/whisper.go b/vendor/github.com/status-im/status-go/eth-node/bridge/geth/whisper.go index 2423a3264e..87f381c931 100644 --- a/vendor/github.com/status-im/status-go/eth-node/bridge/geth/whisper.go +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/geth/whisper.go @@ -80,6 +80,11 @@ func (w *gethWhisperWrapper) DeleteKeyPair(keyID string) bool { return w.whisper.DeleteKeyPair(keyID) } +// DeleteKeyPairs removes all cryptographic identities known to the node +func (w *gethWhisperWrapper) DeleteKeyPairs() error { + return w.whisper.DeleteKeyPairs() +} + func (w *gethWhisperWrapper) AddSymKeyDirect(key []byte) (string, error) { return w.whisper.AddSymKeyDirect(key) } diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/build-nimbus.sh b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/build-nimbus.sh new file mode 100644 index 0000000000..48751f54f7 --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/build-nimbus.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Pre-requisites: Git, Nix + +set -e + +GIT_ROOT=$(cd "${BASH_SOURCE%/*}" && git rev-parse --show-toplevel) + +# NOTE: To use a local Nimbus repository, uncomment and edit the following line +#nimbus_dir=~/src/github.com/status-im/nimbus + +target_dir="${GIT_ROOT}/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus" + +if [ -z "$nimbus_dir" ]; then + # The git ref of Nimbus to fetch and build. This should represent a commit SHA or a tag, for reproducible builds + nimbus_ref='feature/android-api' # TODO: Use a tag once + + nimbus_src='https://github.com/status-im/nimbus/' + nimbus_dir="${GIT_ROOT}/vendor/github.com/status-im/nimbus" + + trap "rm -rf $nimbus_dir" ERR INT QUIT + + # Clone nimbus repo into vendor directory, if necessary + if [ -d "$nimbus_dir" ]; then + cd $nimbus_dir && git reset --hard $nimbus_ref; cd - + else + # List fetched from vendorDeps array in https://github.com/status-im/nimbus/blob/master/nix/nimbus-wrappers.nix#L9-L12 + vendor_paths=( nim-chronicles nim-faststreams nim-json-serialization nim-chronos nim-eth nim-json nim-metrics nim-secp256k1 nim-serialization nim-stew nim-stint nimcrypto ) + vendor_path_opts="${vendor_paths[@]/#/--recurse-submodules=vendor/}" + git clone $nimbus_src --progress ${vendor_path_opts} --depth 1 -j8 -b $nimbus_ref $nimbus_dir + fi +fi + +# Build Nimbus wrappers and copy them into the Nimbus bridge in status-eth-node +build_dir=$(nix-build --pure --no-out-link -A wrappers-native $nimbus_dir/nix/default.nix) +rm -f ${target_dir}/libnimbus.* +mkdir -p ${target_dir} +cp -f ${build_dir}/include/* ${build_dir}/lib/libnimbus.so \ + ${target_dir}/ +chmod +w ${target_dir}/libnimbus.{so,h} diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/cfuncs.go b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/cfuncs.go new file mode 100644 index 0000000000..5a1f952adf --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/cfuncs.go @@ -0,0 +1,16 @@ +// +build nimbus + +package nimbusbridge + +/* + +#include + +// onMessageHandler gateway function +void onMessageHandler_cgo(received_message * msg, void* udata) +{ + void onMessageHandler(received_message* msg, void* udata); + onMessageHandler(msg, udata); +} +*/ +import "C" diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/filter.go b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/filter.go new file mode 100644 index 0000000000..69acb7635e --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/filter.go @@ -0,0 +1,61 @@ +// +build nimbus + +package nimbusbridge + +// https://golang.org/cmd/cgo/ + +/* +#include +#include +#include +#include +*/ +import "C" + +import ( + "unsafe" + + "github.com/status-im/status-go/eth-node/types" +) + +type nimbusFilterWrapper struct { + filter *C.filter_options + id string + own bool +} + +// NewNimbusFilterWrapper returns an object that wraps Nimbus's Filter in a types interface +func NewNimbusFilterWrapper(f *C.filter_options, id string, own bool) types.Filter { + wrapper := &nimbusFilterWrapper{ + filter: f, + id: id, + own: own, + } + return wrapper +} + +// GetNimbusFilterFrom retrieves the underlying whisper Filter struct from a wrapped Filter interface +func GetNimbusFilterFrom(f types.Filter) *C.filter_options { + return f.(*nimbusFilterWrapper).filter +} + +// ID returns the filter ID +func (w *nimbusFilterWrapper) ID() string { + return w.id +} + +// Free frees the C memory associated with the filter +func (w *nimbusFilterWrapper) Free() { + if !w.own { + panic("native filter is not owned by Go") + } + + if w.filter.privateKeyID != nil { + C.free(unsafe.Pointer(w.filter.privateKeyID)) + w.filter.privateKeyID = nil + } + if w.filter.symKeyID != nil { + C.free(unsafe.Pointer(w.filter.symKeyID)) + w.filter.symKeyID = nil + } +} diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/node.go b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/node.go new file mode 100644 index 0000000000..1feff085d7 --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/node.go @@ -0,0 +1,159 @@ +// +build nimbus + +package nimbusbridge + +// https://golang.org/cmd/cgo/ + +/* +#cgo LDFLAGS: -Wl,-rpath,'$ORIGIN' -L${SRCDIR} -lnimbus -lm +#include +#include +#include +#include +*/ +import "C" +import ( + "crypto/ecdsa" + "errors" + "fmt" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + "time" + "unsafe" + + "go.uber.org/zap" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + enstypes "github.com/status-im/status-go/eth-node/types/ens" +) + +type nimbusNodeWrapper struct { + mu sync.Mutex + + routineQueue *RoutineQueue + tid int + nodeStarted bool + cancelPollingChan chan struct{} + + w types.Whisper +} + +type Node interface { + types.Node + + StartNimbus(privateKey *ecdsa.PrivateKey, listenAddr string, staging bool) error + Stop() +} + +func NewNodeBridge() Node { + c := make(chan Node, 1) + go func(c chan<- Node, delay time.Duration) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + n := &nimbusNodeWrapper{ + routineQueue: NewRoutineQueue(), + tid: syscall.Gettid(), + cancelPollingChan: make(chan struct{}, 1), + } + c <- n + + for { + select { + case <-time.After(delay): + n.poll() + case <-n.cancelPollingChan: + return + } + } + }(c, 50*time.Millisecond) + + return <-c +} + +func (n *nimbusNodeWrapper) StartNimbus(privateKey *ecdsa.PrivateKey, listenAddr string, staging bool) error { + return n.routineQueue.Send(func(c chan<- callReturn) { + c <- callReturn{err: startNimbus(privateKey, listenAddr, staging)} + n.nodeStarted = true + }).err +} + +func (n *nimbusNodeWrapper) Stop() { + if n.cancelPollingChan != nil { + close(n.cancelPollingChan) + n.nodeStarted = false + n.cancelPollingChan = nil + } +} + +func (n *nimbusNodeWrapper) NewENSVerifier(_ *zap.Logger) enstypes.ENSVerifier { + panic("not implemented") +} + +func (n *nimbusNodeWrapper) GetWhisper(ctx interface{}) (types.Whisper, error) { + n.mu.Lock() + defer n.mu.Unlock() + + if n.w == nil { + n.w = NewNimbusWhisperWrapper(n.routineQueue) + } + return n.w, nil +} + +func (w *nimbusNodeWrapper) GetWaku(ctx interface{}) (types.Waku, error) { + panic("not implemented") +} + +func (n *nimbusNodeWrapper) AddPeer(url string) error { + urlC := C.CString(url) + defer C.free(unsafe.Pointer(urlC)) + if !C.nimbus_add_peer(urlC) { + return fmt.Errorf("failed to add peer: %s", url) + } + + return nil +} + +func (n *nimbusNodeWrapper) RemovePeer(url string) error { + panic("TODO: RemovePeer") +} + +func (n *nimbusNodeWrapper) poll() { + if syscall.Gettid() != n.tid { + panic("poll called from wrong thread") + } + + if n.nodeStarted { + C.nimbus_poll() + } + + n.routineQueue.HandleEvent() +} + +func startNimbus(privateKey *ecdsa.PrivateKey, listenAddr string, staging bool) error { + C.NimMain() + + if listenAddr == "" { + listenAddr = ":30304" + } + addrParts := strings.Split(listenAddr, ":") + port, err := strconv.Atoi(addrParts[len(addrParts)-1]) + if err != nil { + return fmt.Errorf("failed to parse port number from %s", listenAddr) + } + + var privateKeyC unsafe.Pointer + if privateKey != nil { + privateKeyC = C.CBytes(crypto.FromECDSA(privateKey)) + defer C.free(privateKeyC) + } + if !C.nimbus_start(C.ushort(port), true, false, 0.002, (*C.uchar)(privateKeyC), C.bool(staging)) { + return errors.New("failed to start Nimbus node") + } + + return nil +} diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/public_whisper_api.go b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/public_whisper_api.go new file mode 100644 index 0000000000..50b37480dd --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/public_whisper_api.go @@ -0,0 +1,212 @@ +// +build nimbus + +package nimbusbridge + +// https://golang.org/cmd/cgo/ + +/* +#include +#include +#include +#include +*/ +import "C" + +import ( + "container/list" + "context" + "errors" + "fmt" + "sync" + "unsafe" + + "github.com/status-im/status-go/eth-node/types" +) + +type nimbusPublicWhisperAPIWrapper struct { + filterMessagesMu *sync.Mutex + filterMessages *map[string]*list.List + routineQueue *RoutineQueue +} + +// NewNimbusPublicWhisperAPIWrapper returns an object that wraps Nimbus's PublicWhisperAPI in a types interface +func NewNimbusPublicWhisperAPIWrapper(filterMessagesMu *sync.Mutex, filterMessages *map[string]*list.List, routineQueue *RoutineQueue) types.PublicWhisperAPI { + return &nimbusPublicWhisperAPIWrapper{ + filterMessagesMu: filterMessagesMu, + filterMessages: filterMessages, + routineQueue: routineQueue, + } +} + +// AddPrivateKey imports the given private key. +func (w *nimbusPublicWhisperAPIWrapper) AddPrivateKey(ctx context.Context, privateKey types.HexBytes) (string, error) { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + privKeyC := C.CBytes(privateKey) + defer C.free(unsafe.Pointer(privKeyC)) + + idC := C.malloc(C.size_t(C.ID_LEN)) + defer C.free(idC) + if C.nimbus_add_keypair((*C.uchar)(privKeyC), (*C.uchar)(idC)) { + c <- callReturn{value: types.EncodeHex(C.GoBytes(idC, C.ID_LEN))} + } else { + c <- callReturn{err: errors.New("failed to add private key to Nimbus")} + } + }) + if retVal.err != nil { + return "", retVal.err + } + + return retVal.value.(string), nil +} + +// GenerateSymKeyFromPassword derives a key from the given password, stores it, and returns its ID. +func (w *nimbusPublicWhisperAPIWrapper) GenerateSymKeyFromPassword(ctx context.Context, passwd string) (string, error) { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + passwordC := C.CString(passwd) + defer C.free(unsafe.Pointer(passwordC)) + + idC := C.malloc(C.size_t(C.ID_LEN)) + defer C.free(idC) + if C.nimbus_add_symkey_from_password(passwordC, (*C.uchar)(idC)) { + c <- callReturn{value: types.EncodeHex(C.GoBytes(idC, C.ID_LEN))} + } else { + c <- callReturn{err: errors.New("failed to add symkey to Nimbus")} + } + }) + if retVal.err != nil { + return "", retVal.err + } + + return retVal.value.(string), nil +} + +// DeleteKeyPair removes the key with the given key if it exists. +func (w *nimbusPublicWhisperAPIWrapper) DeleteKeyPair(ctx context.Context, key string) (bool, error) { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + keyC, err := decodeHexID(key) + if err != nil { + c <- callReturn{err: err} + return + } + defer C.free(unsafe.Pointer(keyC)) + + c <- callReturn{value: C.nimbus_delete_keypair(keyC)} + }) + if retVal.err != nil { + return false, retVal.err + } + + return retVal.value.(bool), nil +} + +// NewMessageFilter creates a new filter that can be used to poll for +// (new) messages that satisfy the given criteria. +func (w *nimbusPublicWhisperAPIWrapper) NewMessageFilter(req types.Criteria) (string, error) { + // topics := make([]whisper.TopicType, len(req.Topics)) + // for index, tt := range req.Topics { + // topics[index] = whisper.TopicType(tt) + // } + + // criteria := whisper.Criteria{ + // SymKeyID: req.SymKeyID, + // PrivateKeyID: req.PrivateKeyID, + // Sig: req.Sig, + // MinPow: req.MinPow, + // Topics: topics, + // AllowP2P: req.AllowP2P, + // } + // return w.publicWhisperAPI.NewMessageFilter(criteria) + // TODO + return "", errors.New("not implemented") +} + +// GetFilterMessages returns the messages that match the filter criteria and +// are received between the last poll and now. +func (w *nimbusPublicWhisperAPIWrapper) GetFilterMessages(id string) ([]*types.Message, error) { + idC := C.CString(id) + defer C.free(unsafe.Pointer(idC)) + + var ( + messageList *list.List + ok bool + ) + w.filterMessagesMu.Lock() + defer w.filterMessagesMu.Unlock() + if messageList, ok = (*w.filterMessages)[id]; !ok { + return nil, fmt.Errorf("no filter with ID %s", id) + } + + retVal := make([]*types.Message, messageList.Len()) + if messageList.Len() == 0 { + return retVal, nil + } + + elem := messageList.Front() + index := 0 + for elem != nil { + retVal[index] = (elem.Value).(*types.Message) + index++ + next := elem.Next() + messageList.Remove(elem) + elem = next + } + return retVal, nil +} + +// Post posts a message on the Whisper network. +// returns the hash of the message in case of success. +func (w *nimbusPublicWhisperAPIWrapper) Post(ctx context.Context, req types.NewMessage) ([]byte, error) { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + msg := C.post_message{ + ttl: C.uint32_t(req.TTL), + powTime: C.double(req.PowTime), + powTarget: C.double(req.PowTarget), + } + if req.SigID != "" { + sourceID, err := decodeHexID(req.SigID) + if err != nil { + c <- callReturn{err: err} + return + } + msg.sourceID = sourceID + defer C.free(unsafe.Pointer(sourceID)) + } + if req.SymKeyID != "" { + symKeyID, err := decodeHexID(req.SymKeyID) + if err != nil { + c <- callReturn{err: err} + return + } + msg.symKeyID = symKeyID + defer C.free(unsafe.Pointer(symKeyID)) + } + if req.PublicKey != nil && len(req.PublicKey) > 0 { + msg.pubKey = (*C.uchar)(C.CBytes(req.PublicKey[1:])) + defer C.free(unsafe.Pointer(msg.pubKey)) + } + msg.payloadLen = C.size_t(len(req.Payload)) + msg.payload = (*C.uchar)(C.CBytes(req.Payload)) + defer C.free(unsafe.Pointer(msg.payload)) + msg.paddingLen = C.size_t(len(req.Padding)) + msg.padding = (*C.uchar)(C.CBytes(req.Padding)) + defer C.free(unsafe.Pointer(msg.padding)) + copyTopicToCBuffer(&msg.topic[0], req.Topic[:]) + + // TODO: return envelope hash once nimbus_post is improved to return it + if C.nimbus_post(&msg) { + c <- callReturn{value: make([]byte, 0)} + return + } + c <- callReturn{err: fmt.Errorf("failed to post message symkeyid=%s pubkey=%#x topic=%#x", req.SymKeyID, req.PublicKey, req.Topic[:])} + // hashC := C.nimbus_post(&msg) + // if hashC == nil { + // return nil, errors.New("Nimbus failed to post message") + // } + // return hex.DecodeString(C.GoString(hashC)) + }) + if retVal.err != nil { + return nil, retVal.err + } + + return retVal.value.([]byte), nil +} diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/routine_queue.go b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/routine_queue.go new file mode 100644 index 0000000000..ccf0b71f45 --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/routine_queue.go @@ -0,0 +1,64 @@ +// +build nimbus + +package nimbusbridge + +import ( + "syscall" +) + +// RoutineQueue provides a mechanism for marshalling function calls +// so that they are run in a specific thread (the thread where +// RoutineQueue is initialized). +type RoutineQueue struct { + tid int + events chan event +} + +type callReturn struct { + value interface{} + err error +} + +// NewRoutineQueue returns a new RoutineQueue object. +func NewRoutineQueue() *RoutineQueue { + q := &RoutineQueue{ + tid: syscall.Gettid(), + events: make(chan event, 20), + } + + return q +} + +// event represents an event triggered by the user. +type event struct { + f func(chan<- callReturn) + done chan callReturn +} + +func (q *RoutineQueue) HandleEvent() { + if syscall.Gettid() != q.tid { + panic("HandleEvent called from wrong thread") + } + + select { + case ev := <-q.events: + ev.f(ev.done) + default: + return + } +} + +// Send executes the passed function. This method can be called safely from a +// goroutine in order to execute a Nimbus function. It is important to note that the +// passed function won't be executed immediately, instead it will be added to +// the user events queue. +func (q *RoutineQueue) Send(f func(chan<- callReturn)) callReturn { + ev := event{f: f, done: make(chan callReturn, 1)} + defer close(ev.done) + if syscall.Gettid() == q.tid { + f(ev.done) + return <-ev.done + } + q.events <- ev + return <-ev.done +} diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/whisper.go b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/whisper.go new file mode 100644 index 0000000000..de241bf33f --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/whisper.go @@ -0,0 +1,431 @@ +// +build nimbus + +package nimbusbridge + +// https://golang.org/cmd/cgo/ + +/* +#include +#include +#include +#include +void onMessageHandler_cgo(received_message* msg, void* udata); // Forward declaration. +*/ +import "C" + +import ( + "container/list" + "crypto/ecdsa" + "errors" + "fmt" + "sync" + "time" + "unsafe" + + gopointer "github.com/mattn/go-pointer" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" +) + +type nimbusWhisperWrapper struct { + timesource func() time.Time + filters map[string]types.Filter + filterMessagesMu sync.Mutex + filterMessages map[string]*list.List + routineQueue *RoutineQueue +} + +// NewNimbusWhisperWrapper returns an object that wraps Nimbus' Whisper in a types interface +func NewNimbusWhisperWrapper(routineQueue *RoutineQueue) types.Whisper { + return &nimbusWhisperWrapper{ + timesource: func() time.Time { return time.Now() }, + filters: map[string]types.Filter{}, + filterMessages: map[string]*list.List{}, + routineQueue: routineQueue, + } +} + +func (w *nimbusWhisperWrapper) PublicWhisperAPI() types.PublicWhisperAPI { + return NewNimbusPublicWhisperAPIWrapper(&w.filterMessagesMu, &w.filterMessages, w.routineQueue) +} + +// MinPow returns the PoW value required by this node. +func (w *nimbusWhisperWrapper) MinPow() float64 { + return w.routineQueue.Send(func(c chan<- callReturn) { + c <- callReturn{value: float64(C.nimbus_get_min_pow())} + }).value.(float64) +} + +// BloomFilter returns the aggregated bloom filter for all the topics of interest. +// The nodes are required to send only messages that match the advertised bloom filter. +// If a message does not match the bloom, it will tantamount to spam, and the peer will +// be disconnected. +func (w *nimbusWhisperWrapper) BloomFilter() []byte { + return w.routineQueue.Send(func(c chan<- callReturn) { + // Allocate a buffer for Nimbus to return the bloom filter on + dataC := C.malloc(C.size_t(C.BLOOM_LEN)) + defer C.free(unsafe.Pointer(dataC)) + + C.nimbus_get_bloom_filter((*C.uchar)(dataC)) + + // Move the returned data into a Go array + data := make([]byte, C.BLOOM_LEN) + copy(data, C.GoBytes(dataC, C.BLOOM_LEN)) + c <- callReturn{value: data} + }).value.([]byte) +} + +// GetCurrentTime returns current time. +func (w *nimbusWhisperWrapper) GetCurrentTime() time.Time { + return w.timesource() +} + +// SetTimeSource assigns a particular source of time to a whisper object. +func (w *nimbusWhisperWrapper) SetTimeSource(timesource func() time.Time) { + w.timesource = timesource +} + +func (w *nimbusWhisperWrapper) SubscribeEnvelopeEvents(eventsProxy chan<- types.EnvelopeEvent) types.Subscription { + // TODO: when mailserver support is implemented + panic("not implemented") +} + +func (w *nimbusWhisperWrapper) GetPrivateKey(id string) (*ecdsa.PrivateKey, error) { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + idC, err := decodeHexID(id) + if err != nil { + c <- callReturn{err: err} + return + } + defer C.free(unsafe.Pointer(idC)) + privKeyC := C.malloc(types.AesKeyLength) + defer C.free(unsafe.Pointer(privKeyC)) + + if !C.nimbus_get_private_key(idC, (*C.uchar)(privKeyC)) { + c <- callReturn{err: errors.New("failed to get private key from Nimbus")} + return + } + + pk, err := crypto.ToECDSA(C.GoBytes(privKeyC, C.PRIVKEY_LEN)) + if err != nil { + c <- callReturn{err: err} + return + } + + c <- callReturn{value: pk} + }) + if retVal.err != nil { + return nil, retVal.err + } + + return retVal.value.(*ecdsa.PrivateKey), nil +} + +// AddKeyPair imports a asymmetric private key and returns a deterministic identifier. +func (w *nimbusWhisperWrapper) AddKeyPair(key *ecdsa.PrivateKey) (string, error) { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + privKey := crypto.FromECDSA(key) + privKeyC := C.CBytes(privKey) + defer C.free(unsafe.Pointer(privKeyC)) + + idC := C.malloc(C.size_t(C.ID_LEN)) + defer C.free(idC) + if !C.nimbus_add_keypair((*C.uchar)(privKeyC), (*C.uchar)(idC)) { + c <- callReturn{err: errors.New("failed to add keypair to Nimbus")} + return + } + + c <- callReturn{value: types.EncodeHex(C.GoBytes(idC, C.ID_LEN))} + }) + if retVal.err != nil { + return "", retVal.err + } + + return retVal.value.(string), nil +} + +// DeleteKeyPair deletes the key with the specified ID if it exists. +func (w *nimbusWhisperWrapper) DeleteKeyPair(keyID string) bool { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + keyC, err := decodeHexID(keyID) + if err != nil { + c <- callReturn{err: err} + return + } + defer C.free(unsafe.Pointer(keyC)) + + c <- callReturn{value: C.nimbus_delete_keypair(keyC)} + }) + if retVal.err != nil { + return false + } + + return retVal.value.(bool) +} + +// DeleteKeyPairs removes all cryptographic identities known to the node +func (w *nimbusWhisperWrapper) DeleteKeyPairs() error { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + C.nimbus_delete_keypairs() + c <- callReturn{} + }) + + return retVal.err +} + +func (w *nimbusWhisperWrapper) AddSymKeyDirect(key []byte) (string, error) { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + keyC := C.CBytes(key) + defer C.free(unsafe.Pointer(keyC)) + + idC := C.malloc(C.size_t(C.ID_LEN)) + defer C.free(idC) + if !C.nimbus_add_symkey((*C.uchar)(keyC), (*C.uchar)(idC)) { + c <- callReturn{err: errors.New("failed to add symkey to Nimbus")} + return + } + + c <- callReturn{value: types.EncodeHex(C.GoBytes(idC, C.ID_LEN))} + }) + if retVal.err != nil { + return "", retVal.err + } + + return retVal.value.(string), nil +} + +func (w *nimbusWhisperWrapper) AddSymKeyFromPassword(password string) (string, error) { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + passwordC := C.CString(password) + defer C.free(unsafe.Pointer(passwordC)) + + idC := C.malloc(C.size_t(C.ID_LEN)) + defer C.free(idC) + if C.nimbus_add_symkey_from_password(passwordC, (*C.uchar)(idC)) { + id := C.GoBytes(idC, C.ID_LEN) + c <- callReturn{value: types.EncodeHex(id)} + } else { + c <- callReturn{err: errors.New("failed to add symkey to Nimbus")} + } + }) + if retVal.err != nil { + return "", retVal.err + } + + return retVal.value.(string), nil +} + +func (w *nimbusWhisperWrapper) DeleteSymKey(id string) bool { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + idC, err := decodeHexID(id) + if err != nil { + c <- callReturn{err: err} + return + } + defer C.free(unsafe.Pointer(idC)) + + c <- callReturn{value: C.nimbus_delete_symkey(idC)} + }) + if retVal.err != nil { + return false + } + + return retVal.value.(bool) +} + +func (w *nimbusWhisperWrapper) GetSymKey(id string) ([]byte, error) { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + idC, err := decodeHexID(id) + if err != nil { + c <- callReturn{err: err} + return + } + defer C.free(unsafe.Pointer(idC)) + + // Allocate a buffer for Nimbus to return the symkey on + dataC := C.malloc(C.size_t(C.SYMKEY_LEN)) + defer C.free(unsafe.Pointer(dataC)) + if !C.nimbus_get_symkey(idC, (*C.uchar)(dataC)) { + c <- callReturn{err: errors.New("symkey not found")} + return + } + + c <- callReturn{value: C.GoBytes(dataC, C.SYMKEY_LEN)} + }) + if retVal.err != nil { + return nil, retVal.err + } + + return retVal.value.([]byte), nil +} + +//export onMessageHandler +func onMessageHandler(msg *C.received_message, udata unsafe.Pointer) { + messageList := (gopointer.Restore(udata)).(*list.List) + + topic := types.TopicType{} + copy(topic[:], C.GoBytes(unsafe.Pointer(&msg.topic[0]), types.TopicLength)[:types.TopicLength]) + wrappedMsg := &types.Message{ + TTL: uint32(msg.ttl), + Timestamp: uint32(msg.timestamp), + Topic: topic, + Payload: C.GoBytes(unsafe.Pointer(msg.decoded), C.int(msg.decodedLen)), + PoW: float64(msg.pow), + Hash: C.GoBytes(unsafe.Pointer(&msg.hash[0]), types.HashLength), + P2P: true, + } + if msg.source != nil { + wrappedMsg.Sig = append([]byte{0x04}, C.GoBytes(unsafe.Pointer(msg.source), types.PubKeyLength)...) + } + if msg.recipientPublicKey != nil { + wrappedMsg.Dst = append([]byte{0x04}, C.GoBytes(unsafe.Pointer(msg.recipientPublicKey), types.PubKeyLength)...) + } + + messageList.PushBack(wrappedMsg) +} + +func (w *nimbusWhisperWrapper) Subscribe(opts *types.SubscriptionOptions) (string, error) { + f, err := w.createFilterWrapper("", opts) + if err != nil { + return "", err + } + + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + // Create a message store for this filter, so we can add new messages to it from the nimbus_subscribe_filter callback + messageList := list.New() + idC := C.malloc(C.size_t(C.ID_LEN)) + defer C.free(idC) + if !C.nimbus_subscribe_filter( + GetNimbusFilterFrom(f), + (C.received_msg_handler)(unsafe.Pointer(C.onMessageHandler_cgo)), gopointer.Save(messageList), + (*C.uchar)(idC)) { + c <- callReturn{err: errors.New("failed to subscribe to filter in Nimbus")} + return + } + filterID := C.GoString((*C.char)(idC)) + + w.filterMessagesMu.Lock() + w.filterMessages[filterID] = messageList // TODO: Check if this is done too late (race condition with onMessageHandler) + w.filterMessagesMu.Unlock() + + f.(*nimbusFilterWrapper).id = filterID + + c <- callReturn{value: filterID} + }) + if retVal.err != nil { + return "", retVal.err + } + + return retVal.value.(string), nil +} + +func (w *nimbusWhisperWrapper) GetFilter(id string) types.Filter { + idC := C.CString(id) + defer C.free(unsafe.Pointer(idC)) + + panic("GetFilter not implemented") + // pFilter := C.nimbus_get_filter(idC) + // return NewNimbusFilterWrapper(pFilter, id, false) +} + +func (w *nimbusWhisperWrapper) Unsubscribe(id string) error { + retVal := w.routineQueue.Send(func(c chan<- callReturn) { + idC, err := decodeHexID(id) + if err != nil { + c <- callReturn{err: err} + return + } + defer C.free(unsafe.Pointer(idC)) + + if ok := C.nimbus_unsubscribe_filter(idC); !ok { + c <- callReturn{err: errors.New("filter not found")} + return + } + + w.filterMessagesMu.Lock() + if messageList, ok := w.filterMessages[id]; ok { + gopointer.Unref(gopointer.Save(messageList)) + delete(w.filterMessages, id) + } + w.filterMessagesMu.Unlock() + + if f, ok := w.filters[id]; ok { + f.(*nimbusFilterWrapper).Free() + delete(w.filters, id) + } + + c <- callReturn{err: nil} + }) + return retVal.err +} + +func decodeHexID(id string) (*C.uint8_t, error) { + idBytes, err := types.DecodeHex(id) + if err == nil && len(idBytes) != C.ID_LEN { + err = fmt.Errorf("ID length must be %v bytes, actual length is %v", C.ID_LEN, len(idBytes)) + } + if err != nil { + return nil, err + } + + return (*C.uint8_t)(C.CBytes(idBytes)), nil +} + +// copyTopicToCBuffer copies a Go topic buffer to a C topic buffer without allocating new memory +func copyTopicToCBuffer(dst *C.uchar, topic []byte) { + if len(topic) != types.TopicLength { + panic("invalid Whisper topic buffer size") + } + + p := (*[types.TopicLength]C.uchar)(unsafe.Pointer(dst)) + for index, b := range topic { + p[index] = C.uchar(b) + } +} + +func (w *nimbusWhisperWrapper) createFilterWrapper(id string, opts *types.SubscriptionOptions) (types.Filter, error) { + if len(opts.Topics) != 1 { + return nil, errors.New("currently only 1 topic is supported by the Nimbus bridge") + } + + filter := C.filter_options{ + minPow: C.double(opts.PoW), + allowP2P: C.int(1), + } + copyTopicToCBuffer(&filter.topic[0], opts.Topics[0]) + if opts.PrivateKeyID != "" { + if idC, err := decodeHexID(opts.PrivateKeyID); err == nil { + filter.privateKeyID = idC + } else { + return nil, err + } + } + if opts.SymKeyID != "" { + if idC, err := decodeHexID(opts.SymKeyID); err == nil { + filter.symKeyID = idC + } else { + return nil, err + } + } + + return NewNimbusFilterWrapper(&filter, id, true), nil +} + +func (w *nimbusWhisperWrapper) SendMessagesRequest(peerID []byte, r types.MessagesRequest) error { + return errors.New("not implemented") +} + +// RequestHistoricMessages sends a message with p2pRequestCode to a specific peer, +// which is known to implement MailServer interface, and is supposed to process this +// request and respond with a number of peer-to-peer messages (possibly expired), +// which are not supposed to be forwarded any further. +// The whisper protocol is agnostic of the format and contents of envelope. +func (w *nimbusWhisperWrapper) RequestHistoricMessagesWithTimeout(peerID []byte, envelope types.Envelope, timeout time.Duration) error { + return errors.New("not implemented") +} + +// SyncMessages can be sent between two Mail Servers and syncs envelopes between them. +func (w *nimbusWhisperWrapper) SyncMessages(peerID []byte, req types.SyncMailRequest) error { + return errors.New("not implemented") +} diff --git a/vendor/github.com/status-im/status-go/eth-node/types/whisper.go b/vendor/github.com/status-im/status-go/eth-node/types/whisper.go index 23529c4daf..f3266bbab8 100644 --- a/vendor/github.com/status-im/status-go/eth-node/types/whisper.go +++ b/vendor/github.com/status-im/status-go/eth-node/types/whisper.go @@ -31,6 +31,8 @@ type Whisper interface { AddKeyPair(key *ecdsa.PrivateKey) (string, error) // DeleteKeyPair deletes the key with the specified ID if it exists. DeleteKeyPair(keyID string) bool + // DeleteKeyPairs removes all cryptographic identities known to the node + DeleteKeyPairs() error AddSymKeyDirect(key []byte) (string, error) AddSymKeyFromPassword(password string) (string, error) DeleteSymKey(id string) bool diff --git a/vendor/golang.org/x/tools/go/analysis/doc.go b/vendor/golang.org/x/tools/go/analysis/doc.go index a2353fc88b..8fa4a8531b 100644 --- a/vendor/golang.org/x/tools/go/analysis/doc.go +++ b/vendor/golang.org/x/tools/go/analysis/doc.go @@ -3,6 +3,7 @@ The analysis package defines the interface between a modular static analysis and an analysis driver program. + Background A static analysis is a function that inspects a package of Go code and @@ -41,9 +42,9 @@ the go/analysis/passes/ subdirectory: package unusedresult var Analyzer = &analysis.Analyzer{ - Name: "unusedresult", - Doc: "check for unused results of calls to some functions", - Run: run, + Name: "unusedresult", + Doc: "check for unused results of calls to some functions", + Run: run, ... } @@ -51,7 +52,6 @@ the go/analysis/passes/ subdirectory: ... } - An analysis driver is a program such as vet that runs a set of analyses and prints the diagnostics that they report. The driver program must import the list of Analyzers it needs. @@ -70,51 +70,18 @@ A driver may use the name, flags, and documentation to provide on-line help that describes the analyses it performs. The doc comment contains a brief one-line summary, optionally followed by paragraphs of explanation. -The vet command, shown below, is an example of a driver that runs -multiple analyzers. It is based on the multichecker package -(see the "Standalone commands" section for details). - - $ go build golang.org/x/tools/go/analysis/cmd/vet - $ ./vet help - vet is a tool for static analysis of Go programs. - - Usage: vet [-flag] [package] - - Registered analyzers: - - asmdecl report mismatches between assembly files and Go declarations - assign check for useless assignments - atomic check for common mistakes using the sync/atomic package - ... - unusedresult check for unused results of calls to some functions - - $ ./vet help unusedresult - unusedresult: check for unused results of calls to some functions - - Analyzer flags: - - -unusedresult.funcs value - comma-separated list of functions whose results must be used (default Error,String) - -unusedresult.stringmethods value - comma-separated list of names of methods of type func() string whose results must be used - - Some functions like fmt.Errorf return a result and have no side effects, - so it is always a mistake to discard the result. This analyzer reports - calls to certain functions in which the result of the call is ignored. - - The set of functions may be controlled using flags. The Analyzer type has more fields besides those shown above: type Analyzer struct { - Name string - Doc string - Flags flag.FlagSet - Run func(*Pass) (interface{}, error) - RunDespiteErrors bool - ResultType reflect.Type - Requires []*Analyzer - FactTypes []Fact + Name string + Doc string + Flags flag.FlagSet + Run func(*Pass) (interface{}, error) + RunDespiteErrors bool + ResultType reflect.Type + Requires []*Analyzer + FactTypes []Fact } The Flags field declares a set of named (global) flag variables that @@ -154,13 +121,13 @@ package being analyzed, and provides operations to the Run function for reporting diagnostics and other information back to the driver. type Pass struct { - Fset *token.FileSet - Files []*ast.File - OtherFiles []string - Pkg *types.Package - TypesInfo *types.Info - ResultOf map[*Analyzer]interface{} - Report func(Diagnostic) + Fset *token.FileSet + Files []*ast.File + OtherFiles []string + Pkg *types.Package + TypesInfo *types.Info + ResultOf map[*Analyzer]interface{} + Report func(Diagnostic) ... } @@ -245,7 +212,7 @@ package. An Analyzer that uses facts must declare their types: var Analyzer = &analysis.Analyzer{ - Name: "printf", + Name: "printf", FactTypes: []analysis.Fact{new(isWrapper)}, ... } @@ -330,7 +297,5 @@ entirety as: A tool that provides multiple analyzers can use multichecker in a similar way, giving it the list of Analyzers. - - */ package analysis diff --git a/vendor/golang.org/x/tools/go/packages/doc.go b/vendor/golang.org/x/tools/go/packages/doc.go index 3799f8ed8b..4bfe28a51f 100644 --- a/vendor/golang.org/x/tools/go/packages/doc.go +++ b/vendor/golang.org/x/tools/go/packages/doc.go @@ -60,8 +60,7 @@ causes Load to run in LoadFiles mode, collecting minimal information. See the documentation for type Config for details. As noted earlier, the Config.Mode controls the amount of detail -reported about the loaded packages, with each mode returning all the data of the -previous mode with some extra added. See the documentation for type LoadMode +reported about the loaded packages. See the documentation for type LoadMode for details. Most tools should pass their command-line arguments (after any flags) diff --git a/vendor/golang.org/x/tools/go/packages/external.go b/vendor/golang.org/x/tools/go/packages/external.go index 6ac3e4f5b5..8c8473fd0b 100644 --- a/vendor/golang.org/x/tools/go/packages/external.go +++ b/vendor/golang.org/x/tools/go/packages/external.go @@ -84,13 +84,14 @@ func findExternalDriver(cfg *Config) driver { cmd.Stdin = bytes.NewReader(req) cmd.Stdout = buf cmd.Stderr = stderr - if len(stderr.Bytes()) != 0 && os.Getenv("GOPACKAGESPRINTDRIVERERRORS") != "" { - fmt.Fprintf(os.Stderr, "%s stderr: <<%s>>\n", cmdDebugStr(cmd, words...), stderr) - } if err := cmd.Run(); err != nil { return nil, fmt.Errorf("%v: %v: %s", tool, err, cmd.Stderr) } + if len(stderr.Bytes()) != 0 && os.Getenv("GOPACKAGESPRINTDRIVERERRORS") != "" { + fmt.Fprintf(os.Stderr, "%s stderr: <<%s>>\n", cmdDebugStr(cmd, words...), stderr) + } + var response driverResponse if err := json.Unmarshal(buf.Bytes(), &response); err != nil { return nil, err diff --git a/vendor/golang.org/x/tools/go/packages/golist.go b/vendor/golang.org/x/tools/go/packages/golist.go index c581bce976..a9a1ba89e8 100644 --- a/vendor/golang.org/x/tools/go/packages/golist.go +++ b/vendor/golang.org/x/tools/go/packages/golist.go @@ -26,7 +26,6 @@ import ( "golang.org/x/tools/go/internal/packagesdriver" "golang.org/x/tools/internal/gopathwalk" "golang.org/x/tools/internal/semver" - "golang.org/x/tools/internal/span" ) // debug controls verbose logging. @@ -254,12 +253,7 @@ func addNeededOverlayPackages(cfg *Config, driver driver, response *responseDedu if len(pkgs) == 0 { return nil } - drivercfg := *cfg - if getGoInfo().env.modulesOn { - drivercfg.BuildFlags = append(drivercfg.BuildFlags, "-mod=readonly") - } - dr, err := driver(&drivercfg, pkgs...) - + dr, err := driver(cfg, pkgs...) if err != nil { return err } @@ -270,10 +264,7 @@ func addNeededOverlayPackages(cfg *Config, driver driver, response *responseDedu if err != nil { return err } - if err := addNeededOverlayPackages(cfg, driver, response, needPkgs, getGoInfo); err != nil { - return err - } - return nil + return addNeededOverlayPackages(cfg, driver, response, needPkgs, getGoInfo) } func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, queries []string, goInfo func() *goInfo) error { @@ -287,42 +278,43 @@ func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, q return fmt.Errorf("could not determine absolute path of file= query path %q: %v", query, err) } dirResponse, err := driver(cfg, pattern) - if err != nil { + if err != nil || (len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].Errors) == 1) { + // There was an error loading the package. Try to load the file as an ad-hoc package. + // Usually the error will appear in a returned package, but may not if we're in modules mode + // and the ad-hoc is located outside a module. var queryErr error - if dirResponse, queryErr = adHocPackage(cfg, driver, pattern, query); queryErr != nil { - return err // return the original error + dirResponse, queryErr = driver(cfg, query) + if queryErr != nil { + // Return the original error if the attempt to fall back failed. + return err } - } - // `go list` can report errors for files that are not listed as part of a package's GoFiles. - // In the case of an invalid Go file, we should assume that it is part of package if only - // one package is in the response. The file may have valid contents in an overlay. - if len(dirResponse.Packages) == 1 { - pkg := dirResponse.Packages[0] - for i, err := range pkg.Errors { - s := errorSpan(err) - if !s.IsValid() { - break - } - if len(pkg.CompiledGoFiles) == 0 { - break - } - dir := filepath.Dir(pkg.CompiledGoFiles[0]) - filename := filepath.Join(dir, filepath.Base(s.URI().Filename())) - if info, err := os.Stat(filename); err != nil || info.IsDir() { - break - } - if !contains(pkg.CompiledGoFiles, filename) { - pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, filename) - pkg.GoFiles = append(pkg.GoFiles, filename) - pkg.Errors = append(pkg.Errors[:i], pkg.Errors[i+1:]...) - } + // If we get nothing back from `go list`, try to make this file into its own ad-hoc package. + if len(dirResponse.Packages) == 0 && queryErr == nil { + dirResponse.Packages = append(dirResponse.Packages, &Package{ + ID: "command-line-arguments", + PkgPath: query, + GoFiles: []string{query}, + CompiledGoFiles: []string{query}, + Imports: make(map[string]*Package), + }) + dirResponse.Roots = append(dirResponse.Roots, "command-line-arguments") } - } - // A final attempt to construct an ad-hoc package. - if len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].Errors) == 1 { - var queryErr error - if dirResponse, queryErr = adHocPackage(cfg, driver, pattern, query); queryErr != nil { - return err // return the original error + // Special case to handle issue #33482: + // If this is a file= query for ad-hoc packages where the file only exists on an overlay, + // and exists outside of a module, add the file in for the package. + if len(dirResponse.Packages) == 1 && (dirResponse.Packages[0].ID == "command-line-arguments" || + filepath.ToSlash(dirResponse.Packages[0].PkgPath) == filepath.ToSlash(query)) { + if len(dirResponse.Packages[0].GoFiles) == 0 { + filename := filepath.Join(pattern, filepath.Base(query)) // avoid recomputing abspath + // TODO(matloob): check if the file is outside of a root dir? + for path := range cfg.Overlay { + if path == filename { + dirResponse.Packages[0].Errors = nil + dirResponse.Packages[0].GoFiles = []string{path} + dirResponse.Packages[0].CompiledGoFiles = []string{path} + } + } + } } } isRoot := make(map[string]bool, len(dirResponse.Roots)) @@ -350,74 +342,6 @@ func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, q return nil } -// adHocPackage attempts to construct an ad-hoc package given a query that failed. -func adHocPackage(cfg *Config, driver driver, pattern, query string) (*driverResponse, error) { - // There was an error loading the package. Try to load the file as an ad-hoc package. - // Usually the error will appear in a returned package, but may not if we're in modules mode - // and the ad-hoc is located outside a module. - dirResponse, err := driver(cfg, query) - if err != nil { - return nil, err - } - // If we get nothing back from `go list`, try to make this file into its own ad-hoc package. - if len(dirResponse.Packages) == 0 && err == nil { - dirResponse.Packages = append(dirResponse.Packages, &Package{ - ID: "command-line-arguments", - PkgPath: query, - GoFiles: []string{query}, - CompiledGoFiles: []string{query}, - Imports: make(map[string]*Package), - }) - dirResponse.Roots = append(dirResponse.Roots, "command-line-arguments") - } - // Special case to handle issue #33482: - // If this is a file= query for ad-hoc packages where the file only exists on an overlay, - // and exists outside of a module, add the file in for the package. - if len(dirResponse.Packages) == 1 && (dirResponse.Packages[0].ID == "command-line-arguments" || dirResponse.Packages[0].PkgPath == filepath.ToSlash(query)) { - if len(dirResponse.Packages[0].GoFiles) == 0 { - filename := filepath.Join(pattern, filepath.Base(query)) // avoid recomputing abspath - // TODO(matloob): check if the file is outside of a root dir? - for path := range cfg.Overlay { - if path == filename { - dirResponse.Packages[0].Errors = nil - dirResponse.Packages[0].GoFiles = []string{path} - dirResponse.Packages[0].CompiledGoFiles = []string{path} - } - } - } - } - return dirResponse, nil -} - -func contains(files []string, filename string) bool { - for _, f := range files { - if f == filename { - return true - } - } - return false -} - -// errorSpan attempts to parse a standard `go list` error message -// by stripping off the trailing error message. -// -// It works only on errors whose message is prefixed by colon, -// followed by a space (": "). For example: -// -// attributes.go:13:1: expected 'package', found 'type' -// -func errorSpan(err Error) span.Span { - if err.Pos == "" { - input := strings.TrimSpace(err.Msg) - msgIndex := strings.Index(input, ": ") - if msgIndex < 0 { - return span.Parse(input) - } - return span.Parse(input[:msgIndex]) - } - return span.Parse(err.Pos) -} - // modCacheRegexp splits a path in a module cache into module, module version, and package. var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`) @@ -749,7 +673,7 @@ func golistDriver(cfg *Config, rootsDirs func() *goInfo, words ...string) (*driv // Run "go list" for complete // information on the specified packages. - buf, err := invokeGo(cfg, golistargs(cfg, words)...) + buf, err := invokeGo(cfg, "list", golistargs(cfg, words)...) if err != nil { return nil, err } @@ -881,9 +805,15 @@ func golistDriver(cfg *Config, rootsDirs func() *goInfo, words ...string) (*driv } if p.Error != nil { + msg := strings.TrimSpace(p.Error.Err) // Trim to work around golang.org/issue/32363. + // Address golang.org/issue/35964 by appending import stack to error message. + if msg == "import cycle not allowed" && len(p.Error.ImportStack) != 0 { + msg += fmt.Sprintf(": import stack: %v", p.Error.ImportStack) + } pkg.Errors = append(pkg.Errors, Error{ - Pos: p.Error.Pos, - Msg: strings.TrimSpace(p.Error.Err), // Trim to work around golang.org/issue/32363. + Pos: p.Error.Pos, + Msg: msg, + Kind: ListError, }) } @@ -947,7 +877,7 @@ func absJoin(dir string, fileses ...[]string) (res []string) { func golistargs(cfg *Config, words []string) []string { const findFlags = NeedImports | NeedTypes | NeedSyntax | NeedTypesInfo fullargs := []string{ - "list", "-e", "-json", + "-e", "-json", fmt.Sprintf("-compiled=%t", cfg.Mode&(NeedCompiledGoFiles|NeedSyntax|NeedTypesInfo|NeedTypesSizes) != 0), fmt.Sprintf("-test=%t", cfg.Tests), fmt.Sprintf("-export=%t", usesExportData(cfg)), @@ -963,10 +893,13 @@ func golistargs(cfg *Config, words []string) []string { } // invokeGo returns the stdout of a go command invocation. -func invokeGo(cfg *Config, args ...string) (*bytes.Buffer, error) { +func invokeGo(cfg *Config, verb string, args ...string) (*bytes.Buffer, error) { stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) - cmd := exec.CommandContext(cfg.Context, "go", args...) + goArgs := []string{verb} + goArgs = append(goArgs, cfg.BuildFlags...) + goArgs = append(goArgs, args...) + cmd := exec.CommandContext(cfg.Context, "go", goArgs...) // On darwin the cwd gets resolved to the real path, which breaks anything that // expects the working directory to keep the original path, including the // go command when dealing with modules. diff --git a/vendor/golang.org/x/tools/go/packages/loadmode_string.go b/vendor/golang.org/x/tools/go/packages/loadmode_string.go new file mode 100644 index 0000000000..aff94a3fe9 --- /dev/null +++ b/vendor/golang.org/x/tools/go/packages/loadmode_string.go @@ -0,0 +1,57 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package packages + +import ( + "fmt" + "strings" +) + +var allModes = []LoadMode{ + NeedName, + NeedFiles, + NeedCompiledGoFiles, + NeedImports, + NeedDeps, + NeedExportsFile, + NeedTypes, + NeedSyntax, + NeedTypesInfo, + NeedTypesSizes, +} + +var modeStrings = []string{ + "NeedName", + "NeedFiles", + "NeedCompiledGoFiles", + "NeedImports", + "NeedDeps", + "NeedExportsFile", + "NeedTypes", + "NeedSyntax", + "NeedTypesInfo", + "NeedTypesSizes", +} + +func (mod LoadMode) String() string { + m := mod + if m == 0 { + return fmt.Sprintf("LoadMode(0)") + } + var out []string + for i, x := range allModes { + if x > m { + break + } + if (m & x) != 0 { + out = append(out, modeStrings[i]) + m = m ^ x + } + } + if m != 0 { + out = append(out, "Unknown") + } + return fmt.Sprintf("LoadMode(%s)", strings.Join(out, "|")) +} diff --git a/vendor/golang.org/x/tools/go/packages/packages.go b/vendor/golang.org/x/tools/go/packages/packages.go index 050cca43a2..f98a0bdcae 100644 --- a/vendor/golang.org/x/tools/go/packages/packages.go +++ b/vendor/golang.org/x/tools/go/packages/packages.go @@ -160,7 +160,7 @@ type Config struct { Tests bool // Overlay provides a mapping of absolute file paths to file contents. - // If the file with the given path already exists, the parser will use the + // If the file with the given path already exists, the parser will use the // alternative file contents provided by the map. // // Overlays provide incomplete support for when a given file doesn't @@ -713,7 +713,7 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { // which would then require that such created packages be explicitly // inserted back into the Import graph as a final step after export data loading. // The Diamond test exercises this case. - if !lpkg.needtypes { + if !lpkg.needtypes && !lpkg.needsrc { return } if !lpkg.needsrc { diff --git a/vendor/golang.org/x/tools/internal/gopathwalk/walk.go b/vendor/golang.org/x/tools/internal/gopathwalk/walk.go index 9a61bdbf5d..d067562289 100644 --- a/vendor/golang.org/x/tools/internal/gopathwalk/walk.go +++ b/vendor/golang.org/x/tools/internal/gopathwalk/walk.go @@ -77,6 +77,7 @@ func WalkSkip(roots []Root, add func(root Root, dir string), skip func(root Root } } +// walkDir creates a walker and starts fastwalk with this walker. func walkDir(root Root, add func(Root, string), skip func(root Root, dir string) bool, opts Options) { if _, err := os.Stat(root.Path); os.IsNotExist(err) { if opts.Debug { @@ -114,7 +115,7 @@ type walker struct { ignoredDirs []os.FileInfo // The ignored directories, loaded from .goimportsignore files. } -// init initializes the walker based on its Options. +// init initializes the walker based on its Options func (w *walker) init() { var ignoredPaths []string if w.root.Type == RootModuleCache { @@ -167,6 +168,7 @@ func (w *walker) getIgnoredDirs(path string) []string { return ignoredDirs } +// shouldSkipDir reports whether the file should be skipped or not. func (w *walker) shouldSkipDir(fi os.FileInfo, dir string) bool { for _, ignoredDir := range w.ignoredDirs { if os.SameFile(fi, ignoredDir) { @@ -180,6 +182,7 @@ func (w *walker) shouldSkipDir(fi os.FileInfo, dir string) bool { return false } +// walk walks through the given path. func (w *walker) walk(path string, typ os.FileMode) error { dir := filepath.Dir(path) if typ.IsRegular() { diff --git a/vendor/golang.org/x/tools/internal/span/parse.go b/vendor/golang.org/x/tools/internal/span/parse.go deleted file mode 100644 index b3f268a38a..0000000000 --- a/vendor/golang.org/x/tools/internal/span/parse.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package span - -import ( - "strconv" - "strings" - "unicode/utf8" -) - -// Parse returns the location represented by the input. -// All inputs are valid locations, as they can always be a pure filename. -// The returned span will be normalized, and thus if printed may produce a -// different string. -func Parse(input string) Span { - // :0:0#0-0:0#0 - valid := input - var hold, offset int - hadCol := false - suf := rstripSuffix(input) - if suf.sep == "#" { - offset = suf.num - suf = rstripSuffix(suf.remains) - } - if suf.sep == ":" { - valid = suf.remains - hold = suf.num - hadCol = true - suf = rstripSuffix(suf.remains) - } - switch { - case suf.sep == ":": - return New(NewURI(suf.remains), NewPoint(suf.num, hold, offset), Point{}) - case suf.sep == "-": - // we have a span, fall out of the case to continue - default: - // separator not valid, rewind to either the : or the start - return New(NewURI(valid), NewPoint(hold, 0, offset), Point{}) - } - // only the span form can get here - // at this point we still don't know what the numbers we have mean - // if have not yet seen a : then we might have either a line or a column depending - // on whether start has a column or not - // we build an end point and will fix it later if needed - end := NewPoint(suf.num, hold, offset) - hold, offset = 0, 0 - suf = rstripSuffix(suf.remains) - if suf.sep == "#" { - offset = suf.num - suf = rstripSuffix(suf.remains) - } - if suf.sep != ":" { - // turns out we don't have a span after all, rewind - return New(NewURI(valid), end, Point{}) - } - valid = suf.remains - hold = suf.num - suf = rstripSuffix(suf.remains) - if suf.sep != ":" { - // line#offset only - return New(NewURI(valid), NewPoint(hold, 0, offset), end) - } - // we have a column, so if end only had one number, it is also the column - if !hadCol { - end = NewPoint(suf.num, end.v.Line, end.v.Offset) - } - return New(NewURI(suf.remains), NewPoint(suf.num, hold, offset), end) -} - -type suffix struct { - remains string - sep string - num int -} - -func rstripSuffix(input string) suffix { - if len(input) == 0 { - return suffix{"", "", -1} - } - remains := input - num := -1 - // first see if we have a number at the end - last := strings.LastIndexFunc(remains, func(r rune) bool { return r < '0' || r > '9' }) - if last >= 0 && last < len(remains)-1 { - number, err := strconv.ParseInt(remains[last+1:], 10, 64) - if err == nil { - num = int(number) - remains = remains[:last+1] - } - } - // now see if we have a trailing separator - r, w := utf8.DecodeLastRuneInString(remains) - if r != ':' && r != '#' && r == '#' { - return suffix{input, "", -1} - } - remains = remains[:len(remains)-w] - return suffix{remains, string(r), num} -} diff --git a/vendor/golang.org/x/tools/internal/span/span.go b/vendor/golang.org/x/tools/internal/span/span.go deleted file mode 100644 index 4d2ad09866..0000000000 --- a/vendor/golang.org/x/tools/internal/span/span.go +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package span contains support for representing with positions and ranges in -// text files. -package span - -import ( - "encoding/json" - "fmt" - "path" -) - -// Span represents a source code range in standardized form. -type Span struct { - v span -} - -// Point represents a single point within a file. -// In general this should only be used as part of a Span, as on its own it -// does not carry enough information. -type Point struct { - v point -} - -type span struct { - URI URI `json:"uri"` - Start point `json:"start"` - End point `json:"end"` -} - -type point struct { - Line int `json:"line"` - Column int `json:"column"` - Offset int `json:"offset"` -} - -// Invalid is a span that reports false from IsValid -var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}} - -var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}} - -// Converter is the interface to an object that can convert between line:column -// and offset forms for a single file. -type Converter interface { - //ToPosition converts from an offset to a line:column pair. - ToPosition(offset int) (int, int, error) - //ToOffset converts from a line:column pair to an offset. - ToOffset(line, col int) (int, error) -} - -func New(uri URI, start Point, end Point) Span { - s := Span{v: span{URI: uri, Start: start.v, End: end.v}} - s.v.clean() - return s -} - -func NewPoint(line, col, offset int) Point { - p := Point{v: point{Line: line, Column: col, Offset: offset}} - p.v.clean() - return p -} - -func Compare(a, b Span) int { - if r := CompareURI(a.URI(), b.URI()); r != 0 { - return r - } - if r := comparePoint(a.v.Start, b.v.Start); r != 0 { - return r - } - return comparePoint(a.v.End, b.v.End) -} - -func ComparePoint(a, b Point) int { - return comparePoint(a.v, b.v) -} - -func comparePoint(a, b point) int { - if !a.hasPosition() { - if a.Offset < b.Offset { - return -1 - } - if a.Offset > b.Offset { - return 1 - } - return 0 - } - if a.Line < b.Line { - return -1 - } - if a.Line > b.Line { - return 1 - } - if a.Column < b.Column { - return -1 - } - if a.Column > b.Column { - return 1 - } - return 0 -} - -func (s Span) HasPosition() bool { return s.v.Start.hasPosition() } -func (s Span) HasOffset() bool { return s.v.Start.hasOffset() } -func (s Span) IsValid() bool { return s.v.Start.isValid() } -func (s Span) IsPoint() bool { return s.v.Start == s.v.End } -func (s Span) URI() URI { return s.v.URI } -func (s Span) Start() Point { return Point{s.v.Start} } -func (s Span) End() Point { return Point{s.v.End} } -func (s *Span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) } -func (s *Span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) } - -func (p Point) HasPosition() bool { return p.v.hasPosition() } -func (p Point) HasOffset() bool { return p.v.hasOffset() } -func (p Point) IsValid() bool { return p.v.isValid() } -func (p *Point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) } -func (p *Point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) } -func (p Point) Line() int { - if !p.v.hasPosition() { - panic(fmt.Errorf("position not set in %v", p.v)) - } - return p.v.Line -} -func (p Point) Column() int { - if !p.v.hasPosition() { - panic(fmt.Errorf("position not set in %v", p.v)) - } - return p.v.Column -} -func (p Point) Offset() int { - if !p.v.hasOffset() { - panic(fmt.Errorf("offset not set in %v", p.v)) - } - return p.v.Offset -} - -func (p point) hasPosition() bool { return p.Line > 0 } -func (p point) hasOffset() bool { return p.Offset >= 0 } -func (p point) isValid() bool { return p.hasPosition() || p.hasOffset() } -func (p point) isZero() bool { - return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0) -} - -func (s *span) clean() { - //this presumes the points are already clean - if !s.End.isValid() || (s.End == point{}) { - s.End = s.Start - } -} - -func (p *point) clean() { - if p.Line < 0 { - p.Line = 0 - } - if p.Column <= 0 { - if p.Line > 0 { - p.Column = 1 - } else { - p.Column = 0 - } - } - if p.Offset == 0 && (p.Line > 1 || p.Column > 1) { - p.Offset = -1 - } -} - -// Format implements fmt.Formatter to print the Location in a standard form. -// The format produced is one that can be read back in using Parse. -func (s Span) Format(f fmt.State, c rune) { - fullForm := f.Flag('+') - preferOffset := f.Flag('#') - // we should always have a uri, simplify if it is file format - //TODO: make sure the end of the uri is unambiguous - uri := string(s.v.URI) - if c == 'f' { - uri = path.Base(uri) - } else if !fullForm { - uri = s.v.URI.Filename() - } - fmt.Fprint(f, uri) - if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) { - return - } - // see which bits of start to write - printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition()) - printLine := s.HasPosition() && (fullForm || !printOffset) - printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1)) - fmt.Fprint(f, ":") - if printLine { - fmt.Fprintf(f, "%d", s.v.Start.Line) - } - if printColumn { - fmt.Fprintf(f, ":%d", s.v.Start.Column) - } - if printOffset { - fmt.Fprintf(f, "#%d", s.v.Start.Offset) - } - // start is written, do we need end? - if s.IsPoint() { - return - } - // we don't print the line if it did not change - printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line) - fmt.Fprint(f, "-") - if printLine { - fmt.Fprintf(f, "%d", s.v.End.Line) - } - if printColumn { - if printLine { - fmt.Fprint(f, ":") - } - fmt.Fprintf(f, "%d", s.v.End.Column) - } - if printOffset { - fmt.Fprintf(f, "#%d", s.v.End.Offset) - } -} - -func (s Span) WithPosition(c Converter) (Span, error) { - if err := s.update(c, true, false); err != nil { - return Span{}, err - } - return s, nil -} - -func (s Span) WithOffset(c Converter) (Span, error) { - if err := s.update(c, false, true); err != nil { - return Span{}, err - } - return s, nil -} - -func (s Span) WithAll(c Converter) (Span, error) { - if err := s.update(c, true, true); err != nil { - return Span{}, err - } - return s, nil -} - -func (s *Span) update(c Converter, withPos, withOffset bool) error { - if !s.IsValid() { - return fmt.Errorf("cannot add information to an invalid span") - } - if withPos && !s.HasPosition() { - if err := s.v.Start.updatePosition(c); err != nil { - return err - } - if s.v.End.Offset == s.v.Start.Offset { - s.v.End = s.v.Start - } else if err := s.v.End.updatePosition(c); err != nil { - return err - } - } - if withOffset && (!s.HasOffset() || (s.v.End.hasPosition() && !s.v.End.hasOffset())) { - if err := s.v.Start.updateOffset(c); err != nil { - return err - } - if s.v.End.Line == s.v.Start.Line && s.v.End.Column == s.v.Start.Column { - s.v.End.Offset = s.v.Start.Offset - } else if err := s.v.End.updateOffset(c); err != nil { - return err - } - } - return nil -} - -func (p *point) updatePosition(c Converter) error { - line, col, err := c.ToPosition(p.Offset) - if err != nil { - return err - } - p.Line = line - p.Column = col - return nil -} - -func (p *point) updateOffset(c Converter) error { - offset, err := c.ToOffset(p.Line, p.Column) - if err != nil { - return err - } - p.Offset = offset - return nil -} diff --git a/vendor/golang.org/x/tools/internal/span/token.go b/vendor/golang.org/x/tools/internal/span/token.go deleted file mode 100644 index ce44541b2f..0000000000 --- a/vendor/golang.org/x/tools/internal/span/token.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package span - -import ( - "fmt" - "go/token" -) - -// Range represents a source code range in token.Pos form. -// It also carries the FileSet that produced the positions, so that it is -// self contained. -type Range struct { - FileSet *token.FileSet - Start token.Pos - End token.Pos -} - -// TokenConverter is a Converter backed by a token file set and file. -// It uses the file set methods to work out the conversions, which -// makes it fast and does not require the file contents. -type TokenConverter struct { - fset *token.FileSet - file *token.File -} - -// NewRange creates a new Range from a FileSet and two positions. -// To represent a point pass a 0 as the end pos. -func NewRange(fset *token.FileSet, start, end token.Pos) Range { - return Range{ - FileSet: fset, - Start: start, - End: end, - } -} - -// NewTokenConverter returns an implementation of Converter backed by a -// token.File. -func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter { - return &TokenConverter{fset: fset, file: f} -} - -// NewContentConverter returns an implementation of Converter for the -// given file content. -func NewContentConverter(filename string, content []byte) *TokenConverter { - fset := token.NewFileSet() - f := fset.AddFile(filename, -1, len(content)) - f.SetLinesForContent(content) - return &TokenConverter{fset: fset, file: f} -} - -// IsPoint returns true if the range represents a single point. -func (r Range) IsPoint() bool { - return r.Start == r.End -} - -// Span converts a Range to a Span that represents the Range. -// It will fill in all the members of the Span, calculating the line and column -// information. -func (r Range) Span() (Span, error) { - f := r.FileSet.File(r.Start) - if f == nil { - return Span{}, fmt.Errorf("file not found in FileSet") - } - s := Span{v: span{URI: FileURI(f.Name())}} - var err error - s.v.Start.Offset, err = offset(f, r.Start) - if err != nil { - return Span{}, err - } - if r.End.IsValid() { - s.v.End.Offset, err = offset(f, r.End) - if err != nil { - return Span{}, err - } - } - s.v.Start.clean() - s.v.End.clean() - s.v.clean() - converter := NewTokenConverter(r.FileSet, f) - return s.WithPosition(converter) -} - -// offset is a copy of the Offset function in go/token, but with the adjustment -// that it does not panic on invalid positions. -func offset(f *token.File, pos token.Pos) (int, error) { - if int(pos) < f.Base() || int(pos) > f.Base()+f.Size() { - return 0, fmt.Errorf("invalid pos") - } - return int(pos) - f.Base(), nil -} - -// Range converts a Span to a Range that represents the Span for the supplied -// File. -func (s Span) Range(converter *TokenConverter) (Range, error) { - s, err := s.WithOffset(converter) - if err != nil { - return Range{}, err - } - // go/token will panic if the offset is larger than the file's size, - // so check here to avoid panicking. - if s.Start().Offset() > converter.file.Size() { - return Range{}, fmt.Errorf("start offset %v is past the end of the file %v", s.Start(), converter.file.Size()) - } - if s.End().Offset() > converter.file.Size() { - return Range{}, fmt.Errorf("end offset %v is past the end of the file %v", s.End(), converter.file.Size()) - } - return Range{ - FileSet: converter.fset, - Start: converter.file.Pos(s.Start().Offset()), - End: converter.file.Pos(s.End().Offset()), - }, nil -} - -func (l *TokenConverter) ToPosition(offset int) (int, int, error) { - if offset > l.file.Size() { - return 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, l.file.Size()) - } - pos := l.file.Pos(offset) - p := l.fset.Position(pos) - if offset == l.file.Size() { - return p.Line + 1, 1, nil - } - return p.Line, p.Column, nil -} - -func (l *TokenConverter) ToOffset(line, col int) (int, error) { - if line < 0 { - return -1, fmt.Errorf("line is not valid") - } - lineMax := l.file.LineCount() + 1 - if line > lineMax { - return -1, fmt.Errorf("line is beyond end of file %v", lineMax) - } else if line == lineMax { - if col > 1 { - return -1, fmt.Errorf("column is beyond end of file") - } - // at the end of the file, allowing for a trailing eol - return l.file.Size(), nil - } - pos := lineStart(l.file, line) - if !pos.IsValid() { - return -1, fmt.Errorf("line is not in file") - } - // we assume that column is in bytes here, and that the first byte of a - // line is at column 1 - pos += token.Pos(col - 1) - return offset(l.file, pos) -} diff --git a/vendor/golang.org/x/tools/internal/span/token111.go b/vendor/golang.org/x/tools/internal/span/token111.go deleted file mode 100644 index bf7a5406b6..0000000000 --- a/vendor/golang.org/x/tools/internal/span/token111.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.12 - -package span - -import ( - "go/token" -) - -// lineStart is the pre-Go 1.12 version of (*token.File).LineStart. For Go -// versions <= 1.11, we borrow logic from the analysisutil package. -// TODO(rstambler): Delete this file when we no longer support Go 1.11. -func lineStart(f *token.File, line int) token.Pos { - // Use binary search to find the start offset of this line. - - min := 0 // inclusive - max := f.Size() // exclusive - for { - offset := (min + max) / 2 - pos := f.Pos(offset) - posn := f.Position(pos) - if posn.Line == line { - return pos - (token.Pos(posn.Column) - 1) - } - - if min+1 >= max { - return token.NoPos - } - - if posn.Line < line { - min = offset - } else { - max = offset - } - } -} diff --git a/vendor/golang.org/x/tools/internal/span/token112.go b/vendor/golang.org/x/tools/internal/span/token112.go deleted file mode 100644 index 017aec9c13..0000000000 --- a/vendor/golang.org/x/tools/internal/span/token112.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.12 - -package span - -import ( - "go/token" -) - -// TODO(rstambler): Delete this file when we no longer support Go 1.11. -func lineStart(f *token.File, line int) token.Pos { - return f.LineStart(line) -} diff --git a/vendor/golang.org/x/tools/internal/span/uri.go b/vendor/golang.org/x/tools/internal/span/uri.go deleted file mode 100644 index e05a9e6ef5..0000000000 --- a/vendor/golang.org/x/tools/internal/span/uri.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package span - -import ( - "fmt" - "net/url" - "os" - "path" - "path/filepath" - "runtime" - "strings" - "unicode" -) - -const fileScheme = "file" - -// URI represents the full URI for a file. -type URI string - -// Filename returns the file path for the given URI. -// It is an error to call this on a URI that is not a valid filename. -func (uri URI) Filename() string { - filename, err := filename(uri) - if err != nil { - panic(err) - } - return filepath.FromSlash(filename) -} - -func filename(uri URI) (string, error) { - if uri == "" { - return "", nil - } - u, err := url.ParseRequestURI(string(uri)) - if err != nil { - return "", err - } - if u.Scheme != fileScheme { - return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri) - } - if isWindowsDriveURI(u.Path) { - u.Path = u.Path[1:] - } - return u.Path, nil -} - -// NewURI returns a span URI for the string. -// It will attempt to detect if the string is a file path or uri. -func NewURI(s string) URI { - if u, err := url.PathUnescape(s); err == nil { - s = u - } - if strings.HasPrefix(s, fileScheme+"://") { - return URI(s) - } - return FileURI(s) -} - -func CompareURI(a, b URI) int { - if equalURI(a, b) { - return 0 - } - if a < b { - return -1 - } - return 1 -} - -func equalURI(a, b URI) bool { - if a == b { - return true - } - // If we have the same URI basename, we may still have the same file URIs. - if !strings.EqualFold(path.Base(string(a)), path.Base(string(b))) { - return false - } - fa, err := filename(a) - if err != nil { - return false - } - fb, err := filename(b) - if err != nil { - return false - } - // Stat the files to check if they are equal. - infoa, err := os.Stat(filepath.FromSlash(fa)) - if err != nil { - return false - } - infob, err := os.Stat(filepath.FromSlash(fb)) - if err != nil { - return false - } - return os.SameFile(infoa, infob) -} - -// FileURI returns a span URI for the supplied file path. -// It will always have the file scheme. -func FileURI(path string) URI { - if path == "" { - return "" - } - // Handle standard library paths that contain the literal "$GOROOT". - // TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT. - const prefix = "$GOROOT" - if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) { - suffix := path[len(prefix):] - path = runtime.GOROOT() + suffix - } - if !isWindowsDrivePath(path) { - if abs, err := filepath.Abs(path); err == nil { - path = abs - } - } - // Check the file path again, in case it became absolute. - if isWindowsDrivePath(path) { - path = "/" + path - } - path = filepath.ToSlash(path) - u := url.URL{ - Scheme: fileScheme, - Path: path, - } - uri := u.String() - if unescaped, err := url.PathUnescape(uri); err == nil { - uri = unescaped - } - return URI(uri) -} - -// isWindowsDrivePath returns true if the file path is of the form used by -// Windows. We check if the path begins with a drive letter, followed by a ":". -func isWindowsDrivePath(path string) bool { - if len(path) < 4 { - return false - } - return unicode.IsLetter(rune(path[0])) && path[1] == ':' -} - -// isWindowsDriveURI returns true if the file URI is of the format used by -// Windows URIs. The url.Parse package does not specially handle Windows paths -// (see https://golang.org/issue/6027). We check if the URI path has -// a drive prefix (e.g. "/C:"). If so, we trim the leading "/". -func isWindowsDriveURI(uri string) bool { - if len(uri) < 4 { - return false - } - return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' -} diff --git a/vendor/golang.org/x/tools/internal/span/utf16.go b/vendor/golang.org/x/tools/internal/span/utf16.go deleted file mode 100644 index 561b3fa50a..0000000000 --- a/vendor/golang.org/x/tools/internal/span/utf16.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package span - -import ( - "fmt" - "unicode/utf16" - "unicode/utf8" -) - -// ToUTF16Column calculates the utf16 column expressed by the point given the -// supplied file contents. -// This is used to convert from the native (always in bytes) column -// representation and the utf16 counts used by some editors. -func ToUTF16Column(p Point, content []byte) (int, error) { - if content == nil { - return -1, fmt.Errorf("ToUTF16Column: missing content") - } - if !p.HasPosition() { - return -1, fmt.Errorf("ToUTF16Column: point is missing position") - } - if !p.HasOffset() { - return -1, fmt.Errorf("ToUTF16Column: point is missing offset") - } - offset := p.Offset() // 0-based - colZero := p.Column() - 1 // 0-based - if colZero == 0 { - // 0-based column 0, so it must be chr 1 - return 1, nil - } else if colZero < 0 { - return -1, fmt.Errorf("ToUTF16Column: column is invalid (%v)", colZero) - } - // work out the offset at the start of the line using the column - lineOffset := offset - colZero - if lineOffset < 0 || offset > len(content) { - return -1, fmt.Errorf("ToUTF16Column: offsets %v-%v outside file contents (%v)", lineOffset, offset, len(content)) - } - // Use the offset to pick out the line start. - // This cannot panic: offset > len(content) and lineOffset < offset. - start := content[lineOffset:] - - // Now, truncate down to the supplied column. - start = start[:colZero] - - // and count the number of utf16 characters - // in theory we could do this by hand more efficiently... - return len(utf16.Encode([]rune(string(start)))) + 1, nil -} - -// FromUTF16Column advances the point by the utf16 character offset given the -// supplied line contents. -// This is used to convert from the utf16 counts used by some editors to the -// native (always in bytes) column representation. -func FromUTF16Column(p Point, chr int, content []byte) (Point, error) { - if !p.HasOffset() { - return Point{}, fmt.Errorf("FromUTF16Column: point is missing offset") - } - // if chr is 1 then no adjustment needed - if chr <= 1 { - return p, nil - } - if p.Offset() >= len(content) { - return p, fmt.Errorf("FromUTF16Column: offset (%v) greater than length of content (%v)", p.Offset(), len(content)) - } - remains := content[p.Offset():] - // scan forward the specified number of characters - for count := 1; count < chr; count++ { - if len(remains) <= 0 { - return Point{}, fmt.Errorf("FromUTF16Column: chr goes beyond the content") - } - r, w := utf8.DecodeRune(remains) - if r == '\n' { - // Per the LSP spec: - // - // > If the character value is greater than the line length it - // > defaults back to the line length. - break - } - remains = remains[w:] - if r >= 0x10000 { - // a two point rune - count++ - // if we finished in a two point rune, do not advance past the first - if count >= chr { - break - } - } - p.v.Column += w - p.v.Offset += w - } - return p, nil -} diff --git a/vendor/modules.txt b/vendor/modules.txt index d4c6a3ca6f..be714d0eda 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -282,6 +282,8 @@ github.com/lucasb-eyer/go-colorful github.com/mattn/go-colorable # github.com/mattn/go-isatty v0.0.7 github.com/mattn/go-isatty +# github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f +github.com/mattn/go-pointer # github.com/mattn/go-runewidth v0.0.4 github.com/mattn/go-runewidth # github.com/matttproud/golang_protobuf_extensions v1.0.1 @@ -371,6 +373,7 @@ github.com/status-im/rendezvous/server # github.com/status-im/status-go/eth-node v1.1.0 => ./eth-node github.com/status-im/status-go/eth-node/bridge/geth github.com/status-im/status-go/eth-node/bridge/geth/ens +github.com/status-im/status-go/eth-node/bridge/nimbus github.com/status-im/status-go/eth-node/core/types github.com/status-im/status-go/eth-node/crypto github.com/status-im/status-go/eth-node/crypto/ecies @@ -541,7 +544,7 @@ golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm -# golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101 +# golang.org/x/tools v0.0.0-20200116062425-473961ec044c golang.org/x/tools/go/analysis golang.org/x/tools/go/analysis/passes/inspect golang.org/x/tools/go/ast/astutil @@ -556,7 +559,6 @@ golang.org/x/tools/go/types/typeutil golang.org/x/tools/internal/fastwalk golang.org/x/tools/internal/gopathwalk golang.org/x/tools/internal/semver -golang.org/x/tools/internal/span # gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/go-playground/validator.v9 # gopkg.in/natefinch/lumberjack.v2 v2.0.0 From db01f0b3e45170e7027c0fdc1d6dea8ea3c0e4e2 Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro Date: Fri, 10 Jan 2020 11:09:07 +0100 Subject: [PATCH 3/9] Avoid passing node to subscriptions service --- api/geth_backend.go | 2 +- api/nimbus_backend.go | 2 +- services/subscriptions/api.go | 18 ++++++++---------- services/subscriptions/service.go | 12 ++++++------ 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/api/geth_backend.go b/api/geth_backend.go index 5ab90c5886..92837fa089 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -383,7 +383,7 @@ func (b *GethStatusBackend) rpcFiltersService() gethnode.ServiceConstructor { func (b *GethStatusBackend) subscriptionService() gethnode.ServiceConstructor { return func(*gethnode.ServiceContext) (gethnode.Service, error) { - return subscriptions.New(b.statusNode), nil + return subscriptions.New(func() *rpc.Client { return b.statusNode.RPCPrivateClient() }), nil } } diff --git a/api/nimbus_backend.go b/api/nimbus_backend.go index d18d18449b..611b645b5a 100644 --- a/api/nimbus_backend.go +++ b/api/nimbus_backend.go @@ -377,7 +377,7 @@ func (b *nimbusStatusBackend) rpcFiltersService() nimbussvc.ServiceConstructor { func (b *nimbusStatusBackend) subscriptionService() nimbussvc.ServiceConstructor { return func(*nimbussvc.ServiceContext) (nimbussvc.Service, error) { - return subscriptions.New(b.statusNode), nil + return subscriptions.New(func() *rpc.Client { return b.statusNode.RPCPrivateClient() }), nil } } diff --git a/services/subscriptions/api.go b/services/subscriptions/api.go index 9018b75955..acde8cd169 100644 --- a/services/subscriptions/api.go +++ b/services/subscriptions/api.go @@ -4,18 +4,18 @@ import ( "fmt" "time" - "github.com/status-im/status-go/node" + "github.com/status-im/status-go/rpc" ) type API struct { - node *node.StatusNode - activeSubscriptions *Subscriptions + rpcPrivateClientFunc func() *rpc.Client + activeSubscriptions *Subscriptions } -func NewPublicAPI(node *node.StatusNode) *API { +func NewPublicAPI(rpcPrivateClientFunc func() *rpc.Client) *API { return &API{ - node: node, - activeSubscriptions: NewSubscriptions(100 * time.Millisecond), + rpcPrivateClientFunc: rpcPrivateClientFunc, + activeSubscriptions: NewSubscriptions(100 * time.Millisecond), } } @@ -26,13 +26,11 @@ func (api *API) SubscribeSignal(method string, args []interface{}) (Subscription namespace = method[:3] ) - rpc := api.node.RPCPrivateClient() - switch namespace { case "shh": - filter, err = installShhFilter(rpc, method, args) + filter, err = installShhFilter(api.rpcPrivateClientFunc(), method, args) case "eth": - filter, err = installEthFilter(rpc, method, args) + filter, err = installEthFilter(api.rpcPrivateClientFunc(), method, args) default: err = fmt.Errorf("unexpected namespace: %s", namespace) } diff --git a/services/subscriptions/service.go b/services/subscriptions/service.go index 25a7927a2d..e24487c9d1 100644 --- a/services/subscriptions/service.go +++ b/services/subscriptions/service.go @@ -3,9 +3,9 @@ package subscriptions import ( gethnode "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rpc" + gethrpc "github.com/ethereum/go-ethereum/rpc" - "github.com/status-im/status-go/node" + "github.com/status-im/status-go/rpc" ) // Make sure that Service implements gethnode.Service interface. @@ -17,9 +17,9 @@ type Service struct { } // New returns a new Service. -func New(node *node.StatusNode) *Service { +func New(rpcPrivateClientFunc func() *rpc.Client) *Service { return &Service{ - api: NewPublicAPI(node), + api: NewPublicAPI(rpcPrivateClientFunc), } } @@ -29,8 +29,8 @@ func (s *Service) Protocols() []p2p.Protocol { } // APIs returns a list of new APIs. -func (s *Service) APIs() []rpc.API { - return []rpc.API{ +func (s *Service) APIs() []gethrpc.API { + return []gethrpc.API{ { Namespace: "eth", Version: "1.0", From d4710faae21443aff751722f01ec62d1be82fb26 Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro Date: Mon, 6 Jan 2020 10:30:35 +0100 Subject: [PATCH 4/9] In progress: Use Nimbus keystore --- account/accounts_nimbus.go | 8 +- account/keystore_geth.go | 2 + account/keystore_nimbus.go | 51 ++++ eth-node/bridge/nimbus/keystore.go | 263 ++++++++++++++++++ eth-node/go.mod | 1 + eth-node/go.sum | 15 +- .../eth-node/bridge/nimbus/keystore.go | 263 ++++++++++++++++++ 7 files changed, 592 insertions(+), 11 deletions(-) create mode 100644 account/keystore_nimbus.go create mode 100644 eth-node/bridge/nimbus/keystore.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/keystore.go diff --git a/account/accounts_nimbus.go b/account/accounts_nimbus.go index b3ceb62ead..7f8bcaf78b 100644 --- a/account/accounts_nimbus.go +++ b/account/accounts_nimbus.go @@ -23,11 +23,7 @@ func (m *Manager) InitKeystore(keydir string) error { m.mu.Lock() defer m.mu.Unlock() - // TODO: Wire with the Nimbus keystore - manager, err := makeAccountManager(keydir) - if err != nil { - return err - } - m.keystore, err = makeKeyStore(manager) + var err error + m.keystore, err = makeKeyStore(keydir) return err } diff --git a/account/keystore_geth.go b/account/keystore_geth.go index a4bebc6603..b3d9044f76 100644 --- a/account/keystore_geth.go +++ b/account/keystore_geth.go @@ -1,3 +1,5 @@ +// +build !nimbus + // TODO: Make independent version for Nimbus package account diff --git a/account/keystore_nimbus.go b/account/keystore_nimbus.go new file mode 100644 index 0000000000..ecf8896b54 --- /dev/null +++ b/account/keystore_nimbus.go @@ -0,0 +1,51 @@ +// +build nimbus + +package account + +import ( + // "io/ioutil" + // "os" + + // gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + nimbusbridge "github.com/status-im/status-go/eth-node/bridge/nimbus" + "github.com/status-im/status-go/eth-node/types" +) + +// // makeAccountManager creates ethereum accounts.Manager with single disk backend and lightweight kdf. +// // If keydir is empty new temporary directory with go-ethereum-keystore will be intialized. +// func makeAccountManager(keydir string) (manager *accounts.Manager, err error) { +// if keydir == "" { +// // There is no datadir. +// keydir, err = ioutil.TempDir("", "nimbus-keystore") +// } +// if err != nil { +// return nil, err +// } +// if err := os.MkdirAll(keydir, 0700); err != nil { +// return nil, err +// } +// config := accounts.Config{InsecureUnlockAllowed: false} +// return accounts.NewManager(&config, keystore.NewKeyStore(keydir, keystore.LightScryptN, keystore.LightScryptP)), nil +// } + +// func makeKeyStore(keydir string) (types.KeyStore, error) { +// manager, err := makeAccountManager(keydir) +// if err != nil { +// return err +// } + +// backends := manager.Backends(keystore.KeyStoreType) +// if len(backends) == 0 { +// return nil, ErrAccountKeyStoreMissing +// } +// keyStore, ok := backends[0].(*keystore.KeyStore) +// if !ok { +// return nil, ErrAccountKeyStoreMissing +// } + +// return gethbridge.WrapKeyStore(keyStore), nil +// } + +func makeKeyStore(_ string) (types.KeyStore, error) { + return nimbusbridge.WrapKeyStore(), nil +} diff --git a/eth-node/bridge/nimbus/keystore.go b/eth-node/bridge/nimbus/keystore.go new file mode 100644 index 0000000000..43ac4d3b0c --- /dev/null +++ b/eth-node/bridge/nimbus/keystore.go @@ -0,0 +1,263 @@ +// +build nimbus + +package nimbusbridge + +// https://golang.org/cmd/cgo/ + +/* +#include +#include +#include +#include +*/ +import "C" +import ( + "crypto/ecdsa" + "encoding/binary" + "errors" + "fmt" + "math/big" + "unsafe" + + "github.com/btcsuite/btcd/btcec" + "github.com/pborman/uuid" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/extkeys" +) + +var ( + ErrInvalidSeed = errors.New("seed is invalid") + ErrInvalidKeyLen = errors.New("Nimbus serialized extended key length is invalid") +) + +type nimbusKeyStoreAdapter struct { +} + +// WrapKeyStore creates a types.KeyStore wrapper over the singleton Nimbus node +func WrapKeyStore() types.KeyStore { + return &nimbusKeyStoreAdapter{} +} + +func (k *nimbusKeyStoreAdapter) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (types.Account, error) { + fmt.Println("ImportECDSA") + panic("ImportECDSA") + + var privateKeyC unsafe.Pointer + if priv != nil { + privateKeyC = C.CBytes(crypto.FromECDSA(priv)) + defer C.free(privateKeyC) + } + passphraseC := C.CString(passphrase) + defer C.free(unsafe.Pointer(passphraseC)) + + var nimbusAccount C.account + if !C.nimbus_keystore_import_ecdsa((*C.uchar)(privateKeyC), passphraseC, &nimbusAccount) { + return types.Account{}, errors.New("failed to import ECDSA private key") + } + return accountFrom(&nimbusAccount), nil +} + +func (k *nimbusKeyStoreAdapter) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (types.Account, error) { + fmt.Println("ImportSingleExtendedKey") + panic("ImportSingleExtendedKey") + + extKeyJSONC := C.CString(extKey.String()) + defer C.free(unsafe.Pointer(extKeyJSONC)) + passphraseC := C.CString(passphrase) + defer C.free(unsafe.Pointer(passphraseC)) + + var nimbusAccount C.account + if !C.nimbus_keystore_import_single_extendedkey(extKeyJSONC, passphraseC, &nimbusAccount) { + return types.Account{}, errors.New("failed to import extended key") + } + return accountFrom(&nimbusAccount), nil +} + +func (k *nimbusKeyStoreAdapter) ImportExtendedKeyForPurpose(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, passphrase string) (types.Account, error) { + fmt.Println("ImportExtendedKeyForPurpose") + + passphraseC := C.CString(passphrase) + defer C.free(unsafe.Pointer(passphraseC)) + extKeyJSONC := C.CString(extKey.String()) + defer C.free(unsafe.Pointer(extKeyJSONC)) + + var nimbusAccount C.account + if !C.nimbus_keystore_import_extendedkeyforpurpose(C.int(keyPurpose), extKeyJSONC, passphraseC, &nimbusAccount) { + return types.Account{}, errors.New("failed to import extended key") + } + return accountFrom(&nimbusAccount), nil +} + +func (k *nimbusKeyStoreAdapter) AccountDecryptedKey(a types.Account, auth string) (types.Account, *types.Key, error) { + fmt.Println("AccountDecryptedKey") + panic("AccountDecryptedKey") + + authC := C.CString(auth) + defer C.free(unsafe.Pointer(authC)) + + var nimbusAccount C.account + err := nimbusAccountFrom(a, &nimbusAccount) + if err != nil { + return types.Account{}, nil, err + } + + var nimbusKey C.key + if !C.nimbus_keystore_account_decrypted_key(authC, &nimbusAccount, &nimbusKey) { + return types.Account{}, nil, errors.New("failed to decrypt account key") + } + key, err := keyFrom(&nimbusKey) + if err != nil { + return types.Account{}, nil, err + } + return accountFrom(&nimbusAccount), key, nil +} + +func (k *nimbusKeyStoreAdapter) Delete(a types.Account, auth string) error { + fmt.Println("Delete") + + var nimbusAccount C.account + err := nimbusAccountFrom(a, &nimbusAccount) + if err != nil { + return err + } + + authC := C.CString(auth) + defer C.free(unsafe.Pointer(authC)) + + if !C.nimbus_keystore_delete(&nimbusAccount, authC) { + return errors.New("failed to delete account") + } + + return nil +} + +func nimbusAccountFrom(account types.Account, nimbusAccount *C.account) error { + fmt.Println("nimbusAccountFrom") + err := copyAddressToCBuffer(&nimbusAccount.address[0], account.Address.Bytes()) + if err != nil { + return err + } + if account.URL == "" { + nimbusAccount.url[0] = C.char(0) + } else if len(account.URL) >= C.URL_LEN { + return errors.New("URL is too long to fit in Nimbus struct") + } else { + copyURLToCBuffer(&nimbusAccount.url[0], account.URL) + } + return err +} + +func accountFrom(nimbusAccount *C.account) types.Account { + return types.Account{ + Address: types.BytesToAddress(C.GoBytes(unsafe.Pointer(&nimbusAccount.address[0]), C.ADDRESS_LEN)), + URL: C.GoString(&nimbusAccount.url[0]), + } +} + +// copyAddressToCBuffer copies a Go buffer to a C buffer without allocating new memory +func copyAddressToCBuffer(dst *C.uchar, src []byte) error { + if len(src) != C.ADDRESS_LEN { + return errors.New("invalid buffer size") + } + + p := (*[C.ADDRESS_LEN]C.uchar)(unsafe.Pointer(dst)) + for index, b := range src { + p[index] = C.uchar(b) + } + + return nil +} + +// copyURLToCBuffer copies a Go buffer to a C buffer without allocating new memory +func copyURLToCBuffer(dst *C.char, src string) error { + if len(src)+1 > C.URL_LEN { + return errors.New("URL is too long to fit in Nimbus struct") + } + + p := (*[C.URL_LEN]C.uchar)(unsafe.Pointer(dst)) + for index := 0; index <= len(src); index++ { + p[index] = C.uchar(src[index]) + } + + return nil +} + +func keyFrom(k *C.key) (*types.Key, error) { + fmt.Println("keyFrom") + if k == nil { + return nil, nil + } + + var err error + key := types.Key{ + Id: uuid.Parse(C.GoString(&k.id[0])), + } + key.Address = types.BytesToAddress(C.GoBytes(unsafe.Pointer(&k.address[0]), C.ADDRESS_LEN)) + key.PrivateKey, err = crypto.ToECDSA(C.GoBytes(unsafe.Pointer(&k.privateKeyID[0]), C.PRIVKEY_LEN)) + if err != nil { + return nil, err + } + key.ExtendedKey, err = newExtKeyFromBuffer(C.GoBytes(unsafe.Pointer(&k.extKey[0]), C.EXTKEY_LEN)) + if err != nil { + return nil, err + } + return &key, err +} + +// newExtKeyFromBuffer returns a new extended key instance from a serialized +// extended key. +func newExtKeyFromBuffer(key []byte) (*extkeys.ExtendedKey, error) { + if len(key) == 0 { + return &extkeys.ExtendedKey{}, nil + } + + if len(key) != C.EXTKEY_LEN { + return nil, ErrInvalidKeyLen + } + + // The serialized format is: + // version (4) || depth (1) || parent fingerprint (4)) || + // child num (4) || chain code (32) || key data (33) + + payload := key + + // Deserialize each of the payload fields. + version := payload[:4] + depth := payload[4:5][0] + fingerPrint := payload[5:9] + childNumber := binary.BigEndian.Uint32(payload[9:13]) + chainCode := payload[13:45] + keyData := payload[45:78] + + // The key data is a private key if it starts with 0x00. Serialized + // compressed pubkeys either start with 0x02 or 0x03. + isPrivate := keyData[0] == 0x00 + if isPrivate { + // Ensure the private key is valid. It must be within the range + // of the order of the secp256k1 curve and not be 0. + keyData = keyData[1:] + keyNum := new(big.Int).SetBytes(keyData) + if keyNum.Cmp(btcec.S256().N) >= 0 || keyNum.Sign() == 0 { + return nil, ErrInvalidSeed + } + } else { + // Ensure the public key parses correctly and is actually on the + // secp256k1 curve. + _, err := btcec.ParsePubKey(keyData, btcec.S256()) + if err != nil { + return nil, err + } + } + + return &extkeys.ExtendedKey{ + Version: version, + KeyData: keyData, + ChainCode: chainCode, + FingerPrint: fingerPrint, + Depth: depth, + ChildNumber: childNumber, + IsPrivate: isPrivate, + }, nil +} diff --git a/eth-node/go.mod b/eth-node/go.mod index 3d0660db4b..6531f17c1e 100644 --- a/eth-node/go.mod +++ b/eth-node/go.mod @@ -11,6 +11,7 @@ replace github.com/status-im/status-go/whisper/v6 => ../whisper replace github.com/status-im/status-go/waku => ../waku require ( + github.com/btcsuite/btcd v0.20.1-beta github.com/ethereum/go-ethereum v1.9.5 github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f github.com/pborman/uuid v1.2.0 diff --git a/eth-node/go.sum b/eth-node/go.sum index c8360de90d..24c52124a2 100644 --- a/eth-node/go.sum +++ b/eth-node/go.sum @@ -31,6 +31,7 @@ github.com/aristanetworks/goarista v0.0.0-20191106175434-873d404c7f40/go.mod h1: github.com/aristanetworks/splunk-hec-go v0.3.3/go.mod h1:1VHO9r17b0K7WmOlLb9nTk/2YanvOEnLMUgsFrxBROc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/btcsuite/btcd v0.0.0-20181013004428-67e573d211ac/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= github.com/btcsuite/btcd v0.0.0-20190418232430-6867ff32788a/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8= @@ -50,7 +51,9 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA= github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= @@ -103,6 +106,7 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= @@ -172,6 +176,7 @@ github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f/go.mod h1:2zXcozF github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= @@ -221,17 +226,18 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/prometheus v0.0.0-20170814170113-3101606756c5/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s= github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= @@ -359,8 +365,7 @@ golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/keystore.go b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/keystore.go new file mode 100644 index 0000000000..43ac4d3b0c --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/bridge/nimbus/keystore.go @@ -0,0 +1,263 @@ +// +build nimbus + +package nimbusbridge + +// https://golang.org/cmd/cgo/ + +/* +#include +#include +#include +#include +*/ +import "C" +import ( + "crypto/ecdsa" + "encoding/binary" + "errors" + "fmt" + "math/big" + "unsafe" + + "github.com/btcsuite/btcd/btcec" + "github.com/pborman/uuid" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/extkeys" +) + +var ( + ErrInvalidSeed = errors.New("seed is invalid") + ErrInvalidKeyLen = errors.New("Nimbus serialized extended key length is invalid") +) + +type nimbusKeyStoreAdapter struct { +} + +// WrapKeyStore creates a types.KeyStore wrapper over the singleton Nimbus node +func WrapKeyStore() types.KeyStore { + return &nimbusKeyStoreAdapter{} +} + +func (k *nimbusKeyStoreAdapter) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (types.Account, error) { + fmt.Println("ImportECDSA") + panic("ImportECDSA") + + var privateKeyC unsafe.Pointer + if priv != nil { + privateKeyC = C.CBytes(crypto.FromECDSA(priv)) + defer C.free(privateKeyC) + } + passphraseC := C.CString(passphrase) + defer C.free(unsafe.Pointer(passphraseC)) + + var nimbusAccount C.account + if !C.nimbus_keystore_import_ecdsa((*C.uchar)(privateKeyC), passphraseC, &nimbusAccount) { + return types.Account{}, errors.New("failed to import ECDSA private key") + } + return accountFrom(&nimbusAccount), nil +} + +func (k *nimbusKeyStoreAdapter) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (types.Account, error) { + fmt.Println("ImportSingleExtendedKey") + panic("ImportSingleExtendedKey") + + extKeyJSONC := C.CString(extKey.String()) + defer C.free(unsafe.Pointer(extKeyJSONC)) + passphraseC := C.CString(passphrase) + defer C.free(unsafe.Pointer(passphraseC)) + + var nimbusAccount C.account + if !C.nimbus_keystore_import_single_extendedkey(extKeyJSONC, passphraseC, &nimbusAccount) { + return types.Account{}, errors.New("failed to import extended key") + } + return accountFrom(&nimbusAccount), nil +} + +func (k *nimbusKeyStoreAdapter) ImportExtendedKeyForPurpose(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, passphrase string) (types.Account, error) { + fmt.Println("ImportExtendedKeyForPurpose") + + passphraseC := C.CString(passphrase) + defer C.free(unsafe.Pointer(passphraseC)) + extKeyJSONC := C.CString(extKey.String()) + defer C.free(unsafe.Pointer(extKeyJSONC)) + + var nimbusAccount C.account + if !C.nimbus_keystore_import_extendedkeyforpurpose(C.int(keyPurpose), extKeyJSONC, passphraseC, &nimbusAccount) { + return types.Account{}, errors.New("failed to import extended key") + } + return accountFrom(&nimbusAccount), nil +} + +func (k *nimbusKeyStoreAdapter) AccountDecryptedKey(a types.Account, auth string) (types.Account, *types.Key, error) { + fmt.Println("AccountDecryptedKey") + panic("AccountDecryptedKey") + + authC := C.CString(auth) + defer C.free(unsafe.Pointer(authC)) + + var nimbusAccount C.account + err := nimbusAccountFrom(a, &nimbusAccount) + if err != nil { + return types.Account{}, nil, err + } + + var nimbusKey C.key + if !C.nimbus_keystore_account_decrypted_key(authC, &nimbusAccount, &nimbusKey) { + return types.Account{}, nil, errors.New("failed to decrypt account key") + } + key, err := keyFrom(&nimbusKey) + if err != nil { + return types.Account{}, nil, err + } + return accountFrom(&nimbusAccount), key, nil +} + +func (k *nimbusKeyStoreAdapter) Delete(a types.Account, auth string) error { + fmt.Println("Delete") + + var nimbusAccount C.account + err := nimbusAccountFrom(a, &nimbusAccount) + if err != nil { + return err + } + + authC := C.CString(auth) + defer C.free(unsafe.Pointer(authC)) + + if !C.nimbus_keystore_delete(&nimbusAccount, authC) { + return errors.New("failed to delete account") + } + + return nil +} + +func nimbusAccountFrom(account types.Account, nimbusAccount *C.account) error { + fmt.Println("nimbusAccountFrom") + err := copyAddressToCBuffer(&nimbusAccount.address[0], account.Address.Bytes()) + if err != nil { + return err + } + if account.URL == "" { + nimbusAccount.url[0] = C.char(0) + } else if len(account.URL) >= C.URL_LEN { + return errors.New("URL is too long to fit in Nimbus struct") + } else { + copyURLToCBuffer(&nimbusAccount.url[0], account.URL) + } + return err +} + +func accountFrom(nimbusAccount *C.account) types.Account { + return types.Account{ + Address: types.BytesToAddress(C.GoBytes(unsafe.Pointer(&nimbusAccount.address[0]), C.ADDRESS_LEN)), + URL: C.GoString(&nimbusAccount.url[0]), + } +} + +// copyAddressToCBuffer copies a Go buffer to a C buffer without allocating new memory +func copyAddressToCBuffer(dst *C.uchar, src []byte) error { + if len(src) != C.ADDRESS_LEN { + return errors.New("invalid buffer size") + } + + p := (*[C.ADDRESS_LEN]C.uchar)(unsafe.Pointer(dst)) + for index, b := range src { + p[index] = C.uchar(b) + } + + return nil +} + +// copyURLToCBuffer copies a Go buffer to a C buffer without allocating new memory +func copyURLToCBuffer(dst *C.char, src string) error { + if len(src)+1 > C.URL_LEN { + return errors.New("URL is too long to fit in Nimbus struct") + } + + p := (*[C.URL_LEN]C.uchar)(unsafe.Pointer(dst)) + for index := 0; index <= len(src); index++ { + p[index] = C.uchar(src[index]) + } + + return nil +} + +func keyFrom(k *C.key) (*types.Key, error) { + fmt.Println("keyFrom") + if k == nil { + return nil, nil + } + + var err error + key := types.Key{ + Id: uuid.Parse(C.GoString(&k.id[0])), + } + key.Address = types.BytesToAddress(C.GoBytes(unsafe.Pointer(&k.address[0]), C.ADDRESS_LEN)) + key.PrivateKey, err = crypto.ToECDSA(C.GoBytes(unsafe.Pointer(&k.privateKeyID[0]), C.PRIVKEY_LEN)) + if err != nil { + return nil, err + } + key.ExtendedKey, err = newExtKeyFromBuffer(C.GoBytes(unsafe.Pointer(&k.extKey[0]), C.EXTKEY_LEN)) + if err != nil { + return nil, err + } + return &key, err +} + +// newExtKeyFromBuffer returns a new extended key instance from a serialized +// extended key. +func newExtKeyFromBuffer(key []byte) (*extkeys.ExtendedKey, error) { + if len(key) == 0 { + return &extkeys.ExtendedKey{}, nil + } + + if len(key) != C.EXTKEY_LEN { + return nil, ErrInvalidKeyLen + } + + // The serialized format is: + // version (4) || depth (1) || parent fingerprint (4)) || + // child num (4) || chain code (32) || key data (33) + + payload := key + + // Deserialize each of the payload fields. + version := payload[:4] + depth := payload[4:5][0] + fingerPrint := payload[5:9] + childNumber := binary.BigEndian.Uint32(payload[9:13]) + chainCode := payload[13:45] + keyData := payload[45:78] + + // The key data is a private key if it starts with 0x00. Serialized + // compressed pubkeys either start with 0x02 or 0x03. + isPrivate := keyData[0] == 0x00 + if isPrivate { + // Ensure the private key is valid. It must be within the range + // of the order of the secp256k1 curve and not be 0. + keyData = keyData[1:] + keyNum := new(big.Int).SetBytes(keyData) + if keyNum.Cmp(btcec.S256().N) >= 0 || keyNum.Sign() == 0 { + return nil, ErrInvalidSeed + } + } else { + // Ensure the public key parses correctly and is actually on the + // secp256k1 curve. + _, err := btcec.ParsePubKey(keyData, btcec.S256()) + if err != nil { + return nil, err + } + } + + return &extkeys.ExtendedKey{ + Version: version, + KeyData: keyData, + ChainCode: chainCode, + FingerPrint: fingerPrint, + Depth: depth, + ChildNumber: childNumber, + IsPrivate: isPrivate, + }, nil +} From 93975aa90041b91472de28017d08fa1b4ec5ecac Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro Date: Thu, 9 Jan 2020 19:11:56 +0100 Subject: [PATCH 5/9] Working Nimbus branch with geth keystore --- account/accounts_geth.go | 2 - account/accounts_nimbus.go | 16 ++++---- account/keystore_nimbus.go | 78 +++++++++++++++++++------------------- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/account/accounts_geth.go b/account/accounts_geth.go index e6cf27d689..0bd25569ac 100644 --- a/account/accounts_geth.go +++ b/account/accounts_geth.go @@ -1,5 +1,3 @@ -// +build !nimbus - package account import ( diff --git a/account/accounts_nimbus.go b/account/accounts_nimbus.go index 7f8bcaf78b..3ebe0f9ecb 100644 --- a/account/accounts_nimbus.go +++ b/account/accounts_nimbus.go @@ -18,12 +18,12 @@ func NewNimbusManager() *NimbusManager { return m } -// InitKeystore sets key manager and key store. -func (m *Manager) InitKeystore(keydir string) error { - m.mu.Lock() - defer m.mu.Unlock() +// // InitKeystore sets key manager and key store. +// func (m *Manager) InitKeystore(keydir string) error { +// m.mu.Lock() +// defer m.mu.Unlock() - var err error - m.keystore, err = makeKeyStore(keydir) - return err -} +// var err error +// m.keystore, err = makeKeyStore(keydir) +// return err +// } diff --git a/account/keystore_nimbus.go b/account/keystore_nimbus.go index ecf8896b54..c343f2c237 100644 --- a/account/keystore_nimbus.go +++ b/account/keystore_nimbus.go @@ -3,49 +3,47 @@ package account import ( - // "io/ioutil" - // "os" + "io/ioutil" + "os" - // gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" - nimbusbridge "github.com/status-im/status-go/eth-node/bridge/nimbus" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" + + gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + // nimbusbridge "github.com/status-im/status-go/eth-node/bridge/nimbus" "github.com/status-im/status-go/eth-node/types" ) -// // makeAccountManager creates ethereum accounts.Manager with single disk backend and lightweight kdf. -// // If keydir is empty new temporary directory with go-ethereum-keystore will be intialized. -// func makeAccountManager(keydir string) (manager *accounts.Manager, err error) { -// if keydir == "" { -// // There is no datadir. -// keydir, err = ioutil.TempDir("", "nimbus-keystore") -// } -// if err != nil { -// return nil, err -// } -// if err := os.MkdirAll(keydir, 0700); err != nil { -// return nil, err -// } -// config := accounts.Config{InsecureUnlockAllowed: false} -// return accounts.NewManager(&config, keystore.NewKeyStore(keydir, keystore.LightScryptN, keystore.LightScryptP)), nil -// } - -// func makeKeyStore(keydir string) (types.KeyStore, error) { -// manager, err := makeAccountManager(keydir) -// if err != nil { -// return err -// } - -// backends := manager.Backends(keystore.KeyStoreType) -// if len(backends) == 0 { -// return nil, ErrAccountKeyStoreMissing -// } -// keyStore, ok := backends[0].(*keystore.KeyStore) -// if !ok { -// return nil, ErrAccountKeyStoreMissing -// } - -// return gethbridge.WrapKeyStore(keyStore), nil -// } +// makeAccountManager creates ethereum accounts.Manager with single disk backend and lightweight kdf. +// If keydir is empty new temporary directory with go-ethereum-keystore will be intialized. +func makeAccountManager(keydir string) (manager *accounts.Manager, err error) { + if keydir == "" { + // There is no datadir. + keydir, err = ioutil.TempDir("", "nimbus-keystore") + } + if err != nil { + return nil, err + } + if err := os.MkdirAll(keydir, 0700); err != nil { + return nil, err + } + config := accounts.Config{InsecureUnlockAllowed: false} + return accounts.NewManager(&config, keystore.NewKeyStore(keydir, keystore.LightScryptN, keystore.LightScryptP)), nil +} -func makeKeyStore(_ string) (types.KeyStore, error) { - return nimbusbridge.WrapKeyStore(), nil +func makeKeyStore(manager *accounts.Manager) (types.KeyStore, error) { + backends := manager.Backends(keystore.KeyStoreType) + if len(backends) == 0 { + return nil, ErrAccountKeyStoreMissing + } + keyStore, ok := backends[0].(*keystore.KeyStore) + if !ok { + return nil, ErrAccountKeyStoreMissing + } + + return gethbridge.WrapKeyStore(keyStore), nil } + +// func makeKeyStore(_ string) (types.KeyStore, error) { +// return nimbusbridge.WrapKeyStore(), nil +// } From 456bcfa022fedb476fadb82bedaa6c07b0c5f869 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Mon, 20 Jan 2020 17:44:32 +0100 Subject: [PATCH 6/9] Peg clock value to whisper timestamp (#1804) This commit pegs the clock value to maximum + 120 seconds from the whisper timestamp. In this way the we avoid the scenario where a client makes the timestamp increase arbitrarely. --- VERSION | 2 +- eth-node/go.mod | 2 +- eth-node/go.sum | 11 ++ protocol/chat.go | 28 ++-- protocol/contact.go | 6 +- protocol/message_builder.go | 4 +- protocol/message_handler.go | 45 +++--- protocol/message_validator.go | 74 +++++++--- protocol/message_validator_test.go | 133 ++++++++++++------ protocol/messenger.go | 84 +++++------ protocol/messenger_installations_test.go | 4 +- protocol/messenger_test.go | 69 ++++----- protocol/transport/transport.go | 1 + protocol/transport/waku/waku_service.go | 6 + protocol/transport/whisper/whisper_service.go | 6 + .../status-im/status-go/protocol/chat.go | 28 ++-- .../status-im/status-go/protocol/contact.go | 6 +- .../status-go/protocol/message_builder.go | 4 +- .../status-go/protocol/message_handler.go | 45 +++--- .../status-go/protocol/message_validator.go | 74 +++++++--- .../status-im/status-go/protocol/messenger.go | 84 +++++------ .../status-go/protocol/transport/transport.go | 1 + .../protocol/transport/waku/waku_service.go | 6 + .../transport/whisper/whisper_service.go | 6 + 24 files changed, 446 insertions(+), 283 deletions(-) diff --git a/VERSION b/VERSION index e5d9eb181f..0c1df6ae2c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.39.6 +0.39.7 diff --git a/eth-node/go.mod b/eth-node/go.mod index 6531f17c1e..770aa0f4fe 100644 --- a/eth-node/go.mod +++ b/eth-node/go.mod @@ -18,7 +18,7 @@ require ( github.com/status-im/doubleratchet v3.0.0+incompatible github.com/status-im/status-go/extkeys v1.0.0 github.com/status-im/status-go/waku v1.0.0 - github.com/status-im/status-go/whisper/v6 v6.0.1 // indirect + github.com/status-im/status-go/whisper/v6 v6.0.1 github.com/stretchr/testify v1.4.0 github.com/wealdtech/go-ens/v3 v3.0.9 go.uber.org/zap v1.13.0 diff --git a/eth-node/go.sum b/eth-node/go.sum index 24c52124a2..c5823f8b74 100644 --- a/eth-node/go.sum +++ b/eth-node/go.sum @@ -27,6 +27,7 @@ github.com/aristanetworks/goarista v0.0.0-20181002214814-33151c4543a7 h1:6TQIK3K github.com/aristanetworks/goarista v0.0.0-20181002214814-33151c4543a7/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= github.com/aristanetworks/goarista v0.0.0-20190219163901-728bce664cf5 h1:L0TwgZQo7Mga9im6FvKEZGIvyLE/VG/HI5loz5LpvC0= github.com/aristanetworks/goarista v0.0.0-20190219163901-728bce664cf5/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= +github.com/aristanetworks/goarista v0.0.0-20191106175434-873d404c7f40 h1:ZdRuixFqR3mfx4FHzclG3COrRgWrYq0VhNgIoYoObcM= github.com/aristanetworks/goarista v0.0.0-20191106175434-873d404c7f40/go.mod h1:Z4RTxGAuYhPzcq8+EdRM+R8M48Ssle2TsWtwRKa+vns= github.com/aristanetworks/splunk-hec-go v0.3.3/go.mod h1:1VHO9r17b0K7WmOlLb9nTk/2YanvOEnLMUgsFrxBROc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -77,6 +78,7 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB github.com/elastic/gosigar v0.0.0-20180330100440-37f05ff46ffa/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs= github.com/elastic/gosigar v0.10.4 h1:6jfw75dsoflhBMRdO6QPzQUgLqUYTsQQQRkkcsHsuPo= github.com/elastic/gosigar v0.10.4/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs= +github.com/elastic/gosigar v0.10.5 h1:GzPQ+78RaAb4J63unidA/JavQRKrB6s8IOzN6Ib59jo= github.com/elastic/gosigar v0.10.5/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs= github.com/ethereum/go-ethereum v1.9.2/go.mod h1:PwpWDrCLZrV+tfrhqqF6kPknbISMHaJv9Ln3kPCZLwY= github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -139,6 +141,7 @@ github.com/influxdata/influxdb1-client v0.0.0-20190809212627-fc22c7df067e/go.mod github.com/jackpal/go-nat-pmp v0.0.0-20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jackpal/go-nat-pmp v1.0.1 h1:i0LektDkO1QlrTm/cSuP+PyBCDnYvjPLGl4LdWEMiaA= github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -226,6 +229,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -233,10 +237,12 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/prometheus v0.0.0-20170814170113-3101606756c5/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s= @@ -251,6 +257,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -345,6 +352,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2 h1:4dVFTC832rPn4pomLSz1vA+are2+dU19w1H8OngV7nc= golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= @@ -353,6 +361,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= @@ -365,6 +374,8 @@ golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= diff --git a/protocol/chat.go b/protocol/chat.go index 08988daca5..4c2c687a89 100644 --- a/protocol/chat.go +++ b/protocol/chat.go @@ -207,39 +207,39 @@ func oneToOneChatID(publicKey *ecdsa.PublicKey) string { return types.EncodeHex(crypto.FromECDSAPub(publicKey)) } -func OneToOneFromPublicKey(pk *ecdsa.PublicKey) *Chat { +func OneToOneFromPublicKey(pk *ecdsa.PublicKey, timesource ClockValueTimesource) *Chat { chatID := types.EncodeHex(crypto.FromECDSAPub(pk)) - newChat := CreateOneToOneChat(chatID[:8], pk) + newChat := CreateOneToOneChat(chatID[:8], pk, timesource) return &newChat } -func CreateOneToOneChat(name string, publicKey *ecdsa.PublicKey) Chat { +func CreateOneToOneChat(name string, publicKey *ecdsa.PublicKey, timesource ClockValueTimesource) Chat { return Chat{ ID: oneToOneChatID(publicKey), Name: name, - Timestamp: int64(timestampInMs()), + Timestamp: int64(timesource.GetCurrentTime()), Active: true, ChatType: ChatTypeOneToOne, } } -func CreatePublicChat(name string) Chat { +func CreatePublicChat(name string, timesource ClockValueTimesource) Chat { return Chat{ ID: name, Name: name, Active: true, - Timestamp: int64(timestampInMs()), + Timestamp: int64(timesource.GetCurrentTime()), Color: chatColors[rand.Intn(len(chatColors))], ChatType: ChatTypePublic, } } -func createGroupChat() Chat { +func createGroupChat(timesource ClockValueTimesource) Chat { return Chat{ Active: true, Color: chatColors[rand.Intn(len(chatColors))], - Timestamp: int64(timestampInMs()), + Timestamp: int64(timesource.GetCurrentTime()), ChatType: ChatTypePrivateGroupChat, } } @@ -276,11 +276,15 @@ func stringSliceToPublicKeys(slice []string, prefixed bool) ([]*ecdsa.PublicKey, return result, nil } +type ClockValueTimesource interface { + GetCurrentTime() uint64 +} + // NextClockAndTimestamp returns the next clock value // and the current timestamp -func (c *Chat) NextClockAndTimestamp() (uint64, uint64) { +func (c *Chat) NextClockAndTimestamp(timesource ClockValueTimesource) (uint64, uint64) { clock := c.LastClockValue - timestamp := timestampInMs() + timestamp := timesource.GetCurrentTime() if clock == 0 || clock < timestamp { clock = timestamp } else { @@ -289,8 +293,8 @@ func (c *Chat) NextClockAndTimestamp() (uint64, uint64) { return clock, timestamp } -func (c *Chat) UpdateFromMessage(message *Message) error { - c.Timestamp = int64(timestampInMs()) +func (c *Chat) UpdateFromMessage(message *Message, timesource ClockValueTimesource) error { + c.Timestamp = int64(timesource.GetCurrentTime()) if c.LastClockValue <= message.Clock { jsonMessage, err := json.Marshal(message) if err != nil { diff --git a/protocol/contact.go b/protocol/contact.go index fdbe8158ed..a12bc12c8b 100644 --- a/protocol/contact.go +++ b/protocol/contact.go @@ -3,7 +3,6 @@ package protocol import ( "crypto/ecdsa" "encoding/hex" - "strings" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" @@ -33,7 +32,7 @@ type Contact struct { // ID of the contact. It's a hex-encoded public key (prefixed with 0x). ID string `json:"id"` // Ethereum address of the contact - Address string `json:"address"` + Address string `json:"address,omitempty"` // ENS name of contact Name string `json:"name,omitempty"` // EnsVerified whether we verified the name of the contact @@ -88,8 +87,6 @@ func existsInStringSlice(set []string, find string) bool { } func buildContact(publicKey *ecdsa.PublicKey) (*Contact, error) { - address := strings.ToLower(crypto.PubkeyToAddress(*publicKey).Hex()) - id := "0x" + hex.EncodeToString(crypto.FromECDSAPub(publicKey)) identicon, err := identicon.GenerateBase64(id) @@ -99,7 +96,6 @@ func buildContact(publicKey *ecdsa.PublicKey) (*Contact, error) { contact := &Contact{ ID: id, - Address: address[2:], Alias: alias.GenerateFromPublicKey(publicKey), Identicon: identicon, } diff --git a/protocol/message_builder.go b/protocol/message_builder.go index 2c4f48882c..da2e11060d 100644 --- a/protocol/message_builder.go +++ b/protocol/message_builder.go @@ -9,8 +9,8 @@ import ( "github.com/status-im/status-go/protocol/identity/identicon" ) -func extendMessageFromChat(message *Message, chat *Chat, key *ecdsa.PublicKey) error { - clock, timestamp := chat.NextClockAndTimestamp() +func extendMessageFromChat(message *Message, chat *Chat, key *ecdsa.PublicKey, timesource ClockValueTimesource) error { + clock, timestamp := chat.NextClockAndTimestamp(timesource) message.LocalChatID = chat.ID message.Clock = clock diff --git a/protocol/message_handler.go b/protocol/message_handler.go index 5e0e286931..7cda081db5 100644 --- a/protocol/message_handler.go +++ b/protocol/message_handler.go @@ -33,12 +33,19 @@ func (m *MessageHandler) HandleMembershipUpdate(messageState *ReceivedMessageSta var group *v1protocol.Group var err error + logger := m.logger.With(zap.String("site", "HandleMembershipUpdate")) + message, err := v1protocol.MembershipUpdateMessageFromProtobuf(&rawMembershipUpdate) if err != nil { return err } + if err := ValidateMembershipUpdateMessage(message, messageState.Timesource.GetCurrentTime()); err != nil { + logger.Warn("failed to validate message", zap.Error(err)) + return err + } + if chat == nil { if len(message.Events) == 0 { return errors.New("can't create new group chat without events") @@ -52,7 +59,7 @@ func (m *MessageHandler) HandleMembershipUpdate(messageState *ReceivedMessageSta if !group.IsMember(contactIDFromPublicKey(&m.identity.PublicKey)) { return errors.New("can't create a new group chat without us being a member") } - newChat := createGroupChat() + newChat := createGroupChat(messageState.Timesource) chat = &newChat } else { @@ -108,7 +115,7 @@ func (m *MessageHandler) handleCommandMessage(state *ReceivedMessageState, messa message.WhisperTimestamp = state.CurrentMessageState.WhisperTimestamp message.PrepareContent() - chat, err := m.matchMessage(message, state.AllChats) + chat, err := m.matchMessage(message, state.AllChats, state.Timesource) if err != nil { return err } @@ -137,7 +144,7 @@ func (m *MessageHandler) handleCommandMessage(state *ReceivedMessageState, messa message.OutgoingStatus = OutgoingStatusSent } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, state.Timesource) if err != nil { return err } @@ -158,7 +165,7 @@ func (m *MessageHandler) handleCommandMessage(state *ReceivedMessageState, messa func (m *MessageHandler) HandleSyncInstallationContact(state *ReceivedMessageState, message protobuf.SyncInstallationContact) error { chat, ok := state.AllChats[state.CurrentMessageState.Contact.ID] if !ok { - chat = OneToOneFromPublicKey(state.CurrentMessageState.PublicKey) + chat = OneToOneFromPublicKey(state.CurrentMessageState.PublicKey, state.Timesource) // We don't want to show the chat to the user chat.Active = false } @@ -205,7 +212,7 @@ func (m *MessageHandler) HandleSyncInstallationPublicChat(state *ReceivedMessage return nil } - chat := CreatePublicChat(chatID) + chat := CreatePublicChat(chatID, state.Timesource) state.AllChats[chat.ID] = &chat state.ModifiedChats[chat.ID] = true @@ -218,7 +225,7 @@ func (m *MessageHandler) HandleContactUpdate(state *ReceivedMessageState, messag contact := state.CurrentMessageState.Contact chat, ok := state.AllChats[contact.ID] if !ok { - chat = OneToOneFromPublicKey(state.CurrentMessageState.PublicKey) + chat = OneToOneFromPublicKey(state.CurrentMessageState.PublicKey, state.Timesource) // We don't want to show the chat to the user chat.Active = false } @@ -252,7 +259,7 @@ func (m *MessageHandler) HandleContactUpdate(state *ReceivedMessageState, messag func (m *MessageHandler) HandlePairInstallation(state *ReceivedMessageState, message protobuf.PairInstallation) error { logger := m.logger.With(zap.String("site", "HandlePairInstallation")) - if err := ValidateReceivedPairInstallation(&message); err != nil { + if err := ValidateReceivedPairInstallation(&message, state.CurrentMessageState.WhisperTimestamp); err != nil { logger.Warn("failed to validate message", zap.Error(err)) return err } @@ -276,7 +283,7 @@ func (m *MessageHandler) HandlePairInstallation(state *ReceivedMessageState, mes func (m *MessageHandler) HandleChatMessage(state *ReceivedMessageState) error { logger := m.logger.With(zap.String("site", "handleChatMessage")) - if err := ValidateReceivedChatMessage(&state.CurrentMessageState.Message); err != nil { + if err := ValidateReceivedChatMessage(&state.CurrentMessageState.Message, state.CurrentMessageState.WhisperTimestamp); err != nil { logger.Warn("failed to validate message", zap.Error(err)) return err } @@ -291,7 +298,7 @@ func (m *MessageHandler) HandleChatMessage(state *ReceivedMessageState) error { } receivedMessage.PrepareContent() - chat, err := m.matchMessage(receivedMessage, state.AllChats) + chat, err := m.matchMessage(receivedMessage, state.AllChats, state.Timesource) if err != nil { return err } @@ -319,7 +326,7 @@ func (m *MessageHandler) HandleChatMessage(state *ReceivedMessageState) error { receivedMessage.OutgoingStatus = OutgoingStatusSent } - err = chat.UpdateFromMessage(receivedMessage) + err = chat.UpdateFromMessage(receivedMessage, state.Timesource) if err != nil { return err } @@ -339,7 +346,7 @@ func (m *MessageHandler) HandleChatMessage(state *ReceivedMessageState) error { } func (m *MessageHandler) HandleRequestAddressForTransaction(messageState *ReceivedMessageState, command protobuf.RequestAddressForTransaction) error { - err := ValidateReceivedRequestAddressForTransaction(&command) + err := ValidateReceivedRequestAddressForTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -363,7 +370,7 @@ func (m *MessageHandler) HandleRequestAddressForTransaction(messageState *Receiv } func (m *MessageHandler) HandleRequestTransaction(messageState *ReceivedMessageState, command protobuf.RequestTransaction) error { - err := ValidateReceivedRequestTransaction(&command) + err := ValidateReceivedRequestTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -388,7 +395,7 @@ func (m *MessageHandler) HandleRequestTransaction(messageState *ReceivedMessageS } func (m *MessageHandler) HandleAcceptRequestAddressForTransaction(messageState *ReceivedMessageState, command protobuf.AcceptRequestAddressForTransaction) error { - err := ValidateReceivedAcceptRequestAddressForTransaction(&command) + err := ValidateReceivedAcceptRequestAddressForTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -437,7 +444,7 @@ func (m *MessageHandler) HandleAcceptRequestAddressForTransaction(messageState * } func (m *MessageHandler) HandleSendTransaction(messageState *ReceivedMessageState, command protobuf.SendTransaction) error { - err := ValidateReceivedSendTransaction(&command) + err := ValidateReceivedSendTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -457,7 +464,7 @@ func (m *MessageHandler) HandleSendTransaction(messageState *ReceivedMessageStat } func (m *MessageHandler) HandleDeclineRequestAddressForTransaction(messageState *ReceivedMessageState, command protobuf.DeclineRequestAddressForTransaction) error { - err := ValidateReceivedDeclineRequestAddressForTransaction(&command) + err := ValidateReceivedDeclineRequestAddressForTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -497,7 +504,7 @@ func (m *MessageHandler) HandleDeclineRequestAddressForTransaction(messageState } func (m *MessageHandler) HandleDeclineRequestTransaction(messageState *ReceivedMessageState, command protobuf.DeclineRequestTransaction) error { - err := ValidateReceivedDeclineRequestTransaction(&command) + err := ValidateReceivedDeclineRequestTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -536,7 +543,7 @@ func (m *MessageHandler) HandleDeclineRequestTransaction(messageState *ReceivedM return m.handleCommandMessage(messageState, oldMessage) } -func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat) (*Chat, error) { +func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat, timesource ClockValueTimesource) (*Chat, error) { if message.SigPubKey == nil { m.logger.Error("public key can't be empty") return nil, errors.New("received a message with empty public key") @@ -571,7 +578,7 @@ func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat) return nil, errors.Wrap(err, "failed to decode pubkey") } - newChat := CreateOneToOneChat(chatID[:8], pubKey) + newChat := CreateOneToOneChat(chatID[:8], pubKey, timesource) chat = &newChat } return chat, nil @@ -582,7 +589,7 @@ func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat) chat := chats[chatID] if chat == nil { // TODO: this should be a three-word name used in the mobile client - newChat := CreateOneToOneChat(chatID[:8], message.SigPubKey) + newChat := CreateOneToOneChat(chatID[:8], message.SigPubKey, timesource) chat = &newChat } return chat, nil diff --git a/protocol/message_validator.go b/protocol/message_validator.go index 682cc60470..cd2fe7ee0e 100644 --- a/protocol/message_validator.go +++ b/protocol/message_validator.go @@ -6,13 +6,41 @@ import ( "strings" "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/v1" ) -func ValidateReceivedPairInstallation(message *protobuf.PairInstallation) error { - if message.Clock == 0 { +// maxWhisperDrift is how many milliseconds we allow the clock value to differ +// from whisperTimestamp +const maxWhisperFutureDriftMs uint64 = 120000 + +func validateClockValue(clock uint64, whisperTimestamp uint64) error { + if clock == 0 { return errors.New("clock can't be 0") } + if clock > whisperTimestamp && clock-whisperTimestamp > maxWhisperFutureDriftMs { + return errors.New("clock value too high") + } + + return nil +} + +func ValidateMembershipUpdateMessage(message *protocol.MembershipUpdateMessage, timeNowMs uint64) error { + + for _, e := range message.Events { + if err := validateClockValue(e.ClockValue, timeNowMs); err != nil { + return err + } + + } + return nil +} + +func ValidateReceivedPairInstallation(message *protobuf.PairInstallation, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err + } + if len(strings.TrimSpace(message.Name)) == 0 { return errors.New("name can't be empty") } @@ -28,9 +56,9 @@ func ValidateReceivedPairInstallation(message *protobuf.PairInstallation) error return nil } -func ValidateReceivedSendTransaction(message *protobuf.SendTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedSendTransaction(message *protobuf.SendTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(strings.TrimSpace(message.TransactionHash)) == 0 { @@ -44,9 +72,9 @@ func ValidateReceivedSendTransaction(message *protobuf.SendTransaction) error { return nil } -func ValidateReceivedRequestAddressForTransaction(message *protobuf.RequestAddressForTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedRequestAddressForTransaction(message *protobuf.RequestAddressForTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(strings.TrimSpace(message.Value)) == 0 { @@ -61,9 +89,9 @@ func ValidateReceivedRequestAddressForTransaction(message *protobuf.RequestAddre return nil } -func ValidateReceivedRequestTransaction(message *protobuf.RequestTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedRequestTransaction(message *protobuf.RequestTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(strings.TrimSpace(message.Value)) == 0 { @@ -82,9 +110,9 @@ func ValidateReceivedRequestTransaction(message *protobuf.RequestTransaction) er return nil } -func ValidateReceivedAcceptRequestAddressForTransaction(message *protobuf.AcceptRequestAddressForTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedAcceptRequestAddressForTransaction(message *protobuf.AcceptRequestAddressForTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(message.Id) == 0 { @@ -98,9 +126,9 @@ func ValidateReceivedAcceptRequestAddressForTransaction(message *protobuf.Accept return nil } -func ValidateReceivedDeclineRequestAddressForTransaction(message *protobuf.DeclineRequestAddressForTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedDeclineRequestAddressForTransaction(message *protobuf.DeclineRequestAddressForTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(message.Id) == 0 { @@ -110,9 +138,9 @@ func ValidateReceivedDeclineRequestAddressForTransaction(message *protobuf.Decli return nil } -func ValidateReceivedDeclineRequestTransaction(message *protobuf.DeclineRequestTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedDeclineRequestTransaction(message *protobuf.DeclineRequestTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(message.Id) == 0 { @@ -122,9 +150,9 @@ func ValidateReceivedDeclineRequestTransaction(message *protobuf.DeclineRequestT return nil } -func ValidateReceivedChatMessage(message *protobuf.ChatMessage) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedChatMessage(message *protobuf.ChatMessage, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if message.Timestamp == 0 { diff --git a/protocol/message_validator_test.go b/protocol/message_validator_test.go index e3688ee7ed..a0843dafb2 100644 --- a/protocol/message_validator_test.go +++ b/protocol/message_validator_test.go @@ -18,13 +18,15 @@ func TestMessageValidatorSuite(t *testing.T) { func (s *MessageValidatorSuite) TestValidateRequestAddressForTransaction() { testCases := []struct { - Name string - Valid bool - Message protobuf.RequestAddressForTransaction + Name string + WhisperTimestamp uint64 + Valid bool + Message protobuf.RequestAddressForTransaction }{ { - Name: "valid message", - Valid: true, + Name: "valid message", + WhisperTimestamp: 30, + Valid: true, Message: protobuf.RequestAddressForTransaction{ Clock: 30, Value: "0.34", @@ -32,34 +34,47 @@ func (s *MessageValidatorSuite) TestValidateRequestAddressForTransaction() { }, }, { - Name: "missing clock value", - Valid: false, + Name: "missing clock value", + WhisperTimestamp: 30, + Valid: false, Message: protobuf.RequestAddressForTransaction{ Value: "0.34", Contract: "some contract", }, }, { - Name: "missing value", - Valid: false, + Name: "missing value", + WhisperTimestamp: 30, + Valid: false, Message: protobuf.RequestAddressForTransaction{ Clock: 30, Contract: "some contract", }, }, { - Name: "non number value", - Valid: false, + Name: "non number value", + WhisperTimestamp: 30, + Valid: false, Message: protobuf.RequestAddressForTransaction{ Clock: 30, Value: "most definitely not a number", Contract: "some contract", }, }, + { + Name: "Clock value too high", + WhisperTimestamp: 30, + Valid: false, + Message: protobuf.RequestAddressForTransaction{ + Clock: 151000, + Value: "0.34", + Contract: "some contract", + }, + }, } for _, tc := range testCases { s.Run(tc.Name, func() { - err := ValidateReceivedRequestAddressForTransaction(&tc.Message) + err := ValidateReceivedRequestAddressForTransaction(&tc.Message, tc.WhisperTimestamp) if tc.Valid { s.Nil(err) } else { @@ -72,13 +87,15 @@ func (s *MessageValidatorSuite) TestValidateRequestAddressForTransaction() { func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { testCases := []struct { - Name string - Valid bool - Message protobuf.ChatMessage + Name string + WhisperTimestamp uint64 + Valid bool + Message protobuf.ChatMessage }{ { - Name: "A valid message", - Valid: true, + Name: "A valid message", + WhisperTimestamp: 2, + Valid: true, Message: protobuf.ChatMessage{ ChatId: "a", Clock: 1, @@ -91,8 +108,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Missing chatId", - Valid: false, + Name: "Missing chatId", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ Clock: 1, Timestamp: 2, @@ -104,8 +122,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Missing clock", - Valid: false, + Name: "Missing clock", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Timestamp: 2, @@ -117,8 +136,24 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Missing timestamp", - Valid: false, + Name: "Clock value too high", + WhisperTimestamp: 2, + Valid: false, + Message: protobuf.ChatMessage{ + ChatId: "a", + Clock: 133000, + Timestamp: 1, + Text: "some-text", + ResponseTo: "", + EnsName: "", + MessageType: protobuf.ChatMessage_ONE_TO_ONE, + ContentType: protobuf.ChatMessage_TEXT_PLAIN, + }, + }, + { + Name: "Missing timestamp", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Clock: 2, @@ -130,8 +165,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Missing text", - Valid: false, + Name: "Missing text", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Clock: 2, @@ -143,8 +179,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Blank text", - Valid: false, + Name: "Blank text", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Text: " \n \t \n ", @@ -157,8 +194,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Unknown MessageType", - Valid: false, + Name: "Unknown MessageType", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Text: "valid", @@ -171,8 +209,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Unknown ContentType", - Valid: false, + Name: "Unknown ContentType", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Text: "valid", @@ -185,8 +224,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "System message MessageType", - Valid: false, + Name: "System message MessageType", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Text: "valid", @@ -199,8 +239,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Request address for transaction message type", - Valid: false, + Name: "Request address for transaction message type", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Text: "valid", @@ -213,8 +254,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Valid emoji only emssage", - Valid: true, + Name: "Valid emoji only emssage", + WhisperTimestamp: 2, + Valid: true, Message: protobuf.ChatMessage{ ChatId: "a", Text: ":+1:", @@ -243,8 +285,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { } ,*/ { - Name: "Valid sticker message", - Valid: true, + Name: "Valid sticker message", + WhisperTimestamp: 2, + Valid: true, Message: protobuf.ChatMessage{ ChatId: "a", Text: "valid", @@ -263,8 +306,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Invalid sticker message without Hash", - Valid: false, + Name: "Invalid sticker message without Hash", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Text: "valid", @@ -282,8 +326,9 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { }, }, { - Name: "Invalid sticker message without any content", - Valid: false, + Name: "Invalid sticker message without any content", + WhisperTimestamp: 2, + Valid: false, Message: protobuf.ChatMessage{ ChatId: "a", Text: "valid", @@ -299,7 +344,7 @@ func (s *MessageValidatorSuite) TestValidatePlainTextMessage() { for _, tc := range testCases { s.Run(tc.Name, func() { - err := ValidateReceivedChatMessage(&tc.Message) + err := ValidateReceivedChatMessage(&tc.Message, tc.WhisperTimestamp) if tc.Valid { s.Nil(err) } else { diff --git a/protocol/messenger.go b/protocol/messenger.go index 1a5d5871d1..4baec271eb 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -283,6 +283,7 @@ func NewMessenger( // Initialize transport layer. var transp transport.Transport + if shh, err := node.GetWhisper(nil); err == nil { transp, err = shhtransp.NewWhisperServiceTransport( shh, @@ -631,14 +632,14 @@ func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string, var response MessengerResponse logger := m.logger.With(zap.String("site", "CreateGroupChatWithMembers")) logger.Info("Creating group chat", zap.String("name", name), zap.Any("members", members)) - chat := createGroupChat() + chat := createGroupChat(m.getTimesource()) group, err := v1protocol.NewGroupWithCreator(name, m.identity) if err != nil { return nil, err } chat.updateChatFromProtocolGroup(group) - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) // Add members event := v1protocol.NewMembersAddedEvent(members, clock) event.ChatID = chat.ID @@ -710,7 +711,7 @@ func (m *Messenger) RemoveMemberFromGroupChat(ctx context.Context, chatID string return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) // Remove member event := v1protocol.NewMemberRemovedEvent(member, clock) event.ChatID = chat.ID @@ -767,7 +768,7 @@ func (m *Messenger) AddMembersToGroupChat(ctx context.Context, chatID string, me return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) // Add members event := v1protocol.NewMembersAddedEvent(members, clock) event.ChatID = chat.ID @@ -832,7 +833,7 @@ func (m *Messenger) AddAdminsToGroupChat(ctx context.Context, chatID string, mem return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) // Add members event := v1protocol.NewAdminsAddedEvent(members, clock) event.ChatID = chat.ID @@ -899,7 +900,7 @@ func (m *Messenger) ConfirmJoiningGroup(ctx context.Context, chatID string) (*Me if err != nil { return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) event := v1protocol.NewMemberJoinedEvent( clock, ) @@ -966,7 +967,7 @@ func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string) (*Messeng if err != nil { return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) event := v1protocol.NewMemberRemovedEvent( contactIDFromPublicKey(&m.identity.PublicKey), clock, @@ -1146,10 +1147,6 @@ func (m *Messenger) Contacts() []*Contact { return contacts } -func timestampInMs() uint64 { - return uint64(time.Now().UnixNano() / int64(time.Millisecond)) -} - // ReSendChatMessage pulls a message from the database and sends it again func (m *Messenger) ReSendChatMessage(ctx context.Context, messageID string) error { m.mutex.Lock() @@ -1307,7 +1304,7 @@ func (m *Messenger) SendChatMessage(ctx context.Context, message *Message) (*Mes return nil, errors.New("Chat not found") } - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.getTimesource()) if err != nil { return nil, err } @@ -1361,7 +1358,7 @@ func (m *Messenger) SendChatMessage(ctx context.Context, message *Message) (*Mes return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.getTimesource()) if err != nil { return nil, err } @@ -1432,13 +1429,13 @@ func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, ensName, prof if err != nil { return nil, err } - chat = OneToOneFromPublicKey(publicKey) + chat = OneToOneFromPublicKey(publicKey, m.getTimesource()) // We don't want to show the chat to the user chat.Active = false } m.allChats[chat.ID] = chat - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) contactUpdate := &protobuf.ContactUpdate{ Clock: clock, @@ -1525,13 +1522,13 @@ func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerRespons chat, ok := m.allChats[chatID] if !ok { - chat = OneToOneFromPublicKey(&m.identity.PublicKey) + chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource()) // We don't want to show the chat to the user chat.Active = false } m.allChats[chat.ID] = chat - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) pairMessage := &protobuf.PairInstallation{ Clock: clock, @@ -1573,13 +1570,13 @@ func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat) error chat, ok := m.allChats[chatID] if !ok { - chat = OneToOneFromPublicKey(&m.identity.PublicKey) + chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource()) // We don't want to show the chat to the user chat.Active = false } m.allChats[chat.ID] = chat - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) syncMessage := &protobuf.SyncInstallationPublicChat{ Clock: clock, @@ -1614,13 +1611,13 @@ func (m *Messenger) syncContact(ctx context.Context, contact *Contact) error { chat, ok := m.allChats[chatID] if !ok { - chat = OneToOneFromPublicKey(&m.identity.PublicKey) + chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource()) // We don't want to show the chat to the user chat.Active = false } m.allChats[chat.ID] = chat - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) syncMessage := &protobuf.SyncInstallationContact{ Clock: clock, @@ -1705,6 +1702,8 @@ type ReceivedMessageState struct { ExistingMessagesMap map[string]bool // Response to the client Response *MessengerResponse + // Timesource is a timesource for clock values/timestamps + Timesource ClockValueTimesource } func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filter][]*types.Message) (*MessengerResponse, error) { @@ -1719,6 +1718,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte ModifiedInstallations: m.modifiedInstallations, ExistingMessagesMap: make(map[string]bool), Response: &MessengerResponse{}, + Timesource: m.getTimesource(), } logger := m.logger.With(zap.String("site", "RetrieveAll")) @@ -2111,7 +2111,7 @@ func (m *Messenger) RequestTransaction(ctx context.Context, chatID, value, contr } message := &Message{} - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport) if err != nil { return nil, err } @@ -2157,7 +2157,7 @@ func (m *Messenger) RequestTransaction(ctx context.Context, chatID, value, contr return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2188,7 +2188,7 @@ func (m *Messenger) RequestAddressForTransaction(ctx context.Context, chatID, fr } message := &Message{} - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport) if err != nil { return nil, err } @@ -2233,7 +2233,7 @@ func (m *Messenger) RequestAddressForTransaction(ctx context.Context, chatID, fr return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2274,7 +2274,7 @@ func (m *Messenger) AcceptRequestAddressForTransaction(ctx context.Context, mess return nil, errors.New("Need to be a one-to-one chat") } - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2328,7 +2328,7 @@ func (m *Messenger) AcceptRequestAddressForTransaction(ctx context.Context, mess return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2369,7 +2369,7 @@ func (m *Messenger) DeclineRequestTransaction(ctx context.Context, messageID str return nil, errors.New("Need to be a one-to-one chat") } - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2410,7 +2410,7 @@ func (m *Messenger) DeclineRequestTransaction(ctx context.Context, messageID str return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2451,7 +2451,7 @@ func (m *Messenger) DeclineRequestAddressForTransaction(ctx context.Context, mes return nil, errors.New("Need to be a one-to-one chat") } - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2492,7 +2492,7 @@ func (m *Messenger) DeclineRequestAddressForTransaction(ctx context.Context, mes return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2533,7 +2533,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas return nil, errors.New("Need to be a one-to-one chat") } - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2591,7 +2591,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2622,7 +2622,7 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract } message := &Message{} - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport) if err != nil { return nil, err } @@ -2631,7 +2631,7 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract message.ContentType = protobuf.ChatMessage_TRANSACTION_COMMAND message.LocalChatID = chatID - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2672,7 +2672,7 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2716,13 +2716,13 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. chatID := contactIDFromPublicKey(validationResult.Transaction.From) chat, ok := m.allChats[chatID] if !ok { - chat = OneToOneFromPublicKey(validationResult.Transaction.From) + chat = OneToOneFromPublicKey(validationResult.Transaction.From, m.transport) } if validationResult.Message != nil { message = validationResult.Message } else { message = &Message{} - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport) if err != nil { return nil, err } @@ -2733,7 +2733,7 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. message.LocalChatID = chatID message.OutgoingStatus = "" - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.Timestamp = timestamp message.WhisperTimestamp = timestamp @@ -2757,7 +2757,7 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2795,3 +2795,7 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. } return &response, nil } + +func (m *Messenger) getTimesource() ClockValueTimesource { + return m.transport +} diff --git a/protocol/messenger_installations_test.go b/protocol/messenger_installations_test.go index 0a712119ea..29e89a3812 100644 --- a/protocol/messenger_installations_test.go +++ b/protocol/messenger_installations_test.go @@ -143,7 +143,7 @@ func (s *MessengerInstallationSuite) TestReceiveInstallation() { s.Require().Equal(contact.ID, actualContact.ID) s.Require().True(actualContact.IsAdded()) - chat := CreatePublicChat("status") + chat := CreatePublicChat("status", s.m.transport) err = s.m.SaveChat(&chat) s.Require().NoError(err) @@ -176,7 +176,7 @@ func (s *MessengerInstallationSuite) TestSyncInstallation() { s.Require().NoError(err) // add chat - chat := CreatePublicChat("status") + chat := CreatePublicChat("status", s.m.transport) err = s.m.SaveChat(&chat) s.Require().NoError(err) diff --git a/protocol/messenger_test.go b/protocol/messenger_test.go index 3576056175..72939b6b38 100644 --- a/protocol/messenger_test.go +++ b/protocol/messenger_test.go @@ -273,12 +273,13 @@ func (s *MessengerSuite) TestInit() { } func buildTestMessage(chat Chat) *Message { - + clock, timestamp := chat.NextClockAndTimestamp(&testTimeSource{}) message := &Message{} message.Text = "text-input-message" message.ChatId = chat.ID - message.Clock = 2 - message.WhisperTimestamp = 10 + message.Clock = clock + message.Timestamp = timestamp + message.WhisperTimestamp = clock message.LocalChatID = chat.ID message.ContentType = protobuf.ChatMessage_TEXT_PLAIN switch chat.ChatType { @@ -294,7 +295,7 @@ func buildTestMessage(chat Chat) *Message { } func (s *MessengerSuite) TestMarkMessagesSeen() { - chat := CreatePublicChat("test-chat") + chat := CreatePublicChat("test-chat", s.m.transport) chat.UnviewedMessagesCount = 2 err := s.m.SaveChat(&chat) s.Require().NoError(err) @@ -317,7 +318,7 @@ func (s *MessengerSuite) TestMarkMessagesSeen() { } func (s *MessengerSuite) TestSendPublic() { - chat := CreatePublicChat("test-chat") + chat := CreatePublicChat("test-chat", s.m.transport) chat.LastClockValue = uint64(100000000000000) err := s.m.SaveChat(&chat) s.NoError(err) @@ -346,7 +347,7 @@ func (s *MessengerSuite) TestSendPrivateOneToOne() { recipientKey, err := crypto.GenerateKey() s.NoError(err) pkString := hex.EncodeToString(crypto.FromECDSAPub(&recipientKey.PublicKey)) - chat := CreateOneToOneChat(pkString, &recipientKey.PublicKey) + chat := CreateOneToOneChat(pkString, &recipientKey.PublicKey, s.m.transport) inputMessage := &Message{} inputMessage.ChatId = chat.ID @@ -432,7 +433,7 @@ func (s *MessengerSuite) TestSendPrivateEmptyGroup() { // Make sure public messages sent by us are not func (s *MessengerSuite) TestRetrieveOwnPublic() { - chat := CreatePublicChat("status") + chat := CreatePublicChat("status", s.m.transport) err := s.m.SaveChat(&chat) s.NoError(err) // Right-to-left text @@ -467,11 +468,11 @@ func (s *MessengerSuite) TestRetrieveOwnPublic() { // Retrieve their public message func (s *MessengerSuite) TestRetrieveTheirPublic() { theirMessenger := s.newMessenger(s.shh) - theirChat := CreatePublicChat("status") + theirChat := CreatePublicChat("status", s.m.transport) err := theirMessenger.SaveChat(&theirChat) s.Require().NoError(err) - chat := CreatePublicChat("status") + chat := CreatePublicChat("status", s.m.transport) err = s.m.SaveChat(&chat) s.Require().NoError(err) @@ -510,11 +511,11 @@ func (s *MessengerSuite) TestRetrieveTheirPublic() { func (s *MessengerSuite) TestDeletedAtClockValue() { theirMessenger := s.newMessenger(s.shh) - theirChat := CreatePublicChat("status") + theirChat := CreatePublicChat("status", s.m.transport) err := theirMessenger.SaveChat(&theirChat) s.Require().NoError(err) - chat := CreatePublicChat("status") + chat := CreatePublicChat("status", s.m.transport) err = s.m.SaveChat(&chat) s.Require().NoError(err) @@ -539,11 +540,11 @@ func (s *MessengerSuite) TestDeletedAtClockValue() { func (s *MessengerSuite) TestRetrieveBlockedContact() { theirMessenger := s.newMessenger(s.shh) - theirChat := CreatePublicChat("status") + theirChat := CreatePublicChat("status", s.m.transport) err := theirMessenger.SaveChat(&theirChat) s.Require().NoError(err) - chat := CreatePublicChat("status") + chat := CreatePublicChat("status", s.m.transport) err = s.m.SaveChat(&chat) s.Require().NoError(err) @@ -553,7 +554,6 @@ func (s *MessengerSuite) TestRetrieveBlockedContact() { publicKeyHex := "0x" + hex.EncodeToString(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey)) blockedContact := Contact{ ID: publicKeyHex, - Address: "contact-address", Name: "contact-name", Photo: "contact-photo", LastUpdated: 20, @@ -578,11 +578,11 @@ func (s *MessengerSuite) TestRetrieveBlockedContact() { // Resend their public message, receive only once func (s *MessengerSuite) TestResendPublicMessage() { theirMessenger := s.newMessenger(s.shh) - theirChat := CreatePublicChat("status") + theirChat := CreatePublicChat("status", s.m.transport) err := theirMessenger.SaveChat(&theirChat) s.Require().NoError(err) - chat := CreatePublicChat("status") + chat := CreatePublicChat("status", s.m.transport) err = s.m.SaveChat(&chat) s.Require().NoError(err) @@ -635,11 +635,11 @@ func (s *MessengerSuite) TestResendPublicMessage() { // Test receiving a message on an existing private chat func (s *MessengerSuite) TestRetrieveTheirPrivateChatExisting() { theirMessenger := s.newMessenger(s.shh) - theirChat := CreateOneToOneChat("XXX", &s.privateKey.PublicKey) + theirChat := CreateOneToOneChat("XXX", &s.privateKey.PublicKey, s.m.transport) err := theirMessenger.SaveChat(&theirChat) s.Require().NoError(err) - ourChat := CreateOneToOneChat("our-chat", &theirMessenger.identity.PublicKey) + ourChat := CreateOneToOneChat("our-chat", &theirMessenger.identity.PublicKey, s.m.transport) ourChat.UnviewedMessagesCount = 1 // Make chat inactive ourChat.Active = false @@ -679,7 +679,7 @@ func (s *MessengerSuite) TestRetrieveTheirPrivateChatExisting() { // Test receiving a message on an non-existing private chat func (s *MessengerSuite) TestRetrieveTheirPrivateChatNonExisting() { theirMessenger := s.newMessenger(s.shh) - chat := CreateOneToOneChat("XXX", &s.privateKey.PublicKey) + chat := CreateOneToOneChat("XXX", &s.privateKey.PublicKey, s.m.transport) err := theirMessenger.SaveChat(&chat) s.NoError(err) @@ -718,7 +718,7 @@ func (s *MessengerSuite) TestRetrieveTheirPrivateChatNonExisting() { // Test receiving a message on an non-existing public chat func (s *MessengerSuite) TestRetrieveTheirPublicChatNonExisting() { theirMessenger := s.newMessenger(s.shh) - chat := CreatePublicChat("test-chat") + chat := CreatePublicChat("test-chat", s.m.transport) err := theirMessenger.SaveChat(&chat) s.NoError(err) @@ -1093,7 +1093,6 @@ func (s *MessengerSuite) TestBlockContact() { contact := Contact{ ID: pk, - Address: "contact-address", Name: "contact-name", Photo: "contact-photo", LastUpdated: 20, @@ -1282,7 +1281,6 @@ func (s *MessengerSuite) TestContactPersistence() { contact := Contact{ ID: "0x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1", - Address: "contact-address", Name: "contact-name", Photo: "contact-photo", LastUpdated: 20, @@ -1390,7 +1388,6 @@ func (s *MessengerSuite) TestContactPersistenceUpdate() { contact := Contact{ ID: contactID, - Address: "contact-address", Name: "contact-name", Photo: "contact-photo", LastUpdated: 20, @@ -1482,7 +1479,7 @@ func (s *MessengerSuite) TestDeclineRequestAddressForTransaction() { theirMessenger := s.newMessenger(s.shh) theirPkString := types.EncodeHex(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey)) - chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey) + chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey, s.m.transport) err := s.m.SaveChat(&chat) s.Require().NoError(err) @@ -1579,7 +1576,7 @@ func (s *MessengerSuite) TestSendEthTransaction() { receiverAddress := crypto.PubkeyToAddress(theirMessenger.identity.PublicKey) receiverAddressString := strings.ToLower(receiverAddress.Hex()) - chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey) + chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey, s.m.transport) err := s.m.SaveChat(&chat) s.Require().NoError(err) @@ -1680,7 +1677,7 @@ func (s *MessengerSuite) TestSendTokenTransaction() { receiverAddress := crypto.PubkeyToAddress(theirMessenger.identity.PublicKey) receiverAddressString := strings.ToLower(receiverAddress.Hex()) - chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey) + chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey, s.m.transport) err := s.m.SaveChat(&chat) s.Require().NoError(err) @@ -1779,7 +1776,7 @@ func (s *MessengerSuite) TestAcceptRequestAddressForTransaction() { myAddress := crypto.PubkeyToAddress(s.m.identity.PublicKey) - chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey) + chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey, s.m.transport) err := s.m.SaveChat(&chat) s.Require().NoError(err) @@ -1874,7 +1871,7 @@ func (s *MessengerSuite) TestDeclineRequestTransaction() { theirMessenger := s.newMessenger(s.shh) theirPkString := types.EncodeHex(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey)) - chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey) + chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey, s.m.transport) err := s.m.SaveChat(&chat) s.Require().NoError(err) @@ -1967,7 +1964,7 @@ func (s *MessengerSuite) TestRequestTransaction() { theirMessenger := s.newMessenger(s.shh) theirPkString := types.EncodeHex(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey)) - chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey) + chat := CreateOneToOneChat(theirPkString, &theirMessenger.identity.PublicKey, s.m.transport) err := s.m.SaveChat(&chat) s.Require().NoError(err) @@ -2203,6 +2200,12 @@ func (s *MessageHandlerSuite) TearDownTest() { _ = s.logger.Sync() } +type testTimeSource struct{} + +func (t *testTimeSource) GetCurrentTime() uint64 { + return uint64(time.Now().Unix()) +} + func (s *MessageHandlerSuite) TestRun() { key1, err := crypto.GenerateKey() s.Require().NoError(err) @@ -2219,7 +2222,7 @@ func (s *MessageHandlerSuite) TestRun() { }{ { Name: "Public chat", - Chat: CreatePublicChat("test-chat"), + Chat: CreatePublicChat("test-chat", &testTimeSource{}), Message: Message{ ChatMessage: protobuf.ChatMessage{ ChatId: "test-chat", @@ -2231,7 +2234,7 @@ func (s *MessageHandlerSuite) TestRun() { }, { Name: "Private message from myself with existing chat", - Chat: CreateOneToOneChat("test-private-chat", &key1.PublicKey), + Chat: CreateOneToOneChat("test-private-chat", &key1.PublicKey, &testTimeSource{}), Message: Message{ ChatMessage: protobuf.ChatMessage{ ChatId: "test-chat", @@ -2243,7 +2246,7 @@ func (s *MessageHandlerSuite) TestRun() { }, { Name: "Private message from other with existing chat", - Chat: CreateOneToOneChat("test-private-chat", &key2.PublicKey), + Chat: CreateOneToOneChat("test-private-chat", &key2.PublicKey, &testTimeSource{}), Message: Message{ ChatMessage: protobuf.ChatMessage{ ChatId: "test-chat", @@ -2309,7 +2312,7 @@ func (s *MessageHandlerSuite) TestRun() { s.Empty(message.LocalChatID) message.ID = strconv.Itoa(idx) // manually set the ID because messages does not go through messageProcessor - chat, err := s.messageHandler.matchMessage(&message, chatsMap) + chat, err := s.messageHandler.matchMessage(&message, chatsMap, &testTimeSource{}) if tc.Error { s.Require().Error(err) } else { diff --git a/protocol/transport/transport.go b/protocol/transport/transport.go index 9ec4c55dbd..6a75fe546b 100644 --- a/protocol/transport/transport.go +++ b/protocol/transport/transport.go @@ -16,6 +16,7 @@ type Transport interface { LeaveGroup(publicKeys []*ecdsa.PublicKey) error JoinPublic(chatID string) error LeavePublic(chatID string) error + GetCurrentTime() uint64 SendPublic(ctx context.Context, newMessage *types.NewMessage, chatName string) ([]byte, error) SendPrivateWithSharedSecret(ctx context.Context, newMessage *types.NewMessage, publicKey *ecdsa.PublicKey, secret []byte) ([]byte, error) diff --git a/protocol/transport/waku/waku_service.go b/protocol/transport/waku/waku_service.go index 57b3193822..92adec1af6 100644 --- a/protocol/transport/waku/waku_service.go +++ b/protocol/transport/waku/waku_service.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "database/sql" "sync" + "time" "github.com/pkg/errors" "go.uber.org/zap" @@ -360,6 +361,11 @@ func (a *WakuServiceTransport) Track(identifiers [][]byte, hash []byte, newMessa } } +// GetCurrentTime returns the current unix timestamp in milliseconds +func (a *WakuServiceTransport) GetCurrentTime() uint64 { + return uint64(a.waku.GetCurrentTime().UnixNano() / int64(time.Millisecond)) +} + func (a *WakuServiceTransport) Stop() error { if a.envelopesMonitor != nil { a.envelopesMonitor.Stop() diff --git a/protocol/transport/whisper/whisper_service.go b/protocol/transport/whisper/whisper_service.go index 38bc3a0a2c..2c54c367e9 100644 --- a/protocol/transport/whisper/whisper_service.go +++ b/protocol/transport/whisper/whisper_service.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "database/sql" "sync" + "time" "github.com/pkg/errors" "go.uber.org/zap" @@ -360,6 +361,11 @@ func (a *WhisperServiceTransport) Track(identifiers [][]byte, hash []byte, newMe } } +// GetCurrentTime returns the current unix timestamp in milliseconds +func (a *WhisperServiceTransport) GetCurrentTime() uint64 { + return uint64(a.shh.GetCurrentTime().UnixNano() / int64(time.Millisecond)) +} + func (a *WhisperServiceTransport) Stop() error { if a.envelopesMonitor != nil { a.envelopesMonitor.Stop() diff --git a/vendor/github.com/status-im/status-go/protocol/chat.go b/vendor/github.com/status-im/status-go/protocol/chat.go index 08988daca5..4c2c687a89 100644 --- a/vendor/github.com/status-im/status-go/protocol/chat.go +++ b/vendor/github.com/status-im/status-go/protocol/chat.go @@ -207,39 +207,39 @@ func oneToOneChatID(publicKey *ecdsa.PublicKey) string { return types.EncodeHex(crypto.FromECDSAPub(publicKey)) } -func OneToOneFromPublicKey(pk *ecdsa.PublicKey) *Chat { +func OneToOneFromPublicKey(pk *ecdsa.PublicKey, timesource ClockValueTimesource) *Chat { chatID := types.EncodeHex(crypto.FromECDSAPub(pk)) - newChat := CreateOneToOneChat(chatID[:8], pk) + newChat := CreateOneToOneChat(chatID[:8], pk, timesource) return &newChat } -func CreateOneToOneChat(name string, publicKey *ecdsa.PublicKey) Chat { +func CreateOneToOneChat(name string, publicKey *ecdsa.PublicKey, timesource ClockValueTimesource) Chat { return Chat{ ID: oneToOneChatID(publicKey), Name: name, - Timestamp: int64(timestampInMs()), + Timestamp: int64(timesource.GetCurrentTime()), Active: true, ChatType: ChatTypeOneToOne, } } -func CreatePublicChat(name string) Chat { +func CreatePublicChat(name string, timesource ClockValueTimesource) Chat { return Chat{ ID: name, Name: name, Active: true, - Timestamp: int64(timestampInMs()), + Timestamp: int64(timesource.GetCurrentTime()), Color: chatColors[rand.Intn(len(chatColors))], ChatType: ChatTypePublic, } } -func createGroupChat() Chat { +func createGroupChat(timesource ClockValueTimesource) Chat { return Chat{ Active: true, Color: chatColors[rand.Intn(len(chatColors))], - Timestamp: int64(timestampInMs()), + Timestamp: int64(timesource.GetCurrentTime()), ChatType: ChatTypePrivateGroupChat, } } @@ -276,11 +276,15 @@ func stringSliceToPublicKeys(slice []string, prefixed bool) ([]*ecdsa.PublicKey, return result, nil } +type ClockValueTimesource interface { + GetCurrentTime() uint64 +} + // NextClockAndTimestamp returns the next clock value // and the current timestamp -func (c *Chat) NextClockAndTimestamp() (uint64, uint64) { +func (c *Chat) NextClockAndTimestamp(timesource ClockValueTimesource) (uint64, uint64) { clock := c.LastClockValue - timestamp := timestampInMs() + timestamp := timesource.GetCurrentTime() if clock == 0 || clock < timestamp { clock = timestamp } else { @@ -289,8 +293,8 @@ func (c *Chat) NextClockAndTimestamp() (uint64, uint64) { return clock, timestamp } -func (c *Chat) UpdateFromMessage(message *Message) error { - c.Timestamp = int64(timestampInMs()) +func (c *Chat) UpdateFromMessage(message *Message, timesource ClockValueTimesource) error { + c.Timestamp = int64(timesource.GetCurrentTime()) if c.LastClockValue <= message.Clock { jsonMessage, err := json.Marshal(message) if err != nil { diff --git a/vendor/github.com/status-im/status-go/protocol/contact.go b/vendor/github.com/status-im/status-go/protocol/contact.go index fdbe8158ed..a12bc12c8b 100644 --- a/vendor/github.com/status-im/status-go/protocol/contact.go +++ b/vendor/github.com/status-im/status-go/protocol/contact.go @@ -3,7 +3,6 @@ package protocol import ( "crypto/ecdsa" "encoding/hex" - "strings" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" @@ -33,7 +32,7 @@ type Contact struct { // ID of the contact. It's a hex-encoded public key (prefixed with 0x). ID string `json:"id"` // Ethereum address of the contact - Address string `json:"address"` + Address string `json:"address,omitempty"` // ENS name of contact Name string `json:"name,omitempty"` // EnsVerified whether we verified the name of the contact @@ -88,8 +87,6 @@ func existsInStringSlice(set []string, find string) bool { } func buildContact(publicKey *ecdsa.PublicKey) (*Contact, error) { - address := strings.ToLower(crypto.PubkeyToAddress(*publicKey).Hex()) - id := "0x" + hex.EncodeToString(crypto.FromECDSAPub(publicKey)) identicon, err := identicon.GenerateBase64(id) @@ -99,7 +96,6 @@ func buildContact(publicKey *ecdsa.PublicKey) (*Contact, error) { contact := &Contact{ ID: id, - Address: address[2:], Alias: alias.GenerateFromPublicKey(publicKey), Identicon: identicon, } diff --git a/vendor/github.com/status-im/status-go/protocol/message_builder.go b/vendor/github.com/status-im/status-go/protocol/message_builder.go index 2c4f48882c..da2e11060d 100644 --- a/vendor/github.com/status-im/status-go/protocol/message_builder.go +++ b/vendor/github.com/status-im/status-go/protocol/message_builder.go @@ -9,8 +9,8 @@ import ( "github.com/status-im/status-go/protocol/identity/identicon" ) -func extendMessageFromChat(message *Message, chat *Chat, key *ecdsa.PublicKey) error { - clock, timestamp := chat.NextClockAndTimestamp() +func extendMessageFromChat(message *Message, chat *Chat, key *ecdsa.PublicKey, timesource ClockValueTimesource) error { + clock, timestamp := chat.NextClockAndTimestamp(timesource) message.LocalChatID = chat.ID message.Clock = clock diff --git a/vendor/github.com/status-im/status-go/protocol/message_handler.go b/vendor/github.com/status-im/status-go/protocol/message_handler.go index 5e0e286931..7cda081db5 100644 --- a/vendor/github.com/status-im/status-go/protocol/message_handler.go +++ b/vendor/github.com/status-im/status-go/protocol/message_handler.go @@ -33,12 +33,19 @@ func (m *MessageHandler) HandleMembershipUpdate(messageState *ReceivedMessageSta var group *v1protocol.Group var err error + logger := m.logger.With(zap.String("site", "HandleMembershipUpdate")) + message, err := v1protocol.MembershipUpdateMessageFromProtobuf(&rawMembershipUpdate) if err != nil { return err } + if err := ValidateMembershipUpdateMessage(message, messageState.Timesource.GetCurrentTime()); err != nil { + logger.Warn("failed to validate message", zap.Error(err)) + return err + } + if chat == nil { if len(message.Events) == 0 { return errors.New("can't create new group chat without events") @@ -52,7 +59,7 @@ func (m *MessageHandler) HandleMembershipUpdate(messageState *ReceivedMessageSta if !group.IsMember(contactIDFromPublicKey(&m.identity.PublicKey)) { return errors.New("can't create a new group chat without us being a member") } - newChat := createGroupChat() + newChat := createGroupChat(messageState.Timesource) chat = &newChat } else { @@ -108,7 +115,7 @@ func (m *MessageHandler) handleCommandMessage(state *ReceivedMessageState, messa message.WhisperTimestamp = state.CurrentMessageState.WhisperTimestamp message.PrepareContent() - chat, err := m.matchMessage(message, state.AllChats) + chat, err := m.matchMessage(message, state.AllChats, state.Timesource) if err != nil { return err } @@ -137,7 +144,7 @@ func (m *MessageHandler) handleCommandMessage(state *ReceivedMessageState, messa message.OutgoingStatus = OutgoingStatusSent } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, state.Timesource) if err != nil { return err } @@ -158,7 +165,7 @@ func (m *MessageHandler) handleCommandMessage(state *ReceivedMessageState, messa func (m *MessageHandler) HandleSyncInstallationContact(state *ReceivedMessageState, message protobuf.SyncInstallationContact) error { chat, ok := state.AllChats[state.CurrentMessageState.Contact.ID] if !ok { - chat = OneToOneFromPublicKey(state.CurrentMessageState.PublicKey) + chat = OneToOneFromPublicKey(state.CurrentMessageState.PublicKey, state.Timesource) // We don't want to show the chat to the user chat.Active = false } @@ -205,7 +212,7 @@ func (m *MessageHandler) HandleSyncInstallationPublicChat(state *ReceivedMessage return nil } - chat := CreatePublicChat(chatID) + chat := CreatePublicChat(chatID, state.Timesource) state.AllChats[chat.ID] = &chat state.ModifiedChats[chat.ID] = true @@ -218,7 +225,7 @@ func (m *MessageHandler) HandleContactUpdate(state *ReceivedMessageState, messag contact := state.CurrentMessageState.Contact chat, ok := state.AllChats[contact.ID] if !ok { - chat = OneToOneFromPublicKey(state.CurrentMessageState.PublicKey) + chat = OneToOneFromPublicKey(state.CurrentMessageState.PublicKey, state.Timesource) // We don't want to show the chat to the user chat.Active = false } @@ -252,7 +259,7 @@ func (m *MessageHandler) HandleContactUpdate(state *ReceivedMessageState, messag func (m *MessageHandler) HandlePairInstallation(state *ReceivedMessageState, message protobuf.PairInstallation) error { logger := m.logger.With(zap.String("site", "HandlePairInstallation")) - if err := ValidateReceivedPairInstallation(&message); err != nil { + if err := ValidateReceivedPairInstallation(&message, state.CurrentMessageState.WhisperTimestamp); err != nil { logger.Warn("failed to validate message", zap.Error(err)) return err } @@ -276,7 +283,7 @@ func (m *MessageHandler) HandlePairInstallation(state *ReceivedMessageState, mes func (m *MessageHandler) HandleChatMessage(state *ReceivedMessageState) error { logger := m.logger.With(zap.String("site", "handleChatMessage")) - if err := ValidateReceivedChatMessage(&state.CurrentMessageState.Message); err != nil { + if err := ValidateReceivedChatMessage(&state.CurrentMessageState.Message, state.CurrentMessageState.WhisperTimestamp); err != nil { logger.Warn("failed to validate message", zap.Error(err)) return err } @@ -291,7 +298,7 @@ func (m *MessageHandler) HandleChatMessage(state *ReceivedMessageState) error { } receivedMessage.PrepareContent() - chat, err := m.matchMessage(receivedMessage, state.AllChats) + chat, err := m.matchMessage(receivedMessage, state.AllChats, state.Timesource) if err != nil { return err } @@ -319,7 +326,7 @@ func (m *MessageHandler) HandleChatMessage(state *ReceivedMessageState) error { receivedMessage.OutgoingStatus = OutgoingStatusSent } - err = chat.UpdateFromMessage(receivedMessage) + err = chat.UpdateFromMessage(receivedMessage, state.Timesource) if err != nil { return err } @@ -339,7 +346,7 @@ func (m *MessageHandler) HandleChatMessage(state *ReceivedMessageState) error { } func (m *MessageHandler) HandleRequestAddressForTransaction(messageState *ReceivedMessageState, command protobuf.RequestAddressForTransaction) error { - err := ValidateReceivedRequestAddressForTransaction(&command) + err := ValidateReceivedRequestAddressForTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -363,7 +370,7 @@ func (m *MessageHandler) HandleRequestAddressForTransaction(messageState *Receiv } func (m *MessageHandler) HandleRequestTransaction(messageState *ReceivedMessageState, command protobuf.RequestTransaction) error { - err := ValidateReceivedRequestTransaction(&command) + err := ValidateReceivedRequestTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -388,7 +395,7 @@ func (m *MessageHandler) HandleRequestTransaction(messageState *ReceivedMessageS } func (m *MessageHandler) HandleAcceptRequestAddressForTransaction(messageState *ReceivedMessageState, command protobuf.AcceptRequestAddressForTransaction) error { - err := ValidateReceivedAcceptRequestAddressForTransaction(&command) + err := ValidateReceivedAcceptRequestAddressForTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -437,7 +444,7 @@ func (m *MessageHandler) HandleAcceptRequestAddressForTransaction(messageState * } func (m *MessageHandler) HandleSendTransaction(messageState *ReceivedMessageState, command protobuf.SendTransaction) error { - err := ValidateReceivedSendTransaction(&command) + err := ValidateReceivedSendTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -457,7 +464,7 @@ func (m *MessageHandler) HandleSendTransaction(messageState *ReceivedMessageStat } func (m *MessageHandler) HandleDeclineRequestAddressForTransaction(messageState *ReceivedMessageState, command protobuf.DeclineRequestAddressForTransaction) error { - err := ValidateReceivedDeclineRequestAddressForTransaction(&command) + err := ValidateReceivedDeclineRequestAddressForTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -497,7 +504,7 @@ func (m *MessageHandler) HandleDeclineRequestAddressForTransaction(messageState } func (m *MessageHandler) HandleDeclineRequestTransaction(messageState *ReceivedMessageState, command protobuf.DeclineRequestTransaction) error { - err := ValidateReceivedDeclineRequestTransaction(&command) + err := ValidateReceivedDeclineRequestTransaction(&command, messageState.CurrentMessageState.WhisperTimestamp) if err != nil { return err } @@ -536,7 +543,7 @@ func (m *MessageHandler) HandleDeclineRequestTransaction(messageState *ReceivedM return m.handleCommandMessage(messageState, oldMessage) } -func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat) (*Chat, error) { +func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat, timesource ClockValueTimesource) (*Chat, error) { if message.SigPubKey == nil { m.logger.Error("public key can't be empty") return nil, errors.New("received a message with empty public key") @@ -571,7 +578,7 @@ func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat) return nil, errors.Wrap(err, "failed to decode pubkey") } - newChat := CreateOneToOneChat(chatID[:8], pubKey) + newChat := CreateOneToOneChat(chatID[:8], pubKey, timesource) chat = &newChat } return chat, nil @@ -582,7 +589,7 @@ func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat) chat := chats[chatID] if chat == nil { // TODO: this should be a three-word name used in the mobile client - newChat := CreateOneToOneChat(chatID[:8], message.SigPubKey) + newChat := CreateOneToOneChat(chatID[:8], message.SigPubKey, timesource) chat = &newChat } return chat, nil diff --git a/vendor/github.com/status-im/status-go/protocol/message_validator.go b/vendor/github.com/status-im/status-go/protocol/message_validator.go index 682cc60470..cd2fe7ee0e 100644 --- a/vendor/github.com/status-im/status-go/protocol/message_validator.go +++ b/vendor/github.com/status-im/status-go/protocol/message_validator.go @@ -6,13 +6,41 @@ import ( "strings" "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/v1" ) -func ValidateReceivedPairInstallation(message *protobuf.PairInstallation) error { - if message.Clock == 0 { +// maxWhisperDrift is how many milliseconds we allow the clock value to differ +// from whisperTimestamp +const maxWhisperFutureDriftMs uint64 = 120000 + +func validateClockValue(clock uint64, whisperTimestamp uint64) error { + if clock == 0 { return errors.New("clock can't be 0") } + if clock > whisperTimestamp && clock-whisperTimestamp > maxWhisperFutureDriftMs { + return errors.New("clock value too high") + } + + return nil +} + +func ValidateMembershipUpdateMessage(message *protocol.MembershipUpdateMessage, timeNowMs uint64) error { + + for _, e := range message.Events { + if err := validateClockValue(e.ClockValue, timeNowMs); err != nil { + return err + } + + } + return nil +} + +func ValidateReceivedPairInstallation(message *protobuf.PairInstallation, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err + } + if len(strings.TrimSpace(message.Name)) == 0 { return errors.New("name can't be empty") } @@ -28,9 +56,9 @@ func ValidateReceivedPairInstallation(message *protobuf.PairInstallation) error return nil } -func ValidateReceivedSendTransaction(message *protobuf.SendTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedSendTransaction(message *protobuf.SendTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(strings.TrimSpace(message.TransactionHash)) == 0 { @@ -44,9 +72,9 @@ func ValidateReceivedSendTransaction(message *protobuf.SendTransaction) error { return nil } -func ValidateReceivedRequestAddressForTransaction(message *protobuf.RequestAddressForTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedRequestAddressForTransaction(message *protobuf.RequestAddressForTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(strings.TrimSpace(message.Value)) == 0 { @@ -61,9 +89,9 @@ func ValidateReceivedRequestAddressForTransaction(message *protobuf.RequestAddre return nil } -func ValidateReceivedRequestTransaction(message *protobuf.RequestTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedRequestTransaction(message *protobuf.RequestTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(strings.TrimSpace(message.Value)) == 0 { @@ -82,9 +110,9 @@ func ValidateReceivedRequestTransaction(message *protobuf.RequestTransaction) er return nil } -func ValidateReceivedAcceptRequestAddressForTransaction(message *protobuf.AcceptRequestAddressForTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedAcceptRequestAddressForTransaction(message *protobuf.AcceptRequestAddressForTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(message.Id) == 0 { @@ -98,9 +126,9 @@ func ValidateReceivedAcceptRequestAddressForTransaction(message *protobuf.Accept return nil } -func ValidateReceivedDeclineRequestAddressForTransaction(message *protobuf.DeclineRequestAddressForTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedDeclineRequestAddressForTransaction(message *protobuf.DeclineRequestAddressForTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(message.Id) == 0 { @@ -110,9 +138,9 @@ func ValidateReceivedDeclineRequestAddressForTransaction(message *protobuf.Decli return nil } -func ValidateReceivedDeclineRequestTransaction(message *protobuf.DeclineRequestTransaction) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedDeclineRequestTransaction(message *protobuf.DeclineRequestTransaction, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if len(message.Id) == 0 { @@ -122,9 +150,9 @@ func ValidateReceivedDeclineRequestTransaction(message *protobuf.DeclineRequestT return nil } -func ValidateReceivedChatMessage(message *protobuf.ChatMessage) error { - if message.Clock == 0 { - return errors.New("clock can't be 0") +func ValidateReceivedChatMessage(message *protobuf.ChatMessage, whisperTimestamp uint64) error { + if err := validateClockValue(message.Clock, whisperTimestamp); err != nil { + return err } if message.Timestamp == 0 { diff --git a/vendor/github.com/status-im/status-go/protocol/messenger.go b/vendor/github.com/status-im/status-go/protocol/messenger.go index 1a5d5871d1..4baec271eb 100644 --- a/vendor/github.com/status-im/status-go/protocol/messenger.go +++ b/vendor/github.com/status-im/status-go/protocol/messenger.go @@ -283,6 +283,7 @@ func NewMessenger( // Initialize transport layer. var transp transport.Transport + if shh, err := node.GetWhisper(nil); err == nil { transp, err = shhtransp.NewWhisperServiceTransport( shh, @@ -631,14 +632,14 @@ func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string, var response MessengerResponse logger := m.logger.With(zap.String("site", "CreateGroupChatWithMembers")) logger.Info("Creating group chat", zap.String("name", name), zap.Any("members", members)) - chat := createGroupChat() + chat := createGroupChat(m.getTimesource()) group, err := v1protocol.NewGroupWithCreator(name, m.identity) if err != nil { return nil, err } chat.updateChatFromProtocolGroup(group) - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) // Add members event := v1protocol.NewMembersAddedEvent(members, clock) event.ChatID = chat.ID @@ -710,7 +711,7 @@ func (m *Messenger) RemoveMemberFromGroupChat(ctx context.Context, chatID string return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) // Remove member event := v1protocol.NewMemberRemovedEvent(member, clock) event.ChatID = chat.ID @@ -767,7 +768,7 @@ func (m *Messenger) AddMembersToGroupChat(ctx context.Context, chatID string, me return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) // Add members event := v1protocol.NewMembersAddedEvent(members, clock) event.ChatID = chat.ID @@ -832,7 +833,7 @@ func (m *Messenger) AddAdminsToGroupChat(ctx context.Context, chatID string, mem return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) // Add members event := v1protocol.NewAdminsAddedEvent(members, clock) event.ChatID = chat.ID @@ -899,7 +900,7 @@ func (m *Messenger) ConfirmJoiningGroup(ctx context.Context, chatID string) (*Me if err != nil { return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) event := v1protocol.NewMemberJoinedEvent( clock, ) @@ -966,7 +967,7 @@ func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string) (*Messeng if err != nil { return nil, err } - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) event := v1protocol.NewMemberRemovedEvent( contactIDFromPublicKey(&m.identity.PublicKey), clock, @@ -1146,10 +1147,6 @@ func (m *Messenger) Contacts() []*Contact { return contacts } -func timestampInMs() uint64 { - return uint64(time.Now().UnixNano() / int64(time.Millisecond)) -} - // ReSendChatMessage pulls a message from the database and sends it again func (m *Messenger) ReSendChatMessage(ctx context.Context, messageID string) error { m.mutex.Lock() @@ -1307,7 +1304,7 @@ func (m *Messenger) SendChatMessage(ctx context.Context, message *Message) (*Mes return nil, errors.New("Chat not found") } - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.getTimesource()) if err != nil { return nil, err } @@ -1361,7 +1358,7 @@ func (m *Messenger) SendChatMessage(ctx context.Context, message *Message) (*Mes return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.getTimesource()) if err != nil { return nil, err } @@ -1432,13 +1429,13 @@ func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, ensName, prof if err != nil { return nil, err } - chat = OneToOneFromPublicKey(publicKey) + chat = OneToOneFromPublicKey(publicKey, m.getTimesource()) // We don't want to show the chat to the user chat.Active = false } m.allChats[chat.ID] = chat - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) contactUpdate := &protobuf.ContactUpdate{ Clock: clock, @@ -1525,13 +1522,13 @@ func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerRespons chat, ok := m.allChats[chatID] if !ok { - chat = OneToOneFromPublicKey(&m.identity.PublicKey) + chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource()) // We don't want to show the chat to the user chat.Active = false } m.allChats[chat.ID] = chat - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) pairMessage := &protobuf.PairInstallation{ Clock: clock, @@ -1573,13 +1570,13 @@ func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat) error chat, ok := m.allChats[chatID] if !ok { - chat = OneToOneFromPublicKey(&m.identity.PublicKey) + chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource()) // We don't want to show the chat to the user chat.Active = false } m.allChats[chat.ID] = chat - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) syncMessage := &protobuf.SyncInstallationPublicChat{ Clock: clock, @@ -1614,13 +1611,13 @@ func (m *Messenger) syncContact(ctx context.Context, contact *Contact) error { chat, ok := m.allChats[chatID] if !ok { - chat = OneToOneFromPublicKey(&m.identity.PublicKey) + chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource()) // We don't want to show the chat to the user chat.Active = false } m.allChats[chat.ID] = chat - clock, _ := chat.NextClockAndTimestamp() + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) syncMessage := &protobuf.SyncInstallationContact{ Clock: clock, @@ -1705,6 +1702,8 @@ type ReceivedMessageState struct { ExistingMessagesMap map[string]bool // Response to the client Response *MessengerResponse + // Timesource is a timesource for clock values/timestamps + Timesource ClockValueTimesource } func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filter][]*types.Message) (*MessengerResponse, error) { @@ -1719,6 +1718,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte ModifiedInstallations: m.modifiedInstallations, ExistingMessagesMap: make(map[string]bool), Response: &MessengerResponse{}, + Timesource: m.getTimesource(), } logger := m.logger.With(zap.String("site", "RetrieveAll")) @@ -2111,7 +2111,7 @@ func (m *Messenger) RequestTransaction(ctx context.Context, chatID, value, contr } message := &Message{} - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport) if err != nil { return nil, err } @@ -2157,7 +2157,7 @@ func (m *Messenger) RequestTransaction(ctx context.Context, chatID, value, contr return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2188,7 +2188,7 @@ func (m *Messenger) RequestAddressForTransaction(ctx context.Context, chatID, fr } message := &Message{} - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport) if err != nil { return nil, err } @@ -2233,7 +2233,7 @@ func (m *Messenger) RequestAddressForTransaction(ctx context.Context, chatID, fr return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2274,7 +2274,7 @@ func (m *Messenger) AcceptRequestAddressForTransaction(ctx context.Context, mess return nil, errors.New("Need to be a one-to-one chat") } - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2328,7 +2328,7 @@ func (m *Messenger) AcceptRequestAddressForTransaction(ctx context.Context, mess return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2369,7 +2369,7 @@ func (m *Messenger) DeclineRequestTransaction(ctx context.Context, messageID str return nil, errors.New("Need to be a one-to-one chat") } - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2410,7 +2410,7 @@ func (m *Messenger) DeclineRequestTransaction(ctx context.Context, messageID str return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2451,7 +2451,7 @@ func (m *Messenger) DeclineRequestAddressForTransaction(ctx context.Context, mes return nil, errors.New("Need to be a one-to-one chat") } - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2492,7 +2492,7 @@ func (m *Messenger) DeclineRequestAddressForTransaction(ctx context.Context, mes return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2533,7 +2533,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas return nil, errors.New("Need to be a one-to-one chat") } - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2591,7 +2591,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2622,7 +2622,7 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract } message := &Message{} - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport) if err != nil { return nil, err } @@ -2631,7 +2631,7 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract message.ContentType = protobuf.ChatMessage_TRANSACTION_COMMAND message.LocalChatID = chatID - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.WhisperTimestamp = timestamp message.Timestamp = timestamp @@ -2672,7 +2672,7 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2716,13 +2716,13 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. chatID := contactIDFromPublicKey(validationResult.Transaction.From) chat, ok := m.allChats[chatID] if !ok { - chat = OneToOneFromPublicKey(validationResult.Transaction.From) + chat = OneToOneFromPublicKey(validationResult.Transaction.From, m.transport) } if validationResult.Message != nil { message = validationResult.Message } else { message = &Message{} - err := extendMessageFromChat(message, chat, &m.identity.PublicKey) + err := extendMessageFromChat(message, chat, &m.identity.PublicKey, m.transport) if err != nil { return nil, err } @@ -2733,7 +2733,7 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. message.LocalChatID = chatID message.OutgoingStatus = "" - clock, timestamp := chat.NextClockAndTimestamp() + clock, timestamp := chat.NextClockAndTimestamp(m.transport) message.Clock = clock message.Timestamp = timestamp message.WhisperTimestamp = timestamp @@ -2757,7 +2757,7 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. return nil, err } - err = chat.UpdateFromMessage(message) + err = chat.UpdateFromMessage(message, m.transport) if err != nil { return nil, err } @@ -2795,3 +2795,7 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. } return &response, nil } + +func (m *Messenger) getTimesource() ClockValueTimesource { + return m.transport +} diff --git a/vendor/github.com/status-im/status-go/protocol/transport/transport.go b/vendor/github.com/status-im/status-go/protocol/transport/transport.go index 9ec4c55dbd..6a75fe546b 100644 --- a/vendor/github.com/status-im/status-go/protocol/transport/transport.go +++ b/vendor/github.com/status-im/status-go/protocol/transport/transport.go @@ -16,6 +16,7 @@ type Transport interface { LeaveGroup(publicKeys []*ecdsa.PublicKey) error JoinPublic(chatID string) error LeavePublic(chatID string) error + GetCurrentTime() uint64 SendPublic(ctx context.Context, newMessage *types.NewMessage, chatName string) ([]byte, error) SendPrivateWithSharedSecret(ctx context.Context, newMessage *types.NewMessage, publicKey *ecdsa.PublicKey, secret []byte) ([]byte, error) diff --git a/vendor/github.com/status-im/status-go/protocol/transport/waku/waku_service.go b/vendor/github.com/status-im/status-go/protocol/transport/waku/waku_service.go index 57b3193822..92adec1af6 100644 --- a/vendor/github.com/status-im/status-go/protocol/transport/waku/waku_service.go +++ b/vendor/github.com/status-im/status-go/protocol/transport/waku/waku_service.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "database/sql" "sync" + "time" "github.com/pkg/errors" "go.uber.org/zap" @@ -360,6 +361,11 @@ func (a *WakuServiceTransport) Track(identifiers [][]byte, hash []byte, newMessa } } +// GetCurrentTime returns the current unix timestamp in milliseconds +func (a *WakuServiceTransport) GetCurrentTime() uint64 { + return uint64(a.waku.GetCurrentTime().UnixNano() / int64(time.Millisecond)) +} + func (a *WakuServiceTransport) Stop() error { if a.envelopesMonitor != nil { a.envelopesMonitor.Stop() diff --git a/vendor/github.com/status-im/status-go/protocol/transport/whisper/whisper_service.go b/vendor/github.com/status-im/status-go/protocol/transport/whisper/whisper_service.go index 38bc3a0a2c..2c54c367e9 100644 --- a/vendor/github.com/status-im/status-go/protocol/transport/whisper/whisper_service.go +++ b/vendor/github.com/status-im/status-go/protocol/transport/whisper/whisper_service.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "database/sql" "sync" + "time" "github.com/pkg/errors" "go.uber.org/zap" @@ -360,6 +361,11 @@ func (a *WhisperServiceTransport) Track(identifiers [][]byte, hash []byte, newMe } } +// GetCurrentTime returns the current unix timestamp in milliseconds +func (a *WhisperServiceTransport) GetCurrentTime() uint64 { + return uint64(a.shh.GetCurrentTime().UnixNano() / int64(time.Millisecond)) +} + func (a *WhisperServiceTransport) Stop() error { if a.envelopesMonitor != nil { a.envelopesMonitor.Stop() From 23a0e9266c0b51e318d2f34c47ff4b495c81b887 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Mon, 20 Jan 2020 20:08:57 +0100 Subject: [PATCH 7/9] v0.39.8 (#1808) --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 0c1df6ae2c..93605679e9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.39.7 +0.39.8 From 79b8112f89e218e5d24b6c4400eeaf6d4e8ec6db Mon Sep 17 00:00:00 2001 From: Adam Babik Date: Mon, 20 Jan 2020 21:56:06 +0100 Subject: [PATCH 8/9] Split shhext into shhext and wakuext (#1803) --- cmd/node-canary/main.go | 3 +- node/get_status_node.go | 31 +- node/geth_node.go | 6 +- protocol/messenger.go | 7 +- services/{shhext => ext}/README.md | 0 services/ext/api.go | 461 ++++++++++ services/ext/api_test.go | 156 ++++ services/{shhext => ext}/context.go | 4 +- services/ext/handler_mock.go | 48 + services/{shhext => ext}/mailrequests.go | 22 +- services/{shhext => ext}/mailrequests_test.go | 8 +- services/{shhext => ext}/mailservers/cache.go | 0 .../{shhext => ext}/mailservers/cache_test.go | 0 .../mailservers/connmanager.go | 14 +- .../mailservers/connmanager_test.go | 0 .../mailservers/connmonitor.go | 14 +- .../mailservers/connmonitor_test.go | 0 .../{shhext => ext}/mailservers/peerstore.go | 0 .../mailservers/peerstore_test.go | 0 services/{shhext => ext}/mailservers/utils.go | 0 .../{shhext => ext}/mailservers/utils_test.go | 0 services/ext/node_mock.go | 36 + services/{shhext => ext}/requests.go | 6 +- services/{shhext => ext}/requests_test.go | 2 +- services/{shhext => ext}/rpc.go | 2 +- services/ext/service.go | 441 ++++++++++ services/{shhext => ext}/signal.go | 5 +- services/shhext/api.go | 167 ---- services/shhext/api_geth.go | 650 +++----------- services/shhext/api_geth_test.go | 541 +++++++++--- services/shhext/context_geth.go | 14 - services/shhext/history.go | 19 - services/shhext/history_geth.go | 340 -------- services/shhext/history_geth_test.go | 360 -------- services/shhext/service.go | 424 +-------- services/shhext/service_nimbus.go | 390 +-------- services/shhext/service_test.go | 819 ------------------ services/shhext_wakuext_test.go | 69 ++ services/wakuext/api.go | 173 ++++ services/wakuext/api_test.go | 411 +++++++++ services/wakuext/service.go | 50 ++ signal/events_shhext.go | 3 - t/benchmarks/mailserver_test.go | 8 +- t/e2e/whisper/whisper_mailbox_test.go | 3 +- .../status-im/status-go/protocol/messenger.go | 7 +- .../status-im/status-go/whisper/v6/whisper.go | 4 - whisper/whisper.go | 4 - 47 files changed, 2499 insertions(+), 3223 deletions(-) rename services/{shhext => ext}/README.md (100%) create mode 100644 services/ext/api.go create mode 100644 services/ext/api_test.go rename services/{shhext => ext}/context.go (95%) create mode 100644 services/ext/handler_mock.go rename services/{shhext => ext}/mailrequests.go (85%) rename services/{shhext => ext}/mailrequests_test.go (95%) rename services/{shhext => ext}/mailservers/cache.go (100%) rename services/{shhext => ext}/mailservers/cache_test.go (100%) rename services/{shhext => ext}/mailservers/connmanager.go (94%) rename services/{shhext => ext}/mailservers/connmanager_test.go (100%) rename services/{shhext => ext}/mailservers/connmonitor.go (82%) rename services/{shhext => ext}/mailservers/connmonitor_test.go (100%) rename services/{shhext => ext}/mailservers/peerstore.go (100%) rename services/{shhext => ext}/mailservers/peerstore_test.go (100%) rename services/{shhext => ext}/mailservers/utils.go (100%) rename services/{shhext => ext}/mailservers/utils_test.go (100%) create mode 100644 services/ext/node_mock.go rename services/{shhext => ext}/requests.go (95%) rename services/{shhext => ext}/requests_test.go (99%) rename services/{shhext => ext}/rpc.go (99%) create mode 100644 services/ext/service.go rename services/{shhext => ext}/signal.go (92%) delete mode 100644 services/shhext/api.go delete mode 100644 services/shhext/context_geth.go delete mode 100644 services/shhext/history.go delete mode 100644 services/shhext/history_geth.go delete mode 100644 services/shhext/history_geth_test.go delete mode 100644 services/shhext/service_test.go create mode 100644 services/shhext_wakuext_test.go create mode 100644 services/wakuext/api.go create mode 100644 services/wakuext/api_test.go create mode 100644 services/wakuext/service.go diff --git a/cmd/node-canary/main.go b/cmd/node-canary/main.go index fbd91425e4..d0bdf35738 100644 --- a/cmd/node-canary/main.go +++ b/cmd/node-canary/main.go @@ -25,6 +25,7 @@ import ( "github.com/status-im/status-go/logutils" "github.com/status-im/status-go/params" "github.com/status-im/status-go/rpc" + "github.com/status-im/status-go/services/ext" "github.com/status-im/status-go/services/shhext" "github.com/status-im/status-go/t/helpers" ) @@ -170,7 +171,7 @@ func verifyMailserverBehavior(mailserverNode *enode.Node) { // request messages from mailbox shhextAPI := shhext.NewPublicAPI(clientShhExtService) requestIDBytes, err := shhextAPI.RequestMessages(context.TODO(), - shhext.MessagesRequest{ + ext.MessagesRequest{ MailServerPeer: mailserverNode.String(), From: uint32(clientWhisperService.GetCurrentTime().Add(-time.Duration(*period) * time.Second).Unix()), Limit: 1, diff --git a/node/get_status_node.go b/node/get_status_node.go index a946a3aa01..e4c434d086 100644 --- a/node/get_status_node.go +++ b/node/get_status_node.go @@ -25,8 +25,6 @@ import ( "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/status-im/status-go/whisper/v6" - "github.com/status-im/status-go/db" "github.com/status-im/status-go/discovery" "github.com/status-im/status-go/params" @@ -37,7 +35,10 @@ import ( "github.com/status-im/status-go/services/permissions" "github.com/status-im/status-go/services/shhext" "github.com/status-im/status-go/services/status" + "github.com/status-im/status-go/services/wakuext" "github.com/status-im/status-go/services/wallet" + "github.com/status-im/status-go/waku" + "github.com/status-im/status-go/whisper/v6" ) // tickerResolution is the delta to check blockchain sync progress. @@ -585,6 +586,19 @@ func (n *StatusNode) WhisperService() (w *whisper.Whisper, err error) { return } +// WakuService exposes reference to Whisper service running on top of the node +func (n *StatusNode) WakuService() (w *waku.Waku, err error) { + n.mu.RLock() + defer n.mu.RUnlock() + + err = n.gethService(&w) + if err == node.ErrServiceUnknown { + err = ErrServiceUnknown + } + + return +} + // ShhExtService exposes reference to shh extension service running on top of the node func (n *StatusNode) ShhExtService() (s *shhext.Service, err error) { n.mu.RLock() @@ -598,6 +612,19 @@ func (n *StatusNode) ShhExtService() (s *shhext.Service, err error) { return } +// WakuExtService exposes reference to shh extension service running on top of the node +func (n *StatusNode) WakuExtService() (s *wakuext.Service, err error) { + n.mu.RLock() + defer n.mu.RUnlock() + + err = n.gethService(&s) + if err == node.ErrServiceUnknown { + err = ErrServiceUnknown + } + + return +} + // WalletService returns wallet.Service instance if it was started. func (n *StatusNode) WalletService() (s *wallet.Service, err error) { n.mu.RLock() diff --git a/node/geth_node.go b/node/geth_node.go index f334bf880e..2066523d87 100644 --- a/node/geth_node.go +++ b/node/geth_node.go @@ -31,12 +31,14 @@ import ( "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/mailserver" "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/ext" "github.com/status-im/status-go/services/incentivisation" "github.com/status-im/status-go/services/nodebridge" "github.com/status-im/status-go/services/peer" "github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/shhext" "github.com/status-im/status-go/services/status" + "github.com/status-im/status-go/services/wakuext" "github.com/status-im/status-go/static" "github.com/status-im/status-go/timesource" "github.com/status-im/status-go/waku" @@ -365,7 +367,7 @@ func activateShhService(stack *node.Node, config *params.NodeConfig, db *leveldb if err := ctx.Service(ðnode); err != nil { return nil, err } - return shhext.New(ethnode.Node, ctx, "shhext", shhext.EnvelopeSignalHandler{}, db, config.ShhextConfig), nil + return shhext.New(config.ShhextConfig, ethnode.Node, ctx, ext.EnvelopeSignalHandler{}, db), nil }) } @@ -389,7 +391,7 @@ func activateWakuService(stack *node.Node, config *params.NodeConfig, db *leveld if err := ctx.Service(ðnode); err != nil { return nil, err } - return shhext.New(ethnode.Node, ctx, "wakuext", shhext.EnvelopeSignalHandler{}, db, config.ShhextConfig), nil + return wakuext.New(config.ShhextConfig, ethnode.Node, ctx, ext.EnvelopeSignalHandler{}, db), nil }) } diff --git a/protocol/messenger.go b/protocol/messenger.go index 4baec271eb..f0afa68e43 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -283,8 +283,7 @@ func NewMessenger( // Initialize transport layer. var transp transport.Transport - - if shh, err := node.GetWhisper(nil); err == nil { + if shh, err := node.GetWhisper(nil); err == nil && shh != nil { transp, err = shhtransp.NewWhisperServiceTransport( shh, identity, @@ -296,10 +295,10 @@ func NewMessenger( if err != nil { return nil, errors.Wrap(err, "failed to create WhisperServiceTransport") } - } else if err != nil { + } else { logger.Info("failed to find Whisper service; trying Waku", zap.Error(err)) waku, err := node.GetWaku(nil) - if err != nil { + if err != nil || waku == nil { return nil, errors.Wrap(err, "failed to find Whisper and Waku services") } transp, err = wakutransp.NewWakuServiceTransport( diff --git a/services/shhext/README.md b/services/ext/README.md similarity index 100% rename from services/shhext/README.md rename to services/ext/README.md diff --git a/services/ext/api.go b/services/ext/api.go new file mode 100644 index 0000000000..80fd0be8e8 --- /dev/null +++ b/services/ext/api.go @@ -0,0 +1,461 @@ +package ext + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + + "github.com/status-im/status-go/eth-node/types" + enstypes "github.com/status-im/status-go/eth-node/types/ens" + "github.com/status-im/status-go/mailserver" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/protocol" + "github.com/status-im/status-go/protocol/encryption/multidevice" + "github.com/status-im/status-go/protocol/transport" + "github.com/status-im/status-go/services/ext/mailservers" +) + +const ( + // defaultRequestTimeout is the default request timeout in seconds + defaultRequestTimeout = 10 + + // ensContractAddress is the address of the ENS resolver + ensContractAddress = "0x314159265dd8dbb310642f98f50c066173c1259b" +) + +var ( + // ErrInvalidMailServerPeer is returned when it fails to parse enode from params. + ErrInvalidMailServerPeer = errors.New("invalid mailServerPeer value") + // ErrInvalidSymKeyID is returned when it fails to get a symmetric key. + ErrInvalidSymKeyID = errors.New("invalid symKeyID value") + // ErrInvalidPublicKey is returned when public key can't be extracted + // from MailServer's nodeID. + ErrInvalidPublicKey = errors.New("can't extract public key") + // ErrPFSNotEnabled is returned when an endpoint PFS only is called but + // PFS is disabled + ErrPFSNotEnabled = errors.New("pfs not enabled") +) + +// ----- +// PAYLOADS +// ----- + +// MessagesRequest is a RequestMessages() request payload. +type MessagesRequest struct { + // MailServerPeer is MailServer's enode address. + MailServerPeer string `json:"mailServerPeer"` + + // From is a lower bound of time range (optional). + // Default is 24 hours back from now. + From uint32 `json:"from"` + + // To is a upper bound of time range (optional). + // Default is now. + To uint32 `json:"to"` + + // Limit determines the number of messages sent by the mail server + // for the current paginated request + Limit uint32 `json:"limit"` + + // Cursor is used as starting point for paginated requests + Cursor string `json:"cursor"` + + // Topic is a regular Whisper topic. + // DEPRECATED + Topic types.TopicType `json:"topic"` + + // Topics is a list of Whisper topics. + Topics []types.TopicType `json:"topics"` + + // SymKeyID is an ID of a symmetric key to authenticate to MailServer. + // It's derived from MailServer password. + SymKeyID string `json:"symKeyID"` + + // Timeout is the time to live of the request specified in seconds. + // Default is 10 seconds + Timeout time.Duration `json:"timeout"` + + // Force ensures that requests will bypass enforced delay. + Force bool `json:"force"` +} + +func (r *MessagesRequest) SetDefaults(now time.Time) { + // set From and To defaults + if r.To == 0 { + r.To = uint32(now.UTC().Unix()) + } + + if r.From == 0 { + oneDay := uint32(86400) // -24 hours + if r.To < oneDay { + r.From = 0 + } else { + r.From = r.To - oneDay + } + } + + if r.Timeout == 0 { + r.Timeout = defaultRequestTimeout + } +} + +// MessagesResponse is a response for requestMessages2 method. +type MessagesResponse struct { + // Cursor from the response can be used to retrieve more messages + // for the previous request. + Cursor string `json:"cursor"` + + // Error indicates that something wrong happened when sending messages + // to the requester. + Error error `json:"error"` +} + +// ----- +// PUBLIC API +// ----- + +// PublicAPI extends whisper public API. +type PublicAPI struct { + service *Service + eventSub mailservers.EnvelopeEventSubscriber + log log.Logger +} + +// NewPublicAPI returns instance of the public API. +func NewPublicAPI(s *Service, eventSub mailservers.EnvelopeEventSubscriber) *PublicAPI { + return &PublicAPI{ + service: s, + eventSub: eventSub, + log: log.New("package", "status-go/services/sshext.PublicAPI"), + } +} + +// RetryConfig specifies configuration for retries with timeout and max amount of retries. +type RetryConfig struct { + BaseTimeout time.Duration + // StepTimeout defines duration increase per each retry. + StepTimeout time.Duration + MaxRetries int +} + +func WaitForExpiredOrCompleted(requestID types.Hash, events chan types.EnvelopeEvent, timeout time.Duration) (*types.MailServerResponse, error) { + expired := fmt.Errorf("request %x expired", requestID) + after := time.NewTimer(timeout) + defer after.Stop() + for { + var ev types.EnvelopeEvent + select { + case ev = <-events: + case <-after.C: + return nil, expired + } + if ev.Hash != requestID { + continue + } + switch ev.Event { + case types.EventMailServerRequestCompleted: + data, ok := ev.Data.(*types.MailServerResponse) + if ok { + return data, nil + } + return nil, errors.New("invalid event data type") + case types.EventMailServerRequestExpired: + return nil, expired + } + } +} + +type Author struct { + PublicKey types.HexBytes `json:"publicKey"` + Alias string `json:"alias"` + Identicon string `json:"identicon"` +} + +type Metadata struct { + DedupID []byte `json:"dedupId"` + EncryptionID types.HexBytes `json:"encryptionId"` + MessageID types.HexBytes `json:"messageId"` + Author Author `json:"author"` +} + +// ConfirmMessagesProcessedByID is a method to confirm that messages was consumed by +// the client side. +// TODO: this is broken now as it requires dedup ID while a message hash should be used. +func (api *PublicAPI) ConfirmMessagesProcessedByID(messageConfirmations []*Metadata) error { + confirmationCount := len(messageConfirmations) + dedupIDs := make([][]byte, confirmationCount) + encryptionIDs := make([][]byte, confirmationCount) + for i, confirmation := range messageConfirmations { + dedupIDs[i] = confirmation.DedupID + encryptionIDs[i] = confirmation.EncryptionID + } + return api.service.ConfirmMessagesProcessed(encryptionIDs) +} + +// SendPublicMessage sends a public chat message to the underlying transport. +// Message's payload is a transit encoded message. +// It's important to call PublicAPI.afterSend() so that the client receives a signal +// with confirmation that the message left the device. +func (api *PublicAPI) SendPublicMessage(ctx context.Context, msg SendPublicMessageRPC) (types.HexBytes, error) { + chat := protocol.Chat{ + Name: msg.Chat, + } + return api.service.messenger.SendRaw(ctx, chat, msg.Payload) +} + +// SendDirectMessage sends a 1:1 chat message to the underlying transport +// Message's payload is a transit encoded message. +// It's important to call PublicAPI.afterSend() so that the client receives a signal +// with confirmation that the message left the device. +func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg SendDirectMessageRPC) (types.HexBytes, error) { + chat := protocol.Chat{ + ChatType: protocol.ChatTypeOneToOne, + ID: types.EncodeHex(msg.PubKey), + } + + return api.service.messenger.SendRaw(ctx, chat, msg.Payload) +} + +func (api *PublicAPI) Join(chat protocol.Chat) error { + return api.service.messenger.Join(chat) +} + +func (api *PublicAPI) Leave(chat protocol.Chat) error { + return api.service.messenger.Leave(chat) +} + +func (api *PublicAPI) LeaveGroupChat(ctx Context, chatID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.LeaveGroupChat(ctx, chatID) +} + +func (api *PublicAPI) CreateGroupChatWithMembers(ctx Context, name string, members []string) (*protocol.MessengerResponse, error) { + return api.service.messenger.CreateGroupChatWithMembers(ctx, name, members) +} + +func (api *PublicAPI) AddMembersToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AddMembersToGroupChat(ctx, chatID, members) +} + +func (api *PublicAPI) RemoveMemberFromGroupChat(ctx Context, chatID string, member string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RemoveMemberFromGroupChat(ctx, chatID, member) +} + +func (api *PublicAPI) AddAdminsToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AddAdminsToGroupChat(ctx, chatID, members) +} + +func (api *PublicAPI) ConfirmJoiningGroup(ctx context.Context, chatID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.ConfirmJoiningGroup(ctx, chatID) +} + +func (api *PublicAPI) LoadFilters(parent context.Context, chats []*transport.Filter) ([]*transport.Filter, error) { + return api.service.messenger.LoadFilters(chats) +} + +func (api *PublicAPI) SaveChat(parent context.Context, chat *protocol.Chat) error { + api.log.Info("saving chat", "chat", chat) + return api.service.messenger.SaveChat(chat) +} + +func (api *PublicAPI) Chats(parent context.Context) []*protocol.Chat { + return api.service.messenger.Chats() +} + +func (api *PublicAPI) DeleteChat(parent context.Context, chatID string) error { + return api.service.messenger.DeleteChat(chatID) +} + +func (api *PublicAPI) SaveContact(parent context.Context, contact *protocol.Contact) error { + return api.service.messenger.SaveContact(contact) +} + +func (api *PublicAPI) BlockContact(parent context.Context, contact *protocol.Contact) ([]*protocol.Chat, error) { + api.log.Info("blocking contact", "contact", contact.ID) + return api.service.messenger.BlockContact(contact) +} + +func (api *PublicAPI) Contacts(parent context.Context) []*protocol.Contact { + return api.service.messenger.Contacts() +} + +func (api *PublicAPI) RemoveFilters(parent context.Context, chats []*transport.Filter) error { + return api.service.messenger.RemoveFilters(chats) +} + +// EnableInstallation enables an installation for multi-device sync. +func (api *PublicAPI) EnableInstallation(installationID string) error { + return api.service.messenger.EnableInstallation(installationID) +} + +// DisableInstallation disables an installation for multi-device sync. +func (api *PublicAPI) DisableInstallation(installationID string) error { + return api.service.messenger.DisableInstallation(installationID) +} + +// GetOurInstallations returns all the installations available given an identity +func (api *PublicAPI) GetOurInstallations() []*multidevice.Installation { + return api.service.messenger.Installations() +} + +// SetInstallationMetadata sets the metadata for our own installation +func (api *PublicAPI) SetInstallationMetadata(installationID string, data *multidevice.InstallationMetadata) error { + return api.service.messenger.SetInstallationMetadata(installationID, data) +} + +// VerifyENSNames takes a list of ensdetails and returns whether they match the public key specified +func (api *PublicAPI) VerifyENSNames(details []enstypes.ENSDetails) (map[string]enstypes.ENSResponse, error) { + return api.service.messenger.VerifyENSNames(params.MainnetEthereumNetworkURL, ensContractAddress, details) +} + +type ApplicationMessagesResponse struct { + Messages []*protocol.Message `json:"messages"` + Cursor string `json:"cursor"` +} + +func (api *PublicAPI) ChatMessages(chatID, cursor string, limit int) (*ApplicationMessagesResponse, error) { + messages, cursor, err := api.service.messenger.MessageByChatID(chatID, cursor, limit) + if err != nil { + return nil, err + } + + return &ApplicationMessagesResponse{ + Messages: messages, + Cursor: cursor, + }, nil +} + +func (api *PublicAPI) DeleteMessage(id string) error { + return api.service.messenger.DeleteMessage(id) +} + +func (api *PublicAPI) DeleteMessagesByChatID(id string) error { + return api.service.messenger.DeleteMessagesByChatID(id) +} + +func (api *PublicAPI) MarkMessagesSeen(chatID string, ids []string) error { + return api.service.messenger.MarkMessagesSeen(chatID, ids) +} + +func (api *PublicAPI) UpdateMessageOutgoingStatus(id, newOutgoingStatus string) error { + return api.service.messenger.UpdateMessageOutgoingStatus(id, newOutgoingStatus) +} + +func (api *PublicAPI) SendChatMessage(ctx context.Context, message *protocol.Message) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendChatMessage(ctx, message) +} + +func (api *PublicAPI) ReSendChatMessage(ctx context.Context, messageID string) error { + return api.service.messenger.ReSendChatMessage(ctx, messageID) +} + +func (api *PublicAPI) RequestTransaction(ctx context.Context, chatID, value, contract, address string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RequestTransaction(ctx, chatID, value, contract, address) +} + +func (api *PublicAPI) RequestAddressForTransaction(ctx context.Context, chatID, from, value, contract string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RequestAddressForTransaction(ctx, chatID, from, value, contract) +} + +func (api *PublicAPI) DeclineRequestAddressForTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.DeclineRequestAddressForTransaction(ctx, messageID) +} + +func (api *PublicAPI) DeclineRequestTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.DeclineRequestTransaction(ctx, messageID) +} + +func (api *PublicAPI) AcceptRequestAddressForTransaction(ctx context.Context, messageID, address string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AcceptRequestAddressForTransaction(ctx, messageID, address) +} + +func (api *PublicAPI) SendTransaction(ctx context.Context, chatID, value, contract, transactionHash string, signature types.HexBytes) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendTransaction(ctx, chatID, value, contract, transactionHash, signature) +} + +func (api *PublicAPI) AcceptRequestTransaction(ctx context.Context, transactionHash, messageID string, signature types.HexBytes) (*protocol.MessengerResponse, error) { + return api.service.messenger.AcceptRequestTransaction(ctx, transactionHash, messageID, signature) +} + +func (api *PublicAPI) SendContactUpdates(ctx context.Context, name, picture string) error { + return api.service.messenger.SendContactUpdates(ctx, name, picture) +} + +func (api *PublicAPI) SendContactUpdate(ctx context.Context, contactID, name, picture string) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendContactUpdate(ctx, contactID, name, picture) +} + +func (api *PublicAPI) SendPairInstallation(ctx context.Context) (*protocol.MessengerResponse, error) { + return api.service.messenger.SendPairInstallation(ctx) +} + +func (api *PublicAPI) SyncDevices(ctx context.Context, name, picture string) error { + return api.service.messenger.SyncDevices(ctx, name, picture) +} + +// Echo is a method for testing purposes. +func (api *PublicAPI) Echo(ctx context.Context, message string) (string, error) { + return message, nil +} + +// ----- +// HELPER +// ----- + +// MakeMessagesRequestPayload makes a specific payload for MailServer +// to request historic messages. +// DEPRECATED +func MakeMessagesRequestPayload(r MessagesRequest) ([]byte, error) { + cursor, err := hex.DecodeString(r.Cursor) + if err != nil { + return nil, fmt.Errorf("invalid cursor: %v", err) + } + + if len(cursor) > 0 && len(cursor) != mailserver.CursorLength { + return nil, fmt.Errorf("invalid cursor size: expected %d but got %d", mailserver.CursorLength, len(cursor)) + } + + payload := mailserver.MessagesRequestPayload{ + Lower: r.From, + Upper: r.To, + Bloom: createBloomFilter(r), + Limit: r.Limit, + Cursor: cursor, + // Client must tell the MailServer if it supports batch responses. + // This can be removed in the future. + Batch: true, + } + + return rlp.EncodeToBytes(payload) +} + +func createBloomFilter(r MessagesRequest) []byte { + if len(r.Topics) > 0 { + return topicsToBloom(r.Topics...) + } + return types.TopicToBloom(r.Topic) +} + +func topicsToBloom(topics ...types.TopicType) []byte { + i := new(big.Int) + for _, topic := range topics { + bloom := types.TopicToBloom(topic) + i.Or(i, new(big.Int).SetBytes(bloom[:])) + } + + combined := make([]byte, types.BloomFilterSize) + data := i.Bytes() + copy(combined[types.BloomFilterSize-len(data):], data[:]) + + return combined +} + +// TopicsToBloom squashes all topics into a single bloom filter. +func TopicsToBloom(topics ...types.TopicType) []byte { + return topicsToBloom(topics...) +} diff --git a/services/ext/api_test.go b/services/ext/api_test.go new file mode 100644 index 0000000000..6d46c2ca9f --- /dev/null +++ b/services/ext/api_test.go @@ -0,0 +1,156 @@ +package ext + +import ( + "encoding/hex" + "fmt" + "testing" + "time" + + "github.com/status-im/status-go/eth-node/types" + + "github.com/status-im/status-go/mailserver" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMessagesRequest_setDefaults(t *testing.T) { + daysAgo := func(now time.Time, days int) uint32 { + return uint32(now.UTC().Add(-24 * time.Hour * time.Duration(days)).Unix()) + } + + tnow := time.Now() + now := uint32(tnow.UTC().Unix()) + yesterday := daysAgo(tnow, 1) + + scenarios := []struct { + given *MessagesRequest + expected *MessagesRequest + }{ + { + &MessagesRequest{From: 0, To: 0}, + &MessagesRequest{From: yesterday, To: now, Timeout: defaultRequestTimeout}, + }, + { + &MessagesRequest{From: 1, To: 0}, + &MessagesRequest{From: uint32(1), To: now, Timeout: defaultRequestTimeout}, + }, + { + &MessagesRequest{From: 0, To: yesterday}, + &MessagesRequest{From: daysAgo(tnow, 2), To: yesterday, Timeout: defaultRequestTimeout}, + }, + // 100 - 1 day would be invalid, so we set From to 0 + { + &MessagesRequest{From: 0, To: 100}, + &MessagesRequest{From: 0, To: 100, Timeout: defaultRequestTimeout}, + }, + // set Timeout + { + &MessagesRequest{From: 0, To: 0, Timeout: 100}, + &MessagesRequest{From: yesterday, To: now, Timeout: 100}, + }, + } + + for i, s := range scenarios { + t.Run(fmt.Sprintf("Scenario %d", i), func(t *testing.T) { + s.given.SetDefaults(tnow) + require.Equal(t, s.expected, s.given) + }) + } +} + +func TestMakeMessagesRequestPayload(t *testing.T) { + var emptyTopic types.TopicType + testCases := []struct { + Name string + Req MessagesRequest + Err string + }{ + { + Name: "empty cursor", + Req: MessagesRequest{Cursor: ""}, + Err: "", + }, + { + Name: "invalid cursor size", + Req: MessagesRequest{Cursor: hex.EncodeToString([]byte{0x01, 0x02, 0x03})}, + Err: fmt.Sprintf("invalid cursor size: expected %d but got 3", mailserver.CursorLength), + }, + { + Name: "valid cursor", + Req: MessagesRequest{ + Cursor: hex.EncodeToString(mailserver.NewDBKey(123, emptyTopic, types.Hash{}).Cursor()), + }, + Err: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + _, err := MakeMessagesRequestPayload(tc.Req) + if tc.Err == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.Err) + } + }) + } +} + +func TestTopicsToBloom(t *testing.T) { + t1 := stringToTopic("t1") + b1 := types.TopicToBloom(t1) + t2 := stringToTopic("t2") + b2 := types.TopicToBloom(t2) + t3 := stringToTopic("t3") + b3 := types.TopicToBloom(t3) + + reqBloom := topicsToBloom(t1) + assert.True(t, types.BloomFilterMatch(reqBloom, b1)) + assert.False(t, types.BloomFilterMatch(reqBloom, b2)) + assert.False(t, types.BloomFilterMatch(reqBloom, b3)) + + reqBloom = topicsToBloom(t1, t2) + assert.True(t, types.BloomFilterMatch(reqBloom, b1)) + assert.True(t, types.BloomFilterMatch(reqBloom, b2)) + assert.False(t, types.BloomFilterMatch(reqBloom, b3)) + + reqBloom = topicsToBloom(t1, t2, t3) + assert.True(t, types.BloomFilterMatch(reqBloom, b1)) + assert.True(t, types.BloomFilterMatch(reqBloom, b2)) + assert.True(t, types.BloomFilterMatch(reqBloom, b3)) +} + +func TestCreateBloomFilter(t *testing.T) { + t1 := stringToTopic("t1") + t2 := stringToTopic("t2") + + req := MessagesRequest{Topic: t1} + bloom := createBloomFilter(req) + assert.Equal(t, topicsToBloom(t1), bloom) + + req = MessagesRequest{Topics: []types.TopicType{t1, t2}} + bloom = createBloomFilter(req) + assert.Equal(t, topicsToBloom(t1, t2), bloom) +} + +func stringToTopic(s string) types.TopicType { + return types.BytesToTopic([]byte(s)) +} + +func TestExpiredOrCompleted(t *testing.T) { + timeout := time.Millisecond + events := make(chan types.EnvelopeEvent) + errors := make(chan error, 1) + hash := types.Hash{1} + go func() { + _, err := WaitForExpiredOrCompleted(hash, events, timeout) + errors <- err + }() + select { + case <-time.After(time.Second): + require.FailNow(t, "timed out waiting for waitForExpiredOrCompleted to complete") + case err := <-errors: + require.EqualError(t, err, fmt.Sprintf("request %x expired", hash)) + } +} diff --git a/services/shhext/context.go b/services/ext/context.go similarity index 95% rename from services/shhext/context.go rename to services/ext/context.go index 037857f681..358b197a38 100644 --- a/services/shhext/context.go +++ b/services/ext/context.go @@ -1,4 +1,4 @@ -package shhext +package ext import ( "context" @@ -7,7 +7,7 @@ import ( "github.com/status-im/status-go/db" ) -// ContextKey is a type used for keys in shhext Context. +// ContextKey is a type used for keys in ext Context. type ContextKey struct { Name string } diff --git a/services/ext/handler_mock.go b/services/ext/handler_mock.go new file mode 100644 index 0000000000..4f8ef0907f --- /dev/null +++ b/services/ext/handler_mock.go @@ -0,0 +1,48 @@ +package ext + +import ( + "github.com/status-im/status-go/eth-node/types" +) + +type failureMessage struct { + IDs [][]byte + Error error +} + +func NewHandlerMock(buf int) HandlerMock { + return HandlerMock{ + confirmations: make(chan [][]byte, buf), + expirations: make(chan failureMessage, buf), + requestsCompleted: make(chan types.Hash, buf), + requestsExpired: make(chan types.Hash, buf), + requestsFailed: make(chan types.Hash, buf), + } +} + +type HandlerMock struct { + confirmations chan [][]byte + expirations chan failureMessage + requestsCompleted chan types.Hash + requestsExpired chan types.Hash + requestsFailed chan types.Hash +} + +func (t HandlerMock) EnvelopeSent(ids [][]byte) { + t.confirmations <- ids +} + +func (t HandlerMock) EnvelopeExpired(ids [][]byte, err error) { + t.expirations <- failureMessage{IDs: ids, Error: err} +} + +func (t HandlerMock) MailServerRequestCompleted(requestID types.Hash, lastEnvelopeHash types.Hash, cursor []byte, err error) { + if err == nil { + t.requestsCompleted <- requestID + } else { + t.requestsFailed <- requestID + } +} + +func (t HandlerMock) MailServerRequestExpired(hash types.Hash) { + t.requestsExpired <- hash +} diff --git a/services/shhext/mailrequests.go b/services/ext/mailrequests.go similarity index 85% rename from services/shhext/mailrequests.go rename to services/ext/mailrequests.go index c2a8819a91..39fc073544 100644 --- a/services/shhext/mailrequests.go +++ b/services/ext/mailrequests.go @@ -1,6 +1,6 @@ // +build !nimbus -package shhext +package ext import ( "sync" @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/services/ext/mailservers" ) // EnvelopeState in local tracker @@ -16,18 +17,14 @@ type EnvelopeState int const ( // NotRegistered returned if asked hash wasn't registered in the tracker. NotRegistered EnvelopeState = -1 - // EnvelopePosted is set when envelope was added to a local whisper queue. - EnvelopePosted EnvelopeState = iota - // EnvelopeSent is set when envelope is sent to atleast one peer. - EnvelopeSent // MailServerRequestSent is set when p2p request is sent to the mailserver MailServerRequestSent ) // MailRequestMonitor is responsible for monitoring history request to mailservers. type MailRequestMonitor struct { - w types.Whisper - handler EnvelopeEventsHandler + eventSub mailservers.EnvelopeEventSubscriber + handler EnvelopeEventsHandler mu sync.Mutex cache map[types.Hash]EnvelopeState @@ -38,6 +35,15 @@ type MailRequestMonitor struct { quit chan struct{} } +func NewMailRequestMonitor(eventSub mailservers.EnvelopeEventSubscriber, h EnvelopeEventsHandler, reg *RequestsRegistry) *MailRequestMonitor { + return &MailRequestMonitor{ + eventSub: eventSub, + handler: h, + cache: make(map[types.Hash]EnvelopeState), + requestsRegistry: reg, + } +} + // Start processing events. func (m *MailRequestMonitor) Start() { m.quit = make(chan struct{}) @@ -67,7 +73,7 @@ func (m *MailRequestMonitor) GetState(hash types.Hash) EnvelopeState { // handleEnvelopeEvents processes whisper envelope events func (m *MailRequestMonitor) handleEnvelopeEvents() { events := make(chan types.EnvelopeEvent, 100) // must be buffered to prevent blocking whisper - sub := m.w.SubscribeEnvelopeEvents(events) + sub := m.eventSub.SubscribeEnvelopeEvents(events) defer sub.Unsubscribe() for { select { diff --git a/services/shhext/mailrequests_test.go b/services/ext/mailrequests_test.go similarity index 95% rename from services/shhext/mailrequests_test.go rename to services/ext/mailrequests_test.go index b6901b860d..2c7b5815fa 100644 --- a/services/shhext/mailrequests_test.go +++ b/services/ext/mailrequests_test.go @@ -1,6 +1,6 @@ // +build !nimbus -package shhext +package ext import ( "errors" @@ -34,7 +34,7 @@ func (s *MailRequestMonitorSuite) SetupTest() { } func (s *MailRequestMonitorSuite) TestRequestCompleted() { - mock := newHandlerMock(1) + mock := NewHandlerMock(1) s.monitor.handler = mock s.monitor.cache[testHash] = MailServerRequestSent s.monitor.handleEvent(types.EnvelopeEvent{ @@ -52,7 +52,7 @@ func (s *MailRequestMonitorSuite) TestRequestCompleted() { } func (s *MailRequestMonitorSuite) TestRequestFailed() { - mock := newHandlerMock(1) + mock := NewHandlerMock(1) s.monitor.handler = mock s.monitor.cache[testHash] = MailServerRequestSent s.monitor.handleEvent(types.EnvelopeEvent{ @@ -70,7 +70,7 @@ func (s *MailRequestMonitorSuite) TestRequestFailed() { } func (s *MailRequestMonitorSuite) TestRequestExpiration() { - mock := newHandlerMock(1) + mock := NewHandlerMock(1) s.monitor.handler = mock s.monitor.cache[testHash] = MailServerRequestSent s.monitor.handleEvent(types.EnvelopeEvent{ diff --git a/services/shhext/mailservers/cache.go b/services/ext/mailservers/cache.go similarity index 100% rename from services/shhext/mailservers/cache.go rename to services/ext/mailservers/cache.go diff --git a/services/shhext/mailservers/cache_test.go b/services/ext/mailservers/cache_test.go similarity index 100% rename from services/shhext/mailservers/cache_test.go rename to services/ext/mailservers/cache_test.go diff --git a/services/shhext/mailservers/connmanager.go b/services/ext/mailservers/connmanager.go similarity index 94% rename from services/shhext/mailservers/connmanager.go rename to services/ext/mailservers/connmanager.go index 266fdfcaa4..f4c5bf3fe8 100644 --- a/services/shhext/mailservers/connmanager.go +++ b/services/ext/mailservers/connmanager.go @@ -14,7 +14,7 @@ import ( const ( peerEventsBuffer = 10 // sufficient buffer to avoid blocking a p2p feed. - whisperEventsBuffer = 20 // sufficient buffer to avod blocking a whisper envelopes feed. + whisperEventsBuffer = 20 // sufficient buffer to avod blocking a eventSub envelopes feed. ) // PeerAdderRemover is an interface for adding or removing peers. @@ -39,10 +39,10 @@ type p2pServer interface { } // NewConnectionManager creates an instance of ConnectionManager. -func NewConnectionManager(server p2pServer, whisper EnvelopeEventSubscriber, target, maxFailures int, timeout time.Duration) *ConnectionManager { +func NewConnectionManager(server p2pServer, eventSub EnvelopeEventSubscriber, target, maxFailures int, timeout time.Duration) *ConnectionManager { return &ConnectionManager{ server: server, - whisper: whisper, + eventSub: eventSub, connectedTarget: target, maxFailures: maxFailures, notifications: make(chan []*enode.Node), @@ -55,8 +55,8 @@ type ConnectionManager struct { wg sync.WaitGroup quit chan struct{} - server p2pServer - whisper EnvelopeEventSubscriber + server p2pServer + eventSub EnvelopeEventSubscriber notifications chan []*enode.Node connectedTarget int @@ -86,7 +86,7 @@ func (ps *ConnectionManager) Start() { events := make(chan *p2p.PeerEvent, peerEventsBuffer) sub := ps.server.SubscribeEvents(events) whisperEvents := make(chan types.EnvelopeEvent, whisperEventsBuffer) - whisperSub := ps.whisper.SubscribeEnvelopeEvents(whisperEvents) + whisperSub := ps.eventSub.SubscribeEnvelopeEvents(whisperEvents) requests := map[types.Hash]struct{}{} failuresPerServer := map[types.EnodeID]int{} @@ -101,7 +101,7 @@ func (ps *ConnectionManager) Start() { log.Error("retry after error subscribing to p2p events", "error", err) return case err := <-whisperSub.Err(): - log.Error("retry after error suscribing to whisper events", "error", err) + log.Error("retry after error suscribing to eventSub events", "error", err) return case newNodes := <-ps.notifications: state.processReplacement(newNodes, events) diff --git a/services/shhext/mailservers/connmanager_test.go b/services/ext/mailservers/connmanager_test.go similarity index 100% rename from services/shhext/mailservers/connmanager_test.go rename to services/ext/mailservers/connmanager_test.go diff --git a/services/shhext/mailservers/connmonitor.go b/services/ext/mailservers/connmonitor.go similarity index 82% rename from services/shhext/mailservers/connmonitor.go rename to services/ext/mailservers/connmonitor.go index d263f22fc9..8d9bd6f404 100644 --- a/services/shhext/mailservers/connmonitor.go +++ b/services/ext/mailservers/connmonitor.go @@ -10,11 +10,11 @@ import ( ) // NewLastUsedConnectionMonitor returns pointer to the instance of LastUsedConnectionMonitor. -func NewLastUsedConnectionMonitor(ps *PeerStore, cache *Cache, whisper EnvelopeEventSubscriber) *LastUsedConnectionMonitor { +func NewLastUsedConnectionMonitor(ps *PeerStore, cache *Cache, eventSub EnvelopeEventSubscriber) *LastUsedConnectionMonitor { return &LastUsedConnectionMonitor{ - ps: ps, - cache: cache, - whisper: whisper, + ps: ps, + cache: cache, + eventSub: eventSub, } } @@ -23,7 +23,7 @@ type LastUsedConnectionMonitor struct { ps *PeerStore cache *Cache - whisper EnvelopeEventSubscriber + eventSub EnvelopeEventSubscriber quit chan struct{} wg sync.WaitGroup @@ -35,7 +35,7 @@ func (mon *LastUsedConnectionMonitor) Start() { mon.wg.Add(1) go func() { events := make(chan types.EnvelopeEvent, whisperEventsBuffer) - sub := mon.whisper.SubscribeEnvelopeEvents(events) + sub := mon.eventSub.SubscribeEnvelopeEvents(events) defer sub.Unsubscribe() defer mon.wg.Done() for { @@ -43,7 +43,7 @@ func (mon *LastUsedConnectionMonitor) Start() { case <-mon.quit: return case err := <-sub.Err(): - log.Error("retry after error suscribing to whisper events", "error", err) + log.Error("retry after error suscribing to eventSub events", "error", err) return case ev := <-events: node := mon.ps.Get(ev.Peer) diff --git a/services/shhext/mailservers/connmonitor_test.go b/services/ext/mailservers/connmonitor_test.go similarity index 100% rename from services/shhext/mailservers/connmonitor_test.go rename to services/ext/mailservers/connmonitor_test.go diff --git a/services/shhext/mailservers/peerstore.go b/services/ext/mailservers/peerstore.go similarity index 100% rename from services/shhext/mailservers/peerstore.go rename to services/ext/mailservers/peerstore.go diff --git a/services/shhext/mailservers/peerstore_test.go b/services/ext/mailservers/peerstore_test.go similarity index 100% rename from services/shhext/mailservers/peerstore_test.go rename to services/ext/mailservers/peerstore_test.go diff --git a/services/shhext/mailservers/utils.go b/services/ext/mailservers/utils.go similarity index 100% rename from services/shhext/mailservers/utils.go rename to services/ext/mailservers/utils.go diff --git a/services/shhext/mailservers/utils_test.go b/services/ext/mailservers/utils_test.go similarity index 100% rename from services/shhext/mailservers/utils_test.go rename to services/ext/mailservers/utils_test.go diff --git a/services/ext/node_mock.go b/services/ext/node_mock.go new file mode 100644 index 0000000000..2eecbef13f --- /dev/null +++ b/services/ext/node_mock.go @@ -0,0 +1,36 @@ +package ext + +import ( + "github.com/status-im/status-go/eth-node/types" + enstypes "github.com/status-im/status-go/eth-node/types/ens" + "go.uber.org/zap" +) + +type TestNodeWrapper struct { + whisper types.Whisper + waku types.Waku +} + +func NewTestNodeWrapper(whisper types.Whisper, waku types.Waku) *TestNodeWrapper { + return &TestNodeWrapper{whisper: whisper, waku: waku} +} + +func (w *TestNodeWrapper) NewENSVerifier(_ *zap.Logger) enstypes.ENSVerifier { + panic("not implemented") +} + +func (w *TestNodeWrapper) GetWhisper(_ interface{}) (types.Whisper, error) { + return w.whisper, nil +} + +func (w *TestNodeWrapper) GetWaku(_ interface{}) (types.Waku, error) { + return w.waku, nil +} + +func (w *TestNodeWrapper) AddPeer(url string) error { + panic("not implemented") +} + +func (w *TestNodeWrapper) RemovePeer(url string) error { + panic("not implemented") +} diff --git a/services/shhext/requests.go b/services/ext/requests.go similarity index 95% rename from services/shhext/requests.go rename to services/ext/requests.go index ac6596be4a..f138325a53 100644 --- a/services/shhext/requests.go +++ b/services/ext/requests.go @@ -1,4 +1,4 @@ -package shhext +package ext import ( "fmt" @@ -10,8 +10,8 @@ import ( ) const ( - // defaultRequestsDelay will be used in RequestsRegistry if no other was provided. - defaultRequestsDelay = 3 * time.Second + // DefaultRequestsDelay will be used in RequestsRegistry if no other was provided. + DefaultRequestsDelay = 3 * time.Second ) type requestMeta struct { diff --git a/services/shhext/requests_test.go b/services/ext/requests_test.go similarity index 99% rename from services/shhext/requests_test.go rename to services/ext/requests_test.go index e6b8765388..e0b6e0445d 100644 --- a/services/shhext/requests_test.go +++ b/services/ext/requests_test.go @@ -1,4 +1,4 @@ -package shhext +package ext import ( "testing" diff --git a/services/shhext/rpc.go b/services/ext/rpc.go similarity index 99% rename from services/shhext/rpc.go rename to services/ext/rpc.go index 951b9f6639..73eb97d8a6 100644 --- a/services/shhext/rpc.go +++ b/services/ext/rpc.go @@ -1,7 +1,7 @@ // TODO: These types should be defined using protobuf, but protoc can only emit []byte instead of types.HexBytes, // which causes issues when marshaling to JSON on the react side. Let's do that once the chat protocol is moved to the go repo. -package shhext +package ext import ( "crypto/ecdsa" diff --git a/services/ext/service.go b/services/ext/service.go new file mode 100644 index 0000000000..12b9bc51a2 --- /dev/null +++ b/services/ext/service.go @@ -0,0 +1,441 @@ +package ext + +import ( + "context" + "crypto/ecdsa" + "database/sql" + "math/big" + "os" + "path/filepath" + "time" + + "github.com/status-im/status-go/services/wallet" + + "github.com/syndtr/goleveldb/leveldb" + + "github.com/status-im/status-go/logutils" + + commongethtypes "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/status-im/status-go/db" + "github.com/status-im/status-go/multiaccounts/accounts" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/ext/mailservers" + "github.com/status-im/status-go/signal" + + "go.uber.org/zap" + + coretypes "github.com/status-im/status-go/eth-node/core/types" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol" + "github.com/status-im/status-go/protocol/transport" +) + +const ( + // defaultConnectionsTarget used in Service.Start if configured connection target is 0. + defaultConnectionsTarget = 1 + // defaultTimeoutWaitAdded is a timeout to use to establish initial connections. + defaultTimeoutWaitAdded = 5 * time.Second +) + +// EnvelopeEventsHandler used for two different event types. +type EnvelopeEventsHandler interface { + EnvelopeSent([][]byte) + EnvelopeExpired([][]byte, error) + MailServerRequestCompleted(types.Hash, types.Hash, []byte, error) + MailServerRequestExpired(types.Hash) +} + +// Service is a service that provides some additional API to whisper-based protocols like Whisper or Waku. +type Service struct { + messenger *protocol.Messenger + identity *ecdsa.PrivateKey + cancelMessenger chan struct{} + storage db.TransactionalStorage + n types.Node + config params.ShhextConfig + mailMonitor *MailRequestMonitor + requestsRegistry *RequestsRegistry + server *p2p.Server + eventSub mailservers.EnvelopeEventSubscriber + peerStore *mailservers.PeerStore + cache *mailservers.Cache + connManager *mailservers.ConnectionManager + lastUsedMonitor *mailservers.LastUsedConnectionMonitor + accountsDB *accounts.Database +} + +// Make sure that Service implements node.Service interface. +var _ node.Service = (*Service)(nil) + +func New( + config params.ShhextConfig, + n types.Node, + ldb *leveldb.DB, + mailMonitor *MailRequestMonitor, + reqRegistry *RequestsRegistry, + eventSub mailservers.EnvelopeEventSubscriber, +) *Service { + cache := mailservers.NewCache(ldb) + peerStore := mailservers.NewPeerStore(cache) + return &Service{ + storage: db.NewLevelDBStorage(ldb), + n: n, + config: config, + mailMonitor: mailMonitor, + requestsRegistry: reqRegistry, + peerStore: peerStore, + cache: mailservers.NewCache(ldb), + eventSub: eventSub, + } +} + +func (s *Service) NodeID() *ecdsa.PrivateKey { + if s.server == nil { + return nil + } + return s.server.PrivateKey +} + +func (s *Service) RequestsRegistry() *RequestsRegistry { + return s.requestsRegistry +} + +func (s *Service) GetPeer(rawURL string) (*enode.Node, error) { + if len(rawURL) == 0 { + return mailservers.GetFirstConnected(s.server, s.peerStore) + } + return enode.ParseV4(rawURL) +} + +func (s *Service) InitProtocol(identity *ecdsa.PrivateKey, db *sql.DB) error { // nolint: gocyclo + if !s.config.PFSEnabled { + return nil + } + + // If Messenger has been already set up, we need to shut it down + // before we init it again. Otherwise, it will lead to goroutines leakage + // due to not stopped filters. + if s.messenger != nil { + if err := s.messenger.Shutdown(); err != nil { + return err + } + } + + s.identity = identity + + dataDir := filepath.Clean(s.config.BackupDisabledDataDir) + + if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { + return err + } + + // Create a custom zap.Logger which will forward logs from status-go/protocol to status-go logger. + zapLogger, err := logutils.NewZapLoggerWithAdapter(logutils.Logger()) + if err != nil { + return err + } + + envelopesMonitorConfig := &transport.EnvelopesMonitorConfig{ + MaxAttempts: s.config.MaxMessageDeliveryAttempts, + MailserverConfirmationsEnabled: s.config.MailServerConfirmations, + IsMailserver: func(peer types.EnodeID) bool { + return s.peerStore.Exist(peer) + }, + EnvelopeEventsHandler: EnvelopeSignalHandler{}, + Logger: zapLogger, + } + options := buildMessengerOptions(s.config, db, envelopesMonitorConfig, zapLogger) + + messenger, err := protocol.NewMessenger( + identity, + s.n, + s.config.InstallationID, + options..., + ) + if err != nil { + return err + } + s.accountsDB = accounts.NewDB(db) + s.messenger = messenger + // Start a loop that retrieves all messages and propagates them to status-react. + s.cancelMessenger = make(chan struct{}) + go s.retrieveMessagesLoop(time.Second, s.cancelMessenger) + go s.verifyTransactionLoop(30*time.Second, s.cancelMessenger) + + return s.messenger.Init() +} + +func (s *Service) retrieveMessagesLoop(tick time.Duration, cancel <-chan struct{}) { + ticker := time.NewTicker(tick) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + response, err := s.messenger.RetrieveAll() + if err != nil { + log.Error("failed to retrieve raw messages", "err", err) + continue + } + if !response.IsEmpty() { + PublisherSignalHandler{}.NewMessages(response) + } + case <-cancel: + return + } + } +} + +type verifyTransactionClient struct { + chainID *big.Int + url string +} + +func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) { + signer := gethtypes.NewEIP155Signer(c.chainID) + client, err := ethclient.Dial(c.url) + if err != nil { + return coretypes.Message{}, coretypes.TransactionStatusPending, err + } + + transaction, pending, err := client.TransactionByHash(ctx, commongethtypes.BytesToHash(hash.Bytes())) + if err != nil { + return coretypes.Message{}, coretypes.TransactionStatusPending, err + } + + message, err := transaction.AsMessage(signer) + if err != nil { + return coretypes.Message{}, coretypes.TransactionStatusPending, err + } + from := types.BytesToAddress(message.From().Bytes()) + to := types.BytesToAddress(message.To().Bytes()) + + if pending { + return coretypes.NewMessage( + from, + &to, + message.Nonce(), + message.Value(), + message.Gas(), + message.GasPrice(), + message.Data(), + message.CheckNonce(), + ), coretypes.TransactionStatusPending, nil + } + + receipt, err := client.TransactionReceipt(ctx, commongethtypes.BytesToHash(hash.Bytes())) + if err != nil { + return coretypes.Message{}, coretypes.TransactionStatusPending, err + } + + coremessage := coretypes.NewMessage( + from, + &to, + message.Nonce(), + message.Value(), + message.Gas(), + message.GasPrice(), + message.Data(), + message.CheckNonce(), + ) + + // Token transfer, check the logs + if len(coremessage.Data()) != 0 { + if wallet.IsTokenTransfer(receipt.Logs) { + return coremessage, coretypes.TransactionStatus(receipt.Status), nil + } + return coremessage, coretypes.TransactionStatusFailed, nil + } + + return coremessage, coretypes.TransactionStatus(receipt.Status), nil +} + +func (s *Service) verifyTransactionLoop(tick time.Duration, cancel <-chan struct{}) { + if s.config.VerifyTransactionURL == "" { + log.Warn("not starting transaction loop") + return + } + + ticker := time.NewTicker(tick) + defer ticker.Stop() + + ctx, cancelVerifyTransaction := context.WithCancel(context.Background()) + + for { + select { + case <-ticker.C: + accounts, err := s.accountsDB.GetAccounts() + if err != nil { + log.Error("failed to retrieve accounts", "err", err) + } + var wallets []types.Address + for _, account := range accounts { + if account.Wallet { + wallets = append(wallets, types.BytesToAddress(account.Address.Bytes())) + } + } + + response, err := s.messenger.ValidateTransactions(ctx, wallets) + if err != nil { + log.Error("failed to validate transactions", "err", err) + continue + } + if !response.IsEmpty() { + PublisherSignalHandler{}.NewMessages(response) + } + case <-cancel: + cancelVerifyTransaction() + return + } + } +} + +func (s *Service) ConfirmMessagesProcessed(messageIDs [][]byte) error { + return s.messenger.ConfirmMessagesProcessed(messageIDs) +} + +func (s *Service) EnableInstallation(installationID string) error { + return s.messenger.EnableInstallation(installationID) +} + +// DisableInstallation disables an installation for multi-device sync. +func (s *Service) DisableInstallation(installationID string) error { + return s.messenger.DisableInstallation(installationID) +} + +// UpdateMailservers updates information about selected mail servers. +func (s *Service) UpdateMailservers(nodes []*enode.Node) error { + if err := s.peerStore.Update(nodes); err != nil { + return err + } + if s.connManager != nil { + s.connManager.Notify(nodes) + } + return nil +} + +// Protocols returns a new protocols list. In this case, there are none. +func (s *Service) Protocols() []p2p.Protocol { + return []p2p.Protocol{} +} + +// APIs returns a list of new APIs. +func (s *Service) APIs() []rpc.API { + panic("this is abstract service, use shhext or wakuext implementation") +} + +// Start is run when a service is started. +// It does nothing in this case but is required by `node.Service` interface. +func (s *Service) Start(server *p2p.Server) error { + if s.config.EnableConnectionManager { + connectionsTarget := s.config.ConnectionTarget + if connectionsTarget == 0 { + connectionsTarget = defaultConnectionsTarget + } + maxFailures := s.config.MaxServerFailures + // if not defined change server on first expired event + if maxFailures == 0 { + maxFailures = 1 + } + s.connManager = mailservers.NewConnectionManager(server, s.eventSub, connectionsTarget, maxFailures, defaultTimeoutWaitAdded) + s.connManager.Start() + if err := mailservers.EnsureUsedRecordsAddedFirst(s.peerStore, s.connManager); err != nil { + return err + } + } + if s.config.EnableLastUsedMonitor { + s.lastUsedMonitor = mailservers.NewLastUsedConnectionMonitor(s.peerStore, s.cache, s.eventSub) + s.lastUsedMonitor.Start() + } + s.mailMonitor.Start() + s.server = server + return nil +} + +// Stop is run when a service is stopped. +func (s *Service) Stop() error { + log.Info("Stopping shhext service") + if s.config.EnableConnectionManager { + s.connManager.Stop() + } + if s.config.EnableLastUsedMonitor { + s.lastUsedMonitor.Stop() + } + s.requestsRegistry.Clear() + s.mailMonitor.Stop() + + if s.cancelMessenger != nil { + select { + case <-s.cancelMessenger: + // channel already closed + default: + close(s.cancelMessenger) + s.cancelMessenger = nil + } + } + + if s.messenger != nil { + if err := s.messenger.Shutdown(); err != nil { + return err + } + } + + return nil +} + +func onNegotiatedFilters(filters []*transport.Filter) { + var signalFilters []*signal.Filter + for _, filter := range filters { + + signalFilter := &signal.Filter{ + ChatID: filter.ChatID, + SymKeyID: filter.SymKeyID, + Listen: filter.Listen, + FilterID: filter.FilterID, + Identity: filter.Identity, + Topic: filter.Topic, + } + + signalFilters = append(signalFilters, signalFilter) + } + if len(filters) != 0 { + handler := PublisherSignalHandler{} + handler.FilterAdded(signalFilters) + } +} + +func buildMessengerOptions( + config params.ShhextConfig, + db *sql.DB, + envelopesMonitorConfig *transport.EnvelopesMonitorConfig, + logger *zap.Logger, +) []protocol.Option { + options := []protocol.Option{ + protocol.WithCustomLogger(logger), + protocol.WithDatabase(db), + protocol.WithEnvelopesMonitorConfig(envelopesMonitorConfig), + protocol.WithOnNegotiatedFilters(onNegotiatedFilters), + } + + if config.DataSyncEnabled { + options = append(options, protocol.WithDatasync()) + } + + if config.VerifyTransactionURL != "" { + client := &verifyTransactionClient{ + url: config.VerifyTransactionURL, + chainID: big.NewInt(config.VerifyTransactionChainID), + } + options = append(options, protocol.WithVerifyTransactionClient(client)) + } + + return options +} diff --git a/services/shhext/signal.go b/services/ext/signal.go similarity index 92% rename from services/shhext/signal.go rename to services/ext/signal.go index 944330c44a..6c4fc7f368 100644 --- a/services/shhext/signal.go +++ b/services/ext/signal.go @@ -1,4 +1,4 @@ -package shhext +package ext import ( "github.com/status-im/status-go/eth-node/types" @@ -40,7 +40,8 @@ func (h PublisherSignalHandler) BundleAdded(identity string, installationID stri signal.SendBundleAdded(identity, installationID) } -func (h PublisherSignalHandler) WhisperFilterAdded(filters []*signal.Filter) { +func (h PublisherSignalHandler) FilterAdded(filters []*signal.Filter) { + // TODO(waku): change the name of the filter to generic one. signal.SendWhisperFilterAdded(filters) } diff --git a/services/shhext/api.go b/services/shhext/api.go deleted file mode 100644 index 645f8eea6b..0000000000 --- a/services/shhext/api.go +++ /dev/null @@ -1,167 +0,0 @@ -package shhext - -import ( - "errors" - "time" - - "github.com/status-im/status-go/eth-node/types" -) - -const ( - // defaultWorkTime is a work time reported in messages sent to MailServer nodes. - defaultWorkTime = 5 - // defaultRequestTimeout is the default request timeout in seconds - defaultRequestTimeout = 10 - - // ensContractAddress is the address of the ENS resolver - ensContractAddress = "0x314159265dd8dbb310642f98f50c066173c1259b" -) - -var ( - // ErrInvalidMailServerPeer is returned when it fails to parse enode from params. - ErrInvalidMailServerPeer = errors.New("invalid mailServerPeer value") - // ErrInvalidSymKeyID is returned when it fails to get a symmetric key. - ErrInvalidSymKeyID = errors.New("invalid symKeyID value") - // ErrInvalidPublicKey is returned when public key can't be extracted - // from MailServer's nodeID. - ErrInvalidPublicKey = errors.New("can't extract public key") - // ErrPFSNotEnabled is returned when an endpoint PFS only is called but - // PFS is disabled - ErrPFSNotEnabled = errors.New("pfs not enabled") -) - -// ----- -// PAYLOADS -// ----- - -// MessagesRequest is a RequestMessages() request payload. -type MessagesRequest struct { - // MailServerPeer is MailServer's enode address. - MailServerPeer string `json:"mailServerPeer"` - - // From is a lower bound of time range (optional). - // Default is 24 hours back from now. - From uint32 `json:"from"` - - // To is a upper bound of time range (optional). - // Default is now. - To uint32 `json:"to"` - - // Limit determines the number of messages sent by the mail server - // for the current paginated request - Limit uint32 `json:"limit"` - - // Cursor is used as starting point for paginated requests - Cursor string `json:"cursor"` - - // Topic is a regular Whisper topic. - // DEPRECATED - Topic types.TopicType `json:"topic"` - - // Topics is a list of Whisper topics. - Topics []types.TopicType `json:"topics"` - - // SymKeyID is an ID of a symmetric key to authenticate to MailServer. - // It's derived from MailServer password. - SymKeyID string `json:"symKeyID"` - - // Timeout is the time to live of the request specified in seconds. - // Default is 10 seconds - Timeout time.Duration `json:"timeout"` - - // Force ensures that requests will bypass enforced delay. - Force bool `json:"force"` -} - -func (r *MessagesRequest) setDefaults(now time.Time) { - // set From and To defaults - if r.To == 0 { - r.To = uint32(now.UTC().Unix()) - } - - if r.From == 0 { - oneDay := uint32(86400) // -24 hours - if r.To < oneDay { - r.From = 0 - } else { - r.From = r.To - oneDay - } - } - - if r.Timeout == 0 { - r.Timeout = defaultRequestTimeout - } -} - -// MessagesResponse is a response for shhext_requestMessages2 method. -type MessagesResponse struct { - // Cursor from the response can be used to retrieve more messages - // for the previous request. - Cursor string `json:"cursor"` - - // Error indicates that something wrong happened when sending messages - // to the requester. - Error error `json:"error"` -} - -// SyncMessagesRequest is a SyncMessages() request payload. -type SyncMessagesRequest struct { - // MailServerPeer is MailServer's enode address. - MailServerPeer string `json:"mailServerPeer"` - - // From is a lower bound of time range (optional). - // Default is 24 hours back from now. - From uint32 `json:"from"` - - // To is a upper bound of time range (optional). - // Default is now. - To uint32 `json:"to"` - - // Limit determines the number of messages sent by the mail server - // for the current paginated request - Limit uint32 `json:"limit"` - - // Cursor is used as starting point for paginated requests - Cursor string `json:"cursor"` - - // FollowCursor if true loads messages until cursor is empty. - FollowCursor bool `json:"followCursor"` - - // Topics is a list of Whisper topics. - // If empty, a full bloom filter will be used. - Topics []types.TopicType `json:"topics"` -} - -// InitiateHistoryRequestParams type for initiating history requests from a peer. -type InitiateHistoryRequestParams struct { - Peer string - SymKeyID string - Requests []TopicRequest - Force bool - Timeout time.Duration -} - -// SyncMessagesResponse is a response from the mail server -// to which SyncMessagesRequest was sent. -type SyncMessagesResponse struct { - // Cursor from the response can be used to retrieve more messages - // for the previous request. - Cursor string `json:"cursor"` - - // Error indicates that something wrong happened when sending messages - // to the requester. - Error string `json:"error"` -} - -type Author struct { - PublicKey types.HexBytes `json:"publicKey"` - Alias string `json:"alias"` - Identicon string `json:"identicon"` -} - -type Metadata struct { - DedupID []byte `json:"dedupId"` - EncryptionID types.HexBytes `json:"encryptionId"` - MessageID types.HexBytes `json:"messageId"` - Author Author `json:"author"` -} diff --git a/services/shhext/api_geth.go b/services/shhext/api_geth.go index 2318749e32..59687a041e 100644 --- a/services/shhext/api_geth.go +++ b/services/shhext/api_geth.go @@ -6,34 +6,27 @@ import ( "context" "crypto/ecdsa" "encoding/hex" - "errors" "fmt" - "math/big" "time" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rlp" - - "github.com/status-im/status-go/db" - "github.com/status-im/status-go/mailserver" - "github.com/status-im/status-go/services/shhext/mailservers" - "github.com/status-im/status-go/whisper/v6" gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" "github.com/status-im/status-go/eth-node/types" - enstypes "github.com/status-im/status-go/eth-node/types/ens" - "github.com/status-im/status-go/protocol" - "github.com/status-im/status-go/protocol/encryption/multidevice" - "github.com/status-im/status-go/protocol/transport" + "github.com/status-im/status-go/services/ext" + "github.com/status-im/status-go/whisper/v6" ) -// ----- -// PUBLIC API -// ----- +const ( + // defaultWorkTime is a work time reported in messages sent to MailServer nodes. + defaultWorkTime = 5 +) // PublicAPI extends whisper public API. type PublicAPI struct { + *ext.PublicAPI + service *Service publicAPI types.PublicWhisperAPI log log.Logger @@ -42,103 +35,64 @@ type PublicAPI struct { // NewPublicAPI returns instance of the public API. func NewPublicAPI(s *Service) *PublicAPI { return &PublicAPI{ + PublicAPI: ext.NewPublicAPI(s.Service, s.w), service: s, publicAPI: s.w.PublicWhisperAPI(), log: log.New("package", "status-go/services/sshext.PublicAPI"), } } -func (api *PublicAPI) getPeer(rawurl string) (*enode.Node, error) { - if len(rawurl) == 0 { - return mailservers.GetFirstConnected(api.service.server, api.service.peerStore) +// makeEnvelop makes an envelop for a historic messages request. +// Symmetric key is used to authenticate to MailServer. +// PK is the current node ID. +// DEPRECATED +func makeEnvelop( + payload []byte, + symKey []byte, + publicKey *ecdsa.PublicKey, + nodeID *ecdsa.PrivateKey, + pow float64, + now time.Time, +) (types.Envelope, error) { + // TODO: replace with an types.Envelope creator passed to the API struct + params := whisper.MessageParams{ + PoW: pow, + Payload: payload, + WorkTime: defaultWorkTime, + Src: nodeID, } - return enode.ParseV4(rawurl) -} - -// RetryConfig specifies configuration for retries with timeout and max amount of retries. -type RetryConfig struct { - BaseTimeout time.Duration - // StepTimeout defines duration increase per each retry. - StepTimeout time.Duration - MaxRetries int -} - -// RequestMessagesSync repeats MessagesRequest using configuration in retry conf. -func (api *PublicAPI) RequestMessagesSync(conf RetryConfig, r MessagesRequest) (MessagesResponse, error) { - var resp MessagesResponse - - shh := api.service.w - events := make(chan types.EnvelopeEvent, 10) - var ( - requestID types.HexBytes - err error - retries int - ) - for retries <= conf.MaxRetries { - sub := shh.SubscribeEnvelopeEvents(events) - r.Timeout = conf.BaseTimeout + conf.StepTimeout*time.Duration(retries) - timeout := r.Timeout - // FIXME this weird conversion is required because MessagesRequest expects seconds but defines time.Duration - r.Timeout = time.Duration(int(r.Timeout.Seconds())) - requestID, err = api.RequestMessages(context.Background(), r) - if err != nil { - sub.Unsubscribe() - return resp, err - } - mailServerResp, err := waitForExpiredOrCompleted(types.BytesToHash(requestID), events, timeout) - sub.Unsubscribe() - if err == nil { - resp.Cursor = hex.EncodeToString(mailServerResp.Cursor) - resp.Error = mailServerResp.Error - return resp, nil - } - retries++ - api.log.Error("[RequestMessagesSync] failed", "err", err, "retries", retries) + // Either symKey or public key is required. + // This condition is verified in `message.Wrap()` method. + if len(symKey) > 0 { + params.KeySym = symKey + } else if publicKey != nil { + params.Dst = publicKey } - return resp, fmt.Errorf("failed to request messages after %d retries", retries) -} - -func waitForExpiredOrCompleted(requestID types.Hash, events chan types.EnvelopeEvent, timeout time.Duration) (*types.MailServerResponse, error) { - expired := fmt.Errorf("request %x expired", requestID) - after := time.NewTimer(timeout) - defer after.Stop() - for { - var ev types.EnvelopeEvent - select { - case ev = <-events: - case <-after.C: - return nil, expired - } - if ev.Hash != requestID { - continue - } - switch ev.Event { - case types.EventMailServerRequestCompleted: - data, ok := ev.Data.(*types.MailServerResponse) - if ok { - return data, nil - } - return nil, errors.New("invalid event data type") - case types.EventMailServerRequestExpired: - return nil, expired - } + message, err := whisper.NewSentMessage(¶ms) + if err != nil { + return nil, err + } + envelope, err := message.Wrap(¶ms, now) + if err != nil { + return nil, err } + return gethbridge.NewWhisperEnvelope(envelope), nil } // RequestMessages sends a request for historic messages to a MailServer. -func (api *PublicAPI) RequestMessages(_ context.Context, r MessagesRequest) (types.HexBytes, error) { +func (api *PublicAPI) RequestMessages(_ context.Context, r ext.MessagesRequest) (types.HexBytes, error) { api.log.Info("RequestMessages", "request", r) - shh := api.service.w + now := api.service.w.GetCurrentTime() - r.setDefaults(now) + r.SetDefaults(now) if r.From > r.To { return nil, fmt.Errorf("Query range is invalid: from > to (%d > %d)", r.From, r.To) } - mailServerNode, err := api.getPeer(r.MailServerPeer) + mailServerNode, err := api.service.GetPeer(r.MailServerPeer) if err != nil { - return nil, fmt.Errorf("%v: %v", ErrInvalidMailServerPeer, err) + return nil, fmt.Errorf("%v: %v", ext.ErrInvalidMailServerPeer, err) } var ( @@ -147,15 +101,15 @@ func (api *PublicAPI) RequestMessages(_ context.Context, r MessagesRequest) (typ ) if r.SymKeyID != "" { - symKey, err = shh.GetSymKey(r.SymKeyID) + symKey, err = api.service.w.GetSymKey(r.SymKeyID) if err != nil { - return nil, fmt.Errorf("%v: %v", ErrInvalidSymKeyID, err) + return nil, fmt.Errorf("%v: %v", ext.ErrInvalidSymKeyID, err) } } else { publicKey = mailServerNode.Pubkey() } - payload, err := makeMessagesRequestPayload(r) + payload, err := ext.MakeMessagesRequestPayload(r) if err != nil { return nil, err } @@ -164,8 +118,8 @@ func (api *PublicAPI) RequestMessages(_ context.Context, r MessagesRequest) (typ payload, symKey, publicKey, - api.service.nodeID, - shh.MinPow(), + api.service.NodeID(), + api.service.w.MinPow(), now, ) if err != nil { @@ -174,15 +128,15 @@ func (api *PublicAPI) RequestMessages(_ context.Context, r MessagesRequest) (typ hash := envelope.Hash() if !r.Force { - err = api.service.requestsRegistry.Register(hash, r.Topics) + err = api.service.RequestsRegistry().Register(hash, r.Topics) if err != nil { return nil, err } } - if err := shh.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, r.Timeout*time.Second); err != nil { + if err := api.service.w.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, r.Timeout*time.Second); err != nil { if !r.Force { - api.service.requestsRegistry.Unregister(hash) + api.service.RequestsRegistry().Unregister(hash) } return nil, err } @@ -190,12 +144,86 @@ func (api *PublicAPI) RequestMessages(_ context.Context, r MessagesRequest) (typ return hash[:], nil } +// RequestMessagesSync repeats MessagesRequest using configuration in retry conf. +func (api *PublicAPI) RequestMessagesSync(conf ext.RetryConfig, r ext.MessagesRequest) (ext.MessagesResponse, error) { + var resp ext.MessagesResponse + + events := make(chan types.EnvelopeEvent, 10) + var ( + requestID types.HexBytes + err error + retries int + ) + for retries <= conf.MaxRetries { + sub := api.service.w.SubscribeEnvelopeEvents(events) + r.Timeout = conf.BaseTimeout + conf.StepTimeout*time.Duration(retries) + timeout := r.Timeout + // FIXME this weird conversion is required because MessagesRequest expects seconds but defines time.Duration + r.Timeout = time.Duration(int(r.Timeout.Seconds())) + requestID, err = api.RequestMessages(context.Background(), r) + if err != nil { + sub.Unsubscribe() + return resp, err + } + mailServerResp, err := ext.WaitForExpiredOrCompleted(types.BytesToHash(requestID), events, timeout) + sub.Unsubscribe() + if err == nil { + resp.Cursor = hex.EncodeToString(mailServerResp.Cursor) + resp.Error = mailServerResp.Error + return resp, nil + } + retries++ + api.log.Error("[RequestMessagesSync] failed", "err", err, "retries", retries) + } + return resp, fmt.Errorf("failed to request messages after %d retries", retries) +} + +// SyncMessagesRequest is a SyncMessages() request payload. +type SyncMessagesRequest struct { + // MailServerPeer is MailServer's enode address. + MailServerPeer string `json:"mailServerPeer"` + + // From is a lower bound of time range (optional). + // Default is 24 hours back from now. + From uint32 `json:"from"` + + // To is a upper bound of time range (optional). + // Default is now. + To uint32 `json:"to"` + + // Limit determines the number of messages sent by the mail server + // for the current paginated request + Limit uint32 `json:"limit"` + + // Cursor is used as starting point for paginated requests + Cursor string `json:"cursor"` + + // FollowCursor if true loads messages until cursor is empty. + FollowCursor bool `json:"followCursor"` + + // Topics is a list of Whisper topics. + // If empty, a full bloom filter will be used. + Topics []types.TopicType `json:"topics"` +} + +// SyncMessagesResponse is a response from the mail server +// to which SyncMessagesRequest was sent. +type SyncMessagesResponse struct { + // Cursor from the response can be used to retrieve more messages + // for the previous request. + Cursor string `json:"cursor"` + + // Error indicates that something wrong happened when sending messages + // to the requester. + Error string `json:"error"` +} + // createSyncMailRequest creates SyncMailRequest. It uses a full bloom filter // if no topics are given. func createSyncMailRequest(r SyncMessagesRequest) (types.SyncMailRequest, error) { var bloom []byte if len(r.Topics) > 0 { - bloom = topicsToBloom(r.Topics...) + bloom = ext.TopicsToBloom(r.Topics...) } else { bloom = types.MakeFullNodeBloom() } @@ -242,7 +270,7 @@ func (api *PublicAPI) SyncMessages(ctx context.Context, r SyncMessagesRequest) ( for { log.Info("Sending a request to sync messages", "request", request) - resp, err := api.service.syncMessages(ctx, mailServerID, request) + resp, err := api.service.SyncMessages(ctx, mailServerID, request) if err != nil { return response, err } @@ -256,421 +284,3 @@ func (api *PublicAPI) SyncMessages(ctx context.Context, r SyncMessagesRequest) ( request.Cursor = resp.Cursor } } - -// ConfirmMessagesProcessedByID is a method to confirm that messages was consumed by -// the client side. -// TODO: this is broken now as it requires dedup ID while a message hash should be used. -func (api *PublicAPI) ConfirmMessagesProcessedByID(messageConfirmations []*Metadata) error { - confirmationCount := len(messageConfirmations) - dedupIDs := make([][]byte, confirmationCount) - encryptionIDs := make([][]byte, confirmationCount) - for i, confirmation := range messageConfirmations { - dedupIDs[i] = confirmation.DedupID - encryptionIDs[i] = confirmation.EncryptionID - } - return api.service.ConfirmMessagesProcessed(encryptionIDs) -} - -// Post is used to send one-to-one for those who did not enabled device-to-device sync, -// in other words don't use PFS-enabled messages. Otherwise, SendDirectMessage is used. -// It's important to call PublicAPI.afterSend() so that the client receives a signal -// with confirmation that the message left the device. -func (api *PublicAPI) Post(ctx context.Context, newMessage types.NewMessage) (types.HexBytes, error) { - return api.publicAPI.Post(ctx, newMessage) -} - -// SendPublicMessage sends a public chat message to the underlying transport. -// Message's payload is a transit encoded message. -// It's important to call PublicAPI.afterSend() so that the client receives a signal -// with confirmation that the message left the device. -func (api *PublicAPI) SendPublicMessage(ctx context.Context, msg SendPublicMessageRPC) (types.HexBytes, error) { - chat := protocol.Chat{ - Name: msg.Chat, - } - return api.service.messenger.SendRaw(ctx, chat, msg.Payload) -} - -// SendDirectMessage sends a 1:1 chat message to the underlying transport -// Message's payload is a transit encoded message. -// It's important to call PublicAPI.afterSend() so that the client receives a signal -// with confirmation that the message left the device. -func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg SendDirectMessageRPC) (types.HexBytes, error) { - chat := protocol.Chat{ - ChatType: protocol.ChatTypeOneToOne, - ID: types.EncodeHex(msg.PubKey), - } - - return api.service.messenger.SendRaw(ctx, chat, msg.Payload) -} - -func (api *PublicAPI) Join(chat protocol.Chat) error { - return api.service.messenger.Join(chat) -} - -func (api *PublicAPI) Leave(chat protocol.Chat) error { - return api.service.messenger.Leave(chat) -} - -func (api *PublicAPI) LeaveGroupChat(ctx Context, chatID string) (*protocol.MessengerResponse, error) { - return api.service.messenger.LeaveGroupChat(ctx, chatID) -} - -func (api *PublicAPI) CreateGroupChatWithMembers(ctx Context, name string, members []string) (*protocol.MessengerResponse, error) { - return api.service.messenger.CreateGroupChatWithMembers(ctx, name, members) -} - -func (api *PublicAPI) AddMembersToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { - return api.service.messenger.AddMembersToGroupChat(ctx, chatID, members) -} - -func (api *PublicAPI) RemoveMemberFromGroupChat(ctx Context, chatID string, member string) (*protocol.MessengerResponse, error) { - return api.service.messenger.RemoveMemberFromGroupChat(ctx, chatID, member) -} - -func (api *PublicAPI) AddAdminsToGroupChat(ctx Context, chatID string, members []string) (*protocol.MessengerResponse, error) { - return api.service.messenger.AddAdminsToGroupChat(ctx, chatID, members) -} - -func (api *PublicAPI) ConfirmJoiningGroup(ctx context.Context, chatID string) (*protocol.MessengerResponse, error) { - return api.service.messenger.ConfirmJoiningGroup(ctx, chatID) -} - -func (api *PublicAPI) requestMessagesUsingPayload(request db.HistoryRequest, peer, symkeyID string, payload []byte, force bool, timeout time.Duration, topics []types.TopicType) (hash types.Hash, err error) { - shh := api.service.w - now := api.service.w.GetCurrentTime() - - mailServerNode, err := api.getPeer(peer) - if err != nil { - return hash, fmt.Errorf("%v: %v", ErrInvalidMailServerPeer, err) - } - - var ( - symKey []byte - publicKey *ecdsa.PublicKey - ) - - if symkeyID != "" { - symKey, err = shh.GetSymKey(symkeyID) - if err != nil { - return hash, fmt.Errorf("%v: %v", ErrInvalidSymKeyID, err) - } - } else { - publicKey = mailServerNode.Pubkey() - } - - envelope, err := makeEnvelop( - payload, - symKey, - publicKey, - api.service.nodeID, - shh.MinPow(), - now, - ) - if err != nil { - return hash, err - } - hash = envelope.Hash() - - err = request.Replace(hash) - if err != nil { - return hash, err - } - - if !force { - err = api.service.requestsRegistry.Register(hash, topics) - if err != nil { - return hash, err - } - } - - if err := shh.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, timeout); err != nil { - if !force { - api.service.requestsRegistry.Unregister(hash) - } - return hash, err - } - - return hash, nil - -} - -// InitiateHistoryRequests is a stateful API for initiating history request for each topic. -// Caller of this method needs to define only two parameters per each TopicRequest: -// - Topic -// - Duration in nanoseconds. Will be used to determine starting time for history request. -// After that status-go will guarantee that request for this topic and date will be performed. -func (api *PublicAPI) InitiateHistoryRequests(parent context.Context, request InitiateHistoryRequestParams) (rst []types.HexBytes, err error) { - tx := api.service.storage.NewTx() - defer func() { - if err == nil { - err = tx.Commit() - } - }() - ctx := NewContextFromService(parent, api.service, tx) - requests, err := api.service.historyUpdates.CreateRequests(ctx, request.Requests) - if err != nil { - return nil, err - } - var ( - payload []byte - hash types.Hash - ) - for i := range requests { - req := requests[i] - options := CreateTopicOptionsFromRequest(req) - bloom := options.ToBloomFilterOption() - payload, err = bloom.ToMessagesRequestPayload() - if err != nil { - return rst, err - } - hash, err = api.requestMessagesUsingPayload(req, request.Peer, request.SymKeyID, payload, request.Force, request.Timeout, options.Topics()) - if err != nil { - return rst, err - } - rst = append(rst, hash.Bytes()) - } - return rst, err -} - -// CompleteRequest client must mark request completed when all envelopes were processed. -func (api *PublicAPI) CompleteRequest(parent context.Context, hex string) (err error) { - tx := api.service.storage.NewTx() - ctx := NewContextFromService(parent, api.service, tx) - err = api.service.historyUpdates.UpdateFinishedRequest(ctx, types.HexToHash(hex)) - if err == nil { - return tx.Commit() - } - return err -} - -func (api *PublicAPI) LoadFilters(parent context.Context, chats []*transport.Filter) ([]*transport.Filter, error) { - return api.service.messenger.LoadFilters(chats) -} - -func (api *PublicAPI) SaveChat(parent context.Context, chat *protocol.Chat) error { - api.log.Info("saving chat", "chat", chat) - return api.service.messenger.SaveChat(chat) -} - -func (api *PublicAPI) Chats(parent context.Context) []*protocol.Chat { - return api.service.messenger.Chats() -} - -func (api *PublicAPI) DeleteChat(parent context.Context, chatID string) error { - return api.service.messenger.DeleteChat(chatID) -} - -func (api *PublicAPI) SaveContact(parent context.Context, contact *protocol.Contact) error { - return api.service.messenger.SaveContact(contact) -} - -func (api *PublicAPI) BlockContact(parent context.Context, contact *protocol.Contact) ([]*protocol.Chat, error) { - api.log.Info("blocking contact", "contact", contact.ID) - return api.service.messenger.BlockContact(contact) -} - -func (api *PublicAPI) Contacts(parent context.Context) []*protocol.Contact { - return api.service.messenger.Contacts() -} - -func (api *PublicAPI) RemoveFilters(parent context.Context, chats []*transport.Filter) error { - return api.service.messenger.RemoveFilters(chats) -} - -// EnableInstallation enables an installation for multi-device sync. -func (api *PublicAPI) EnableInstallation(installationID string) error { - return api.service.messenger.EnableInstallation(installationID) -} - -// DisableInstallation disables an installation for multi-device sync. -func (api *PublicAPI) DisableInstallation(installationID string) error { - return api.service.messenger.DisableInstallation(installationID) -} - -// GetOurInstallations returns all the installations available given an identity -func (api *PublicAPI) GetOurInstallations() []*multidevice.Installation { - return api.service.messenger.Installations() -} - -// SetInstallationMetadata sets the metadata for our own installation -func (api *PublicAPI) SetInstallationMetadata(installationID string, data *multidevice.InstallationMetadata) error { - return api.service.messenger.SetInstallationMetadata(installationID, data) -} - -// VerifyENSNames takes a list of ensdetails and returns whether they match the public key specified -func (api *PublicAPI) VerifyENSNames(details []enstypes.ENSDetails) (map[string]enstypes.ENSResponse, error) { - return api.service.messenger.VerifyENSNames(api.service.config.VerifyENSURL, ensContractAddress, details) -} - -type ApplicationMessagesResponse struct { - Messages []*protocol.Message `json:"messages"` - Cursor string `json:"cursor"` -} - -func (api *PublicAPI) ChatMessages(chatID, cursor string, limit int) (*ApplicationMessagesResponse, error) { - messages, cursor, err := api.service.messenger.MessageByChatID(chatID, cursor, limit) - if err != nil { - return nil, err - } - - return &ApplicationMessagesResponse{ - Messages: messages, - Cursor: cursor, - }, nil -} - -func (api *PublicAPI) DeleteMessage(id string) error { - return api.service.messenger.DeleteMessage(id) -} - -func (api *PublicAPI) DeleteMessagesByChatID(id string) error { - return api.service.messenger.DeleteMessagesByChatID(id) -} - -func (api *PublicAPI) MarkMessagesSeen(chatID string, ids []string) error { - return api.service.messenger.MarkMessagesSeen(chatID, ids) -} - -func (api *PublicAPI) UpdateMessageOutgoingStatus(id, newOutgoingStatus string) error { - return api.service.messenger.UpdateMessageOutgoingStatus(id, newOutgoingStatus) -} - -func (api *PublicAPI) SendChatMessage(ctx context.Context, message *protocol.Message) (*protocol.MessengerResponse, error) { - return api.service.messenger.SendChatMessage(ctx, message) -} - -func (api *PublicAPI) ReSendChatMessage(ctx context.Context, messageID string) error { - return api.service.messenger.ReSendChatMessage(ctx, messageID) -} - -func (api *PublicAPI) RequestTransaction(ctx context.Context, chatID, value, contract, address string) (*protocol.MessengerResponse, error) { - return api.service.messenger.RequestTransaction(ctx, chatID, value, contract, address) -} - -func (api *PublicAPI) RequestAddressForTransaction(ctx context.Context, chatID, from, value, contract string) (*protocol.MessengerResponse, error) { - return api.service.messenger.RequestAddressForTransaction(ctx, chatID, from, value, contract) -} - -func (api *PublicAPI) DeclineRequestAddressForTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { - return api.service.messenger.DeclineRequestAddressForTransaction(ctx, messageID) -} - -func (api *PublicAPI) DeclineRequestTransaction(ctx context.Context, messageID string) (*protocol.MessengerResponse, error) { - return api.service.messenger.DeclineRequestTransaction(ctx, messageID) -} - -func (api *PublicAPI) AcceptRequestAddressForTransaction(ctx context.Context, messageID, address string) (*protocol.MessengerResponse, error) { - return api.service.messenger.AcceptRequestAddressForTransaction(ctx, messageID, address) -} - -func (api *PublicAPI) SendTransaction(ctx context.Context, chatID, value, contract, transactionHash string, signature types.HexBytes) (*protocol.MessengerResponse, error) { - return api.service.messenger.SendTransaction(ctx, chatID, value, contract, transactionHash, signature) -} - -func (api *PublicAPI) AcceptRequestTransaction(ctx context.Context, transactionHash, messageID string, signature types.HexBytes) (*protocol.MessengerResponse, error) { - return api.service.messenger.AcceptRequestTransaction(ctx, transactionHash, messageID, signature) -} - -func (api *PublicAPI) SendContactUpdates(ctx context.Context, name, picture string) error { - return api.service.messenger.SendContactUpdates(ctx, name, picture) -} - -func (api *PublicAPI) SendContactUpdate(ctx context.Context, contactID, name, picture string) (*protocol.MessengerResponse, error) { - return api.service.messenger.SendContactUpdate(ctx, contactID, name, picture) -} - -func (api *PublicAPI) SendPairInstallation(ctx context.Context) (*protocol.MessengerResponse, error) { - return api.service.messenger.SendPairInstallation(ctx) -} - -func (api *PublicAPI) SyncDevices(ctx context.Context, name, picture string) error { - return api.service.messenger.SyncDevices(ctx, name, picture) -} - -// ----- -// HELPER -// ----- - -// makeEnvelop makes an envelop for a historic messages request. -// Symmetric key is used to authenticate to MailServer. -// PK is the current node ID. -func makeEnvelop( - payload []byte, - symKey []byte, - publicKey *ecdsa.PublicKey, - nodeID *ecdsa.PrivateKey, - pow float64, - now time.Time, -) (types.Envelope, error) { - // TODO: replace with an types.Envelope creator passed to the API struct - params := whisper.MessageParams{ - PoW: pow, - Payload: payload, - WorkTime: defaultWorkTime, - Src: nodeID, - } - // Either symKey or public key is required. - // This condition is verified in `message.Wrap()` method. - if len(symKey) > 0 { - params.KeySym = symKey - } else if publicKey != nil { - params.Dst = publicKey - } - message, err := whisper.NewSentMessage(¶ms) - if err != nil { - return nil, err - } - envelope, err := message.Wrap(¶ms, now) - if err != nil { - return nil, err - } - return gethbridge.NewWhisperEnvelope(envelope), nil -} - -// makeMessagesRequestPayload makes a specific payload for MailServer -// to request historic messages. -func makeMessagesRequestPayload(r MessagesRequest) ([]byte, error) { - cursor, err := hex.DecodeString(r.Cursor) - if err != nil { - return nil, fmt.Errorf("invalid cursor: %v", err) - } - - if len(cursor) > 0 && len(cursor) != mailserver.CursorLength { - return nil, fmt.Errorf("invalid cursor size: expected %d but got %d", mailserver.CursorLength, len(cursor)) - } - - payload := mailserver.MessagesRequestPayload{ - Lower: r.From, - Upper: r.To, - Bloom: createBloomFilter(r), - Limit: r.Limit, - Cursor: cursor, - // Client must tell the MailServer if it supports batch responses. - // This can be removed in the future. - Batch: true, - } - - return rlp.EncodeToBytes(payload) -} - -func createBloomFilter(r MessagesRequest) []byte { - if len(r.Topics) > 0 { - return topicsToBloom(r.Topics...) - } - - return types.TopicToBloom(r.Topic) -} - -func topicsToBloom(topics ...types.TopicType) []byte { - i := new(big.Int) - for _, topic := range topics { - bloom := types.TopicToBloom(topic) - i.Or(i, new(big.Int).SetBytes(bloom[:])) - } - - combined := make([]byte, types.BloomFilterSize) - data := i.Bytes() - copy(combined[types.BloomFilterSize-len(data):], data[:]) - - return combined -} diff --git a/services/shhext/api_geth_test.go b/services/shhext/api_geth_test.go index 86e9c207cc..02b9cf45ac 100644 --- a/services/shhext/api_geth_test.go +++ b/services/shhext/api_geth_test.go @@ -1,145 +1,36 @@ -// +build !nimbus - package shhext import ( "context" "encoding/hex" "fmt" + "io/ioutil" + "math" + "net" + "os" + "strconv" "testing" "time" - "github.com/status-im/status-go/eth-node/types" - - "github.com/status-im/status-go/mailserver" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" -) - -func TestMessagesRequest_setDefaults(t *testing.T) { - daysAgo := func(now time.Time, days int) uint32 { - return uint32(now.UTC().Add(-24 * time.Hour * time.Duration(days)).Unix()) - } - - tnow := time.Now() - now := uint32(tnow.UTC().Unix()) - yesterday := daysAgo(tnow, 1) - - scenarios := []struct { - given *MessagesRequest - expected *MessagesRequest - }{ - { - &MessagesRequest{From: 0, To: 0}, - &MessagesRequest{From: yesterday, To: now, Timeout: defaultRequestTimeout}, - }, - { - &MessagesRequest{From: 1, To: 0}, - &MessagesRequest{From: uint32(1), To: now, Timeout: defaultRequestTimeout}, - }, - { - &MessagesRequest{From: 0, To: yesterday}, - &MessagesRequest{From: daysAgo(tnow, 2), To: yesterday, Timeout: defaultRequestTimeout}, - }, - // 100 - 1 day would be invalid, so we set From to 0 - { - &MessagesRequest{From: 0, To: 100}, - &MessagesRequest{From: 0, To: 100, Timeout: defaultRequestTimeout}, - }, - // set Timeout - { - &MessagesRequest{From: 0, To: 0, Timeout: 100}, - &MessagesRequest{From: yesterday, To: now, Timeout: 100}, - }, - } - - for i, s := range scenarios { - t.Run(fmt.Sprintf("Scenario %d", i), func(t *testing.T) { - s.given.setDefaults(tnow) - require.Equal(t, s.expected, s.given) - }) - } -} - -func TestMakeMessagesRequestPayload(t *testing.T) { - var emptyTopic types.TopicType - testCases := []struct { - Name string - Req MessagesRequest - Err string - }{ - { - Name: "empty cursor", - Req: MessagesRequest{Cursor: ""}, - Err: "", - }, - { - Name: "invalid cursor size", - Req: MessagesRequest{Cursor: hex.EncodeToString([]byte{0x01, 0x02, 0x03})}, - Err: fmt.Sprintf("invalid cursor size: expected %d but got 3", mailserver.CursorLength), - }, - { - Name: "valid cursor", - Req: MessagesRequest{ - Cursor: hex.EncodeToString(mailserver.NewDBKey(123, emptyTopic, types.Hash{}).Cursor()), - }, - Err: "", - }, - } + "github.com/stretchr/testify/suite" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/storage" - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - _, err := makeMessagesRequestPayload(tc.Req) - if tc.Err == "" { - require.NoError(t, err) - } else { - require.EqualError(t, err, tc.Err) - } - }) - } -} - -func TestTopicsToBloom(t *testing.T) { - t1 := stringToTopic("t1") - b1 := types.TopicToBloom(t1) - t2 := stringToTopic("t2") - b2 := types.TopicToBloom(t2) - t3 := stringToTopic("t3") - b3 := types.TopicToBloom(t3) - - reqBloom := topicsToBloom(t1) - assert.True(t, types.BloomFilterMatch(reqBloom, b1)) - assert.False(t, types.BloomFilterMatch(reqBloom, b2)) - assert.False(t, types.BloomFilterMatch(reqBloom, b3)) - - reqBloom = topicsToBloom(t1, t2) - assert.True(t, types.BloomFilterMatch(reqBloom, b1)) - assert.True(t, types.BloomFilterMatch(reqBloom, b2)) - assert.False(t, types.BloomFilterMatch(reqBloom, b3)) - - reqBloom = topicsToBloom(t1, t2, t3) - assert.True(t, types.BloomFilterMatch(reqBloom, b1)) - assert.True(t, types.BloomFilterMatch(reqBloom, b2)) - assert.True(t, types.BloomFilterMatch(reqBloom, b3)) -} - -func TestCreateBloomFilter(t *testing.T) { - t1 := stringToTopic("t1") - t2 := stringToTopic("t2") - - req := MessagesRequest{Topic: t1} - bloom := createBloomFilter(req) - assert.Equal(t, topicsToBloom(t1), bloom) - - req = MessagesRequest{Topics: []types.TopicType{t1, t2}} - bloom = createBloomFilter(req) - assert.Equal(t, topicsToBloom(t1, t2), bloom) -} - -func stringToTopic(s string) types.TopicType { - return types.BytesToTopic([]byte(s)) -} + gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/ext" + "github.com/status-im/status-go/sqlite" + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/whisper/v6" +) func TestCreateSyncMailRequest(t *testing.T) { testCases := []struct { @@ -223,19 +114,383 @@ func TestSyncMessagesErrors(t *testing.T) { } } -func TestExpiredOrCompleted(t *testing.T) { - timeout := time.Millisecond - events := make(chan types.EnvelopeEvent) - errors := make(chan error, 1) - hash := types.Hash{1} +func TestRequestMessagesErrors(t *testing.T) { + var err error + + shh := gethbridge.NewGethWhisperWrapper(whisper.New(nil)) + aNode, err := node.New(&node.Config{ + P2P: p2p.Config{ + MaxPeers: math.MaxInt32, + NoDiscovery: true, + }, + NoUSB: true, + }) // in-memory node as no data dir + require.NoError(t, err) + err = aNode.Register(func(*node.ServiceContext) (node.Service, error) { + return gethbridge.GetGethWhisperFrom(shh), nil + }) + require.NoError(t, err) + + err = aNode.Start() + require.NoError(t, err) + defer func() { require.NoError(t, aNode.Stop()) }() + + handler := ext.NewHandlerMock(1) + config := params.ShhextConfig{ + InstallationID: "1", + BackupDisabledDataDir: os.TempDir(), + PFSEnabled: true, + } + nodeWrapper := ext.NewTestNodeWrapper(shh, nil) + service := New(config, nodeWrapper, nil, handler, nil) + api := NewPublicAPI(service) + + const ( + mailServerPeer = "enode://b7e65e1bedc2499ee6cbd806945af5e7df0e59e4070c96821570bd581473eade24a489f5ec95d060c0db118c879403ab88d827d3766978f28708989d35474f87@[::]:51920" + ) + + var hash []byte + + // invalid MailServer enode address + hash, err = api.RequestMessages(context.TODO(), ext.MessagesRequest{MailServerPeer: "invalid-address"}) + require.Nil(t, hash) + require.EqualError(t, err, "invalid mailServerPeer value: invalid URL scheme, want \"enode\"") + + // non-existent symmetric key + hash, err = api.RequestMessages(context.TODO(), ext.MessagesRequest{ + MailServerPeer: mailServerPeer, + SymKeyID: "invalid-sym-key-id", + }) + require.Nil(t, hash) + require.EqualError(t, err, "invalid symKeyID value: non-existent key ID") + + // with a symmetric key + symKeyID, symKeyErr := shh.AddSymKeyFromPassword("some-pass") + require.NoError(t, symKeyErr) + hash, err = api.RequestMessages(context.TODO(), ext.MessagesRequest{ + MailServerPeer: mailServerPeer, + SymKeyID: symKeyID, + }) + require.Nil(t, hash) + require.Contains(t, err.Error(), "Could not find peer with ID") + + // from is greater than to + hash, err = api.RequestMessages(context.TODO(), ext.MessagesRequest{ + From: 10, + To: 5, + }) + require.Nil(t, hash) + require.Contains(t, err.Error(), "Query range is invalid: from > to (10 > 5)") +} + +func TestInitProtocol(t *testing.T) { + directory, err := ioutil.TempDir("", "status-go-testing") + require.NoError(t, err) + + config := params.ShhextConfig{ + InstallationID: "2", + BackupDisabledDataDir: directory, + PFSEnabled: true, + MailServerConfirmations: true, + ConnectionTarget: 10, + } + db, err := leveldb.Open(storage.NewMemStorage(), nil) + require.NoError(t, err) + + shh := gethbridge.NewGethWhisperWrapper(whisper.New(nil)) + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + + nodeWrapper := ext.NewTestNodeWrapper(shh, nil) + service := New(config, nodeWrapper, nil, nil, db) + + tmpdir, err := ioutil.TempDir("", "test-shhext-service-init-protocol") + require.NoError(t, err) + + sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") + require.NoError(t, err) + + err = service.InitProtocol(privateKey, sqlDB) + require.NoError(t, err) +} + +func TestShhExtSuite(t *testing.T) { + suite.Run(t, new(ShhExtSuite)) +} + +type ShhExtSuite struct { + suite.Suite + + dir string + nodes []*node.Node + whispers []types.Whisper + services []*Service +} + +func (s *ShhExtSuite) createAndAddNode() { + idx := len(s.nodes) + + // create a node + cfg := &node.Config{ + Name: strconv.Itoa(idx), + P2P: p2p.Config{ + MaxPeers: math.MaxInt32, + NoDiscovery: true, + ListenAddr: ":0", + }, + NoUSB: true, + } + stack, err := node.New(cfg) + s.NoError(err) + whisper := whisper.New(nil) + err = stack.Register(func(n *node.ServiceContext) (node.Service, error) { + return whisper, nil + }) + s.NoError(err) + + // set up protocol + config := params.ShhextConfig{ + InstallationID: strconv.Itoa(idx), + BackupDisabledDataDir: s.dir, + PFSEnabled: true, + MailServerConfirmations: true, + ConnectionTarget: 10, + } + db, err := leveldb.Open(storage.NewMemStorage(), nil) + s.Require().NoError(err) + nodeWrapper := ext.NewTestNodeWrapper(gethbridge.NewGethWhisperWrapper(whisper), nil) + service := New(config, nodeWrapper, nil, nil, db) + sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/%d", s.dir, idx), "password") + s.Require().NoError(err) + privateKey, err := crypto.GenerateKey() + s.NoError(err) + err = service.InitProtocol(privateKey, sqlDB) + s.NoError(err) + err = stack.Register(func(n *node.ServiceContext) (node.Service, error) { + return service, nil + }) + s.NoError(err) + + // start the node + err = stack.Start() + s.Require().NoError(err) + + // store references + s.nodes = append(s.nodes, stack) + s.whispers = append(s.whispers, gethbridge.NewGethWhisperWrapper(whisper)) + s.services = append(s.services, service) +} + +func (s *ShhExtSuite) SetupTest() { + var err error + s.dir, err = ioutil.TempDir("", "status-go-testing") + s.Require().NoError(err) +} + +func (s *ShhExtSuite) TearDownTest() { + for _, n := range s.nodes { + s.NoError(n.Stop()) + } + s.nodes = nil + s.whispers = nil + s.services = nil +} + +func (s *ShhExtSuite) TestRequestMessagesSuccess() { + // two nodes needed: client and mailserver + s.createAndAddNode() + s.createAndAddNode() + + waitErr := helpers.WaitForPeerAsync(s.nodes[0].Server(), s.nodes[1].Server().Self().URLv4(), p2p.PeerEventTypeAdd, time.Second) + s.nodes[0].Server().AddPeer(s.nodes[1].Server().Self()) + s.Require().NoError(<-waitErr) + + api := NewPublicAPI(s.services[0]) + + _, err := api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: s.nodes[1].Server().Self().URLv4(), + Topics: []types.TopicType{{1}}, + }) + s.NoError(err) +} + +func (s *ShhExtSuite) TestMultipleRequestMessagesWithoutForce() { + // two nodes needed: client and mailserver + s.createAndAddNode() + s.createAndAddNode() + + waitErr := helpers.WaitForPeerAsync(s.nodes[0].Server(), s.nodes[1].Server().Self().URLv4(), p2p.PeerEventTypeAdd, time.Second) + s.nodes[0].Server().AddPeer(s.nodes[1].Server().Self()) + s.Require().NoError(<-waitErr) + + api := NewPublicAPI(s.services[0]) + + _, err := api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: s.nodes[1].Server().Self().URLv4(), + Topics: []types.TopicType{{1}}, + }) + s.NoError(err) + _, err = api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: s.nodes[1].Server().Self().URLv4(), + Topics: []types.TopicType{{1}}, + }) + s.EqualError(err, "another request with the same topics was sent less than 3s ago. Please wait for a bit longer, or set `force` to true in request parameters") + _, err = api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: s.nodes[1].Server().Self().URLv4(), + Topics: []types.TopicType{{2}}, + }) + s.NoError(err) +} + +func (s *ShhExtSuite) TestFailedRequestWithUnknownMailServerPeer() { + s.createAndAddNode() + + api := NewPublicAPI(s.services[0]) + + _, err := api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: "enode://19872f94b1e776da3a13e25afa71b47dfa99e658afd6427ea8d6e03c22a99f13590205a8826443e95a37eee1d815fc433af7a8ca9a8d0df7943d1f55684045b7@0.0.0.0:30305", + Topics: []types.TopicType{{1}}, + }) + s.EqualError(err, "Could not find peer with ID: 10841e6db5c02fc331bf36a8d2a9137a1696d9d3b6b1f872f780e02aa8ec5bba") +} + +const ( + // internal whisper protocol codes + statusCode = 0 + p2pRequestCompleteCode = 125 +) + +type WhisperNodeMockSuite struct { + suite.Suite + + localWhisperAPI *whisper.PublicWhisperAPI + localAPI *PublicAPI + localNode *enode.Node + remoteRW *p2p.MsgPipeRW + + localService *Service +} + +func (s *WhisperNodeMockSuite) SetupTest() { + db, err := leveldb.Open(storage.NewMemStorage(), nil) + s.Require().NoError(err) + conf := &whisper.Config{ + MinimumAcceptedPOW: 0, + MaxMessageSize: 100 << 10, + } + w := whisper.New(conf) + s.Require().NoError(w.Start(nil)) + pkey, err := crypto.GenerateKey() + s.Require().NoError(err) + node := enode.NewV4(&pkey.PublicKey, net.ParseIP("127.0.0.1"), 1, 1) + peer := p2p.NewPeer(node.ID(), "1", []p2p.Cap{{"shh", 6}}) + rw1, rw2 := p2p.MsgPipe() + errorc := make(chan error, 1) go func() { - _, err := waitForExpiredOrCompleted(hash, events, timeout) - errors <- err + err := w.HandlePeer(peer, rw2) + errorc <- err }() - select { - case <-time.After(time.Second): - require.FailNow(t, "timed out waiting for waitForExpiredOrCompleted to complete") - case err := <-errors: - require.EqualError(t, err, fmt.Sprintf("request %x expired", hash)) - } + whisperWrapper := gethbridge.NewGethWhisperWrapper(w) + s.Require().NoError(p2p.ExpectMsg(rw1, statusCode, []interface{}{ + whisper.ProtocolVersion, + math.Float64bits(whisperWrapper.MinPow()), + whisperWrapper.BloomFilter(), + false, + true, + whisper.RateLimits{}, + })) + s.Require().NoError(p2p.SendItems( + rw1, + statusCode, + whisper.ProtocolVersion, + whisper.ProtocolVersion, + math.Float64bits(whisperWrapper.MinPow()), + whisperWrapper.BloomFilter(), + true, + true, + whisper.RateLimits{}, + )) + + nodeWrapper := ext.NewTestNodeWrapper(whisperWrapper, nil) + s.localService = New( + params.ShhextConfig{MailServerConfirmations: true, MaxMessageDeliveryAttempts: 3}, + nodeWrapper, + nil, + nil, + db, + ) + s.Require().NoError(s.localService.UpdateMailservers([]*enode.Node{node})) + + s.localWhisperAPI = whisper.NewPublicWhisperAPI(w) + s.localAPI = NewPublicAPI(s.localService) + s.localNode = node + s.remoteRW = rw1 +} + +func TestRequestMessagesSync(t *testing.T) { + suite.Run(t, new(RequestMessagesSyncSuite)) +} + +type RequestMessagesSyncSuite struct { + WhisperNodeMockSuite +} + +func (s *RequestMessagesSyncSuite) TestExpired() { + // intentionally discarding all requests, so that request will timeout + go func() { + msg, err := s.remoteRW.ReadMsg() + s.Require().NoError(err) + s.Require().NoError(msg.Discard()) + }() + _, err := s.localAPI.RequestMessagesSync( + ext.RetryConfig{ + BaseTimeout: time.Second, + }, + ext.MessagesRequest{ + MailServerPeer: s.localNode.String(), + }, + ) + s.Require().EqualError(err, "failed to request messages after 1 retries") +} + +func (s *RequestMessagesSyncSuite) testCompletedFromAttempt(target int) { + const cursorSize = 36 // taken from mailserver_response.go from whisper package + cursor := [cursorSize]byte{} + cursor[0] = 0x01 + + go func() { + attempt := 0 + for { + attempt++ + msg, err := s.remoteRW.ReadMsg() + s.Require().NoError(err) + if attempt < target { + s.Require().NoError(msg.Discard()) + continue + } + var e whisper.Envelope + s.Require().NoError(msg.Decode(&e)) + s.Require().NoError(p2p.Send(s.remoteRW, p2pRequestCompleteCode, whisper.CreateMailServerRequestCompletedPayload(e.Hash(), common.Hash{}, cursor[:]))) + } + }() + resp, err := s.localAPI.RequestMessagesSync( + ext.RetryConfig{ + BaseTimeout: time.Second, + MaxRetries: target, + }, + ext.MessagesRequest{ + MailServerPeer: s.localNode.String(), + Force: true, // force true is convenient here because timeout is less then default delay (3s) + }, + ) + s.Require().NoError(err) + s.Require().Equal(ext.MessagesResponse{Cursor: hex.EncodeToString(cursor[:])}, resp) +} + +func (s *RequestMessagesSyncSuite) TestCompletedFromFirstAttempt() { + s.testCompletedFromAttempt(1) +} + +func (s *RequestMessagesSyncSuite) TestCompletedFromSecondAttempt() { + s.testCompletedFromAttempt(2) } diff --git a/services/shhext/context_geth.go b/services/shhext/context_geth.go deleted file mode 100644 index e67c887ead..0000000000 --- a/services/shhext/context_geth.go +++ /dev/null @@ -1,14 +0,0 @@ -// +build !nimbus - -package shhext - -import ( - "context" - - "github.com/status-im/status-go/db" -) - -// NewContextFromService creates new context instance using Service fileds directly and Storage. -func NewContextFromService(ctx context.Context, service *Service, storage db.Storage) Context { - return NewContext(ctx, service.w.GetCurrentTime, service.requestsRegistry, storage) -} diff --git a/services/shhext/history.go b/services/shhext/history.go deleted file mode 100644 index f28665385e..0000000000 --- a/services/shhext/history.go +++ /dev/null @@ -1,19 +0,0 @@ -package shhext - -import ( - "time" - - "github.com/status-im/status-go/eth-node/types" -) - -const ( - // WhisperTimeAllowance is needed to ensure that we won't miss envelopes that were - // delivered to mail server after we made a request. - WhisperTimeAllowance = 20 * time.Second -) - -// TopicRequest defines what user has to provide. -type TopicRequest struct { - Topic types.TopicType - Duration time.Duration -} diff --git a/services/shhext/history_geth.go b/services/shhext/history_geth.go deleted file mode 100644 index 0a21d81523..0000000000 --- a/services/shhext/history_geth.go +++ /dev/null @@ -1,340 +0,0 @@ -// +build !nimbus - -package shhext - -import ( - "errors" - "fmt" - "sort" - "sync" - "time" - - "github.com/ethereum/go-ethereum/rlp" - - "github.com/status-im/status-go/db" - "github.com/status-im/status-go/eth-node/types" - "github.com/status-im/status-go/mailserver" -) - -// NewHistoryUpdateReactor creates HistoryUpdateReactor instance. -func NewHistoryUpdateReactor() *HistoryUpdateReactor { - return &HistoryUpdateReactor{} -} - -// HistoryUpdateReactor responsible for tracking progress for all history requests. -// It listens for 2 events: -// - when envelope from mail server is received we will update appropriate topic on disk -// - when confirmation for request completion is received - we will set last envelope timestamp as the last timestamp -// for all TopicLists in current request. -type HistoryUpdateReactor struct { - mu sync.Mutex -} - -// UpdateFinishedRequest removes successfully finished request and updates every topic -// attached to the request. -func (reactor *HistoryUpdateReactor) UpdateFinishedRequest(ctx Context, id types.Hash) error { - reactor.mu.Lock() - defer reactor.mu.Unlock() - req, err := ctx.HistoryStore().GetRequest(id) - if err != nil { - return err - } - for i := range req.Histories() { - th := &req.Histories()[i] - th.RequestID = types.Hash{} - th.Current = th.End - th.End = time.Time{} - if err := th.Save(); err != nil { - return err - } - } - return req.Delete() -} - -// UpdateTopicHistory updates Current timestamp for the TopicHistory with a given timestamp. -func (reactor *HistoryUpdateReactor) UpdateTopicHistory(ctx Context, topic types.TopicType, timestamp time.Time) error { - reactor.mu.Lock() - defer reactor.mu.Unlock() - histories, err := ctx.HistoryStore().GetHistoriesByTopic(topic) - if err != nil { - return err - } - if len(histories) == 0 { - return fmt.Errorf("no histories for topic 0x%x", topic) - } - for i := range histories { - th := &histories[i] - // this case could happen only iff envelopes were delivered out of order - // last envelope received, request completed, then others envelopes received - // request completed, last envelope received, and then all others envelopes received - if !th.Pending() { - continue - } - if timestamp.Before(th.End) && timestamp.After(th.Current) { - th.Current = timestamp - } - err := th.Save() - if err != nil { - return err - } - } - return nil -} - -// CreateRequests receives list of topic with desired timestamps and initiates both pending requests and requests -// that cover new topics. -func (reactor *HistoryUpdateReactor) CreateRequests(ctx Context, topicRequests []TopicRequest) ([]db.HistoryRequest, error) { - reactor.mu.Lock() - defer reactor.mu.Unlock() - seen := map[types.TopicType]struct{}{} - for i := range topicRequests { - if _, exist := seen[topicRequests[i].Topic]; exist { - return nil, errors.New("only one duration per topic is allowed") - } - seen[topicRequests[i].Topic] = struct{}{} - } - histories := map[types.TopicType]db.TopicHistory{} - for i := range topicRequests { - th, err := ctx.HistoryStore().GetHistory(topicRequests[i].Topic, topicRequests[i].Duration) - if err != nil { - return nil, err - } - histories[th.Topic] = th - } - requests, err := ctx.HistoryStore().GetAllRequests() - if err != nil { - return nil, err - } - filtered := []db.HistoryRequest{} - for i := range requests { - req := requests[i] - for _, th := range histories { - if th.Pending() { - delete(histories, th.Topic) - } - } - if !ctx.RequestRegistry().Has(req.ID) { - filtered = append(filtered, req) - } - } - adjusted, err := adjustRequestedHistories(ctx.HistoryStore(), mapToList(histories)) - if err != nil { - return nil, err - } - filtered = append(filtered, - GroupHistoriesByRequestTimespan(ctx.HistoryStore(), adjusted)...) - return RenewRequests(filtered, ctx.Time()), nil -} - -// for every history that is not included in any request check if there are other ranges with such topic in db -// if so check if they can be merged -// if not then adjust second part so that End of it will be equal to First of previous -func adjustRequestedHistories(store db.HistoryStore, histories []db.TopicHistory) ([]db.TopicHistory, error) { - adjusted := []db.TopicHistory{} - for i := range histories { - all, err := store.GetHistoriesByTopic(histories[i].Topic) - if err != nil { - return nil, err - } - th, err := adjustRequestedHistory(&histories[i], all...) - if err != nil { - return nil, err - } - if th != nil { - adjusted = append(adjusted, *th) - } - } - return adjusted, nil -} - -func adjustRequestedHistory(th *db.TopicHistory, others ...db.TopicHistory) (*db.TopicHistory, error) { - sort.Slice(others, func(i, j int) bool { - return others[i].Duration > others[j].Duration - }) - if len(others) == 1 && others[0].Duration == th.Duration { - return th, nil - } - for j := range others { - if others[j].Duration == th.Duration { - // skip instance with same duration - continue - } else if th.Duration > others[j].Duration { - if th.Current.Equal(others[j].First) { - // this condition will be reached when query for new index successfully finished - th.Current = others[j].Current - // FIXME next two db operations must be completed atomically - err := th.Save() - if err != nil { - return nil, err - } - err = others[j].Delete() - if err != nil { - return nil, err - } - } else if (others[j].First != time.Time{}) { - // select First timestamp with lowest value. if there are multiple indexes that cover such ranges: - // 6:00 - 7:00 Duration: 3h - // 7:00 - 8:00 2h - // 8:00 - 9:00 1h - // and client created new index with Duration 4h - // 4h index must have End value set to 6:00 - if (others[j].First.Before(th.End) || th.End == time.Time{}) { - th.End = others[j].First - } - } else { - // remove previous if it is covered by new one - // client created multiple indexes without any succsefully executed query - err := others[j].Delete() - if err != nil { - return nil, err - } - } - } else if th.Duration < others[j].Duration { - if !others[j].Pending() { - th = &others[j] - } else { - return nil, nil - } - } - } - return th, nil -} - -// RenewRequests re-sets current, first and end timestamps. -// Changes should not be persisted on disk in this method. -func RenewRequests(requests []db.HistoryRequest, now time.Time) []db.HistoryRequest { - zero := time.Time{} - for i := range requests { - req := requests[i] - histories := req.Histories() - for j := range histories { - history := &histories[j] - if history.Current == zero { - history.Current = now.Add(-(history.Duration)) - } - if history.First == zero { - history.First = history.Current - } - if history.End == zero { - history.End = now - } - } - } - return requests -} - -// CreateTopicOptionsFromRequest transforms histories attached to a single request to a simpler format - TopicOptions. -func CreateTopicOptionsFromRequest(req db.HistoryRequest) TopicOptions { - histories := req.Histories() - rst := make(TopicOptions, len(histories)) - for i := range histories { - history := histories[i] - rst[i] = TopicOption{ - Topic: history.Topic, - Range: Range{ - Start: uint64(history.Current.Add(-(WhisperTimeAllowance)).Unix()), - End: uint64(history.End.Unix()), - }, - } - } - return rst -} - -func mapToList(topics map[types.TopicType]db.TopicHistory) []db.TopicHistory { - rst := make([]db.TopicHistory, 0, len(topics)) - for key := range topics { - rst = append(rst, topics[key]) - } - return rst -} - -// GroupHistoriesByRequestTimespan creates requests from provided histories. -// Multiple histories will be included into the same request only if they share timespan. -func GroupHistoriesByRequestTimespan(store db.HistoryStore, histories []db.TopicHistory) []db.HistoryRequest { - requests := []db.HistoryRequest{} - for _, th := range histories { - var added bool - for i := range requests { - req := &requests[i] - histories := req.Histories() - if histories[0].SameRange(th) { - req.AddHistory(th) - added = true - } - } - if !added { - req := store.NewRequest() - req.AddHistory(th) - requests = append(requests, req) - } - } - return requests -} - -// Range of the request. -type Range struct { - Start uint64 - End uint64 -} - -// TopicOption request for a single topic. -type TopicOption struct { - Topic types.TopicType - Range Range -} - -// TopicOptions is a list of topic-based requsts. -type TopicOptions []TopicOption - -// ToBloomFilterOption creates bloom filter request from a list of topics. -func (options TopicOptions) ToBloomFilterOption() BloomFilterOption { - topics := make([]types.TopicType, len(options)) - var start, end uint64 - for i := range options { - opt := options[i] - topics[i] = opt.Topic - if opt.Range.Start > start { - start = opt.Range.Start - } - if opt.Range.End > end { - end = opt.Range.End - } - } - - return BloomFilterOption{ - Range: Range{Start: start, End: end}, - Filter: topicsToBloom(topics...), - } -} - -// Topics returns list of whisper TopicType attached to each TopicOption. -func (options TopicOptions) Topics() []types.TopicType { - rst := make([]types.TopicType, len(options)) - for i := range options { - rst[i] = options[i].Topic - } - return rst -} - -// BloomFilterOption is a request based on bloom filter. -type BloomFilterOption struct { - Range Range - Filter []byte -} - -// ToMessagesRequestPayload creates mailserver.MessagesRequestPayload and encodes it to bytes using rlp. -func (filter BloomFilterOption) ToMessagesRequestPayload() ([]byte, error) { - // TODO fix this conversion. - // we start from time.Duration which is int64, then convert to uint64 for rlp-serilizability - // why uint32 here? max uint32 is smaller than max int64 - payload := mailserver.MessagesRequestPayload{ - Lower: uint32(filter.Range.Start), - Upper: uint32(filter.Range.End), - Bloom: filter.Filter, - // Client must tell the MailServer if it supports batch responses. - // This can be removed in the future. - Batch: true, - Limit: 1000, - } - return rlp.EncodeToBytes(payload) -} diff --git a/services/shhext/history_geth_test.go b/services/shhext/history_geth_test.go deleted file mode 100644 index 27c6f6a423..0000000000 --- a/services/shhext/history_geth_test.go +++ /dev/null @@ -1,360 +0,0 @@ -// +build !nimbus - -package shhext - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/ethereum/go-ethereum/rlp" - - "github.com/status-im/status-go/db" - "github.com/status-im/status-go/eth-node/types" - "github.com/status-im/status-go/mailserver" -) - -func newTestContext(t *testing.T) Context { - mdb, err := db.NewMemoryDB() - require.NoError(t, err) - return NewContext(context.Background(), time.Now, NewRequestsRegistry(0), db.NewLevelDBStorage(mdb)) -} - -func createInMemStore(t *testing.T) db.HistoryStore { - mdb, err := db.NewMemoryDB() - require.NoError(t, err) - return db.NewHistoryStore(db.NewLevelDBStorage(mdb)) -} - -func TestRenewRequest(t *testing.T) { - req := db.HistoryRequest{} - duration := time.Hour - req.AddHistory(db.TopicHistory{Duration: duration}) - - firstNow := time.Now() - RenewRequests([]db.HistoryRequest{req}, firstNow) - - initial := firstNow.Add(-duration).Unix() - - th := req.Histories()[0] - require.Equal(t, initial, th.Current.Unix()) - require.Equal(t, initial, th.First.Unix()) - require.Equal(t, firstNow.Unix(), th.End.Unix()) - - secondNow := time.Now() - RenewRequests([]db.HistoryRequest{req}, secondNow) - - require.Equal(t, initial, th.Current.Unix()) - require.Equal(t, initial, th.First.Unix()) - require.Equal(t, secondNow.Unix(), th.End.Unix()) -} - -func TestCreateTopicOptionsFromRequest(t *testing.T) { - req := db.HistoryRequest{} - topic := types.TopicType{1} - now := time.Now() - req.AddHistory(db.TopicHistory{Topic: topic, Current: now, End: now}) - options := CreateTopicOptionsFromRequest(req) - require.Len(t, options, len(req.Histories()), - "length must be equal to the number of topic histories attached to request") - require.Equal(t, topic, options[0].Topic) - require.Equal(t, uint64(now.Add(-WhisperTimeAllowance).Unix()), options[0].Range.Start, - "start of the range must be adjusted by the whisper time allowance") - require.Equal(t, uint64(now.Unix()), options[0].Range.End) -} - -func TestTopicOptionsToBloom(t *testing.T) { - options := TopicOptions{ - {Topic: types.TopicType{1}, Range: Range{Start: 1, End: 10}}, - {Topic: types.TopicType{2}, Range: Range{Start: 3, End: 12}}, - } - bloom := options.ToBloomFilterOption() - require.Equal(t, uint64(3), bloom.Range.Start, "Start must be the latest Start across all options") - require.Equal(t, uint64(12), bloom.Range.End, "End must be the latest End across all options") - require.Equal(t, topicsToBloom(options[0].Topic, options[1].Topic), bloom.Filter) -} - -func TestBloomFilterToMessageRequestPayload(t *testing.T) { - var ( - start uint32 = 10 - end uint32 = 20 - filter = []byte{1, 1, 1, 1} - message = mailserver.MessagesRequestPayload{ - Lower: start, - Upper: end, - Bloom: filter, - Batch: true, - Limit: 1000, - } - bloomOption = BloomFilterOption{ - Filter: filter, - Range: Range{ - Start: uint64(start), - End: uint64(end), - }, - } - ) - expected, err := rlp.EncodeToBytes(message) - require.NoError(t, err) - payload, err := bloomOption.ToMessagesRequestPayload() - require.NoError(t, err) - require.Equal(t, expected, payload) -} - -func TestCreateRequestsEmptyState(t *testing.T) { - ctx := newTestContext(t) - reactor := NewHistoryUpdateReactor() - requests, err := reactor.CreateRequests(ctx, []TopicRequest{ - {Topic: types.TopicType{1}, Duration: time.Hour}, - {Topic: types.TopicType{2}, Duration: time.Hour}, - {Topic: types.TopicType{3}, Duration: 10 * time.Hour}, - }) - require.NoError(t, err) - require.Len(t, requests, 2) - var ( - oneTopic, twoTopic db.HistoryRequest - ) - if len(requests[0].Histories()) == 1 { - oneTopic, twoTopic = requests[0], requests[1] - } else { - oneTopic, twoTopic = requests[1], requests[0] - } - require.Len(t, oneTopic.Histories(), 1) - require.Len(t, twoTopic.Histories(), 2) - -} - -func TestCreateRequestsWithExistingRequest(t *testing.T) { - ctx := newTestContext(t) - store := ctx.HistoryStore() - req := store.NewRequest() - req.ID = types.Hash{1} - th := store.NewHistory(types.TopicType{1}, time.Hour) - req.AddHistory(th) - require.NoError(t, req.Save()) - reactor := NewHistoryUpdateReactor() - requests, err := reactor.CreateRequests(ctx, []TopicRequest{ - {Topic: types.TopicType{1}, Duration: time.Hour}, - {Topic: types.TopicType{2}, Duration: time.Hour}, - {Topic: types.TopicType{3}, Duration: time.Hour}, - }) - require.NoError(t, err) - require.Len(t, requests, 2) - - var ( - oneTopic, twoTopic db.HistoryRequest - ) - if len(requests[0].Histories()) == 1 { - oneTopic, twoTopic = requests[0], requests[1] - } else { - oneTopic, twoTopic = requests[1], requests[0] - } - assert.Len(t, oneTopic.Histories(), 1) - assert.Len(t, twoTopic.Histories(), 2) -} - -func TestCreateMultiRequestsWithSameTopic(t *testing.T) { - ctx := newTestContext(t) - store := ctx.HistoryStore() - reactor := NewHistoryUpdateReactor() - topic := types.TopicType{1} - requests, err := reactor.CreateRequests(ctx, []TopicRequest{ - {Topic: topic, Duration: time.Hour}, - }) - require.NoError(t, err) - require.Len(t, requests, 1) - requests[0].ID = types.Hash{1} - require.NoError(t, requests[0].Save()) - - // duration changed. request wasn't finished - requests, err = reactor.CreateRequests(ctx, []TopicRequest{ - {Topic: topic, Duration: 10 * time.Hour}, - }) - require.NoError(t, err) - require.Len(t, requests, 2) - longest := 0 - for i := range requests { - r := &requests[i] - r.ID = types.Hash{byte(i)} - require.NoError(t, r.Save()) - require.Len(t, r.Histories(), 1) - if r.Histories()[0].Duration == 10*time.Hour { - longest = i - } - } - require.Equal(t, requests[longest].Histories()[0].End, requests[longest^1].Histories()[0].First) - - for _, r := range requests { - require.NoError(t, reactor.UpdateFinishedRequest(ctx, r.ID)) - } - requests, err = reactor.CreateRequests(ctx, []TopicRequest{ - {Topic: topic, Duration: 10 * time.Hour}, - }) - require.NoError(t, err) - require.Len(t, requests, 1) - - topics, err := store.GetHistoriesByTopic(topic) - require.NoError(t, err) - require.Len(t, topics, 1) - require.Equal(t, 10*time.Hour, topics[0].Duration) -} - -func TestRequestFinishedUpdate(t *testing.T) { - ctx := newTestContext(t) - store := ctx.HistoryStore() - req := store.NewRequest() - req.ID = types.Hash{1} - now := ctx.Time() - thOne := store.NewHistory(types.TopicType{1}, time.Hour) - thOne.End = now - thTwo := store.NewHistory(types.TopicType{2}, time.Hour) - thTwo.End = now - req.AddHistory(thOne) - req.AddHistory(thTwo) - require.NoError(t, req.Save()) - - reactor := NewHistoryUpdateReactor() - require.NoError(t, reactor.UpdateTopicHistory(ctx, thOne.Topic, now.Add(-time.Minute))) - require.NoError(t, reactor.UpdateFinishedRequest(ctx, req.ID)) - _, err := store.GetRequest(req.ID) - require.EqualError(t, err, "leveldb: not found") - - require.NoError(t, thOne.Load()) - require.NoError(t, thTwo.Load()) - require.Equal(t, now.Unix(), thOne.Current.Unix()) - require.Equal(t, now.Unix(), thTwo.Current.Unix()) -} - -func TestTopicHistoryUpdate(t *testing.T) { - ctx := newTestContext(t) - store := ctx.HistoryStore() - reqID := types.Hash{1} - request := store.NewRequest() - request.ID = reqID - now := time.Now() - require.NoError(t, request.Save()) - th := store.NewHistory(types.TopicType{1}, time.Hour) - th.RequestID = request.ID - th.End = now - require.NoError(t, th.Save()) - reactor := NewHistoryUpdateReactor() - timestamp := now.Add(-time.Minute) - - require.NoError(t, reactor.UpdateTopicHistory(ctx, th.Topic, timestamp)) - require.NoError(t, th.Load()) - require.Equal(t, timestamp.Unix(), th.Current.Unix()) - - require.NoError(t, reactor.UpdateTopicHistory(ctx, th.Topic, now)) - require.NoError(t, th.Load()) - require.Equal(t, timestamp.Unix(), th.Current.Unix()) -} - -func TestGroupHistoriesByRequestTimestamp(t *testing.T) { - requests := GroupHistoriesByRequestTimespan(createInMemStore(t), []db.TopicHistory{ - {Topic: types.TopicType{1}, Duration: time.Hour}, - {Topic: types.TopicType{2}, Duration: time.Hour}, - {Topic: types.TopicType{3}, Duration: 2 * time.Hour}, - {Topic: types.TopicType{4}, Duration: 2 * time.Hour}, - {Topic: types.TopicType{5}, Duration: 3 * time.Hour}, - {Topic: types.TopicType{6}, Duration: 3 * time.Hour}, - }) - require.Len(t, requests, 3) - for _, req := range requests { - require.Len(t, req.Histories(), 2) - } -} - -// initial creation of the history index. no other histories in store -func TestAdjustHistoryWithNoOtherHistories(t *testing.T) { - store := createInMemStore(t) - th := store.NewHistory(types.TopicType{1}, time.Hour) - adjusted, err := adjustRequestedHistories(store, []db.TopicHistory{th}) - require.NoError(t, err) - require.Len(t, adjusted, 1) - require.Equal(t, th.Topic, adjusted[0].Topic) -} - -// Duration for the history index with same topic was gradually incresed: -// {Duration: 1h} {Duration: 2h} {Duration: 3h} -// But actual request wasn't sent -// So when we receive {Duration: 4h} we can merge all of them into single index -// that covers all of them e.g. {Duration: 4h} -func TestAdjustHistoryWithExistingLowerRanges(t *testing.T) { - store := createInMemStore(t) - topic := types.TopicType{1} - histories := make([]db.TopicHistory, 3) - i := 0 - for i = range histories { - histories[i] = store.NewHistory(topic, time.Duration(i+1)*time.Hour) - require.NoError(t, histories[i].Save()) - } - i++ - th := store.NewHistory(topic, time.Duration(i+1)*time.Hour) - adjusted, err := adjustRequestedHistories(store, []db.TopicHistory{th}) - require.NoError(t, err) - require.Len(t, adjusted, 1) - require.Equal(t, th.Duration, adjusted[0].Duration) - - all, err := store.GetHistoriesByTopic(topic) - require.NoError(t, err) - require.Len(t, all, 1) - require.Equal(t, th.Duration, all[0].Duration) -} - -// Precondition is based on the previous test. We have same information in the database -// but now every history index request was successfully completed. And End timstamp is set to the First of the next index. -// So, we have: -// {First: now-1h, End: now} {First: now-2h, End: now-1h} {First: now-3h: End: now-2h} -// When we want to create new request with {Duration: 4h} -// We see that there is no reason to keep all indexes and we can squash them. -func TestAdjustHistoriesWithExistingCoveredLowerRanges(t *testing.T) { - store := createInMemStore(t) - topic := types.TopicType{1} - histories := make([]db.TopicHistory, 3) - i := 0 - now := time.Now() - for i = range histories { - duration := time.Duration(i+1) * time.Hour - prevduration := time.Duration(i) * time.Hour - histories[i] = store.NewHistory(topic, duration) - histories[i].First = now.Add(-duration) - histories[i].Current = now.Add(-prevduration) - require.NoError(t, histories[i].Save()) - } - i++ - th := store.NewHistory(topic, time.Duration(i+1)*time.Hour) - th.Current = now.Add(-time.Duration(i) * time.Hour) - adjusted, err := adjustRequestedHistories(store, []db.TopicHistory{th}) - require.NoError(t, err) - require.Len(t, adjusted, 1) - require.Equal(t, th.Duration, adjusted[0].Duration) -} - -func TestAdjustHistoryReplaceTopicWithHigherDuration(t *testing.T) { - store := createInMemStore(t) - topic := types.TopicType{1} - hour := store.NewHistory(topic, time.Hour) - require.NoError(t, hour.Save()) - minute := store.NewHistory(topic, time.Minute) - adjusted, err := adjustRequestedHistories(store, []db.TopicHistory{minute}) - require.NoError(t, err) - require.Len(t, adjusted, 1) - require.Equal(t, hour.Duration, adjusted[0].Duration) -} - -// if client requested lower duration than the one we have in the index already it will -// it will be discarded and we will use existing index -func TestAdjustHistoryRemoveTopicIfPendingWithHigherDuration(t *testing.T) { - store := createInMemStore(t) - topic := types.TopicType{1} - hour := store.NewHistory(topic, time.Hour) - hour.RequestID = types.Hash{1} - require.NoError(t, hour.Save()) - minute := store.NewHistory(topic, time.Minute) - adjusted, err := adjustRequestedHistories(store, []db.TopicHistory{minute}) - require.NoError(t, err) - require.Len(t, adjusted, 0) -} diff --git a/services/shhext/service.go b/services/shhext/service.go index 0449f8c9b5..6c20a41381 100644 --- a/services/shhext/service.go +++ b/services/shhext/service.go @@ -4,335 +4,50 @@ package shhext import ( "context" - "crypto/ecdsa" - "database/sql" "fmt" - "math/big" - "os" - "path/filepath" "time" - "github.com/status-im/status-go/logutils" + "github.com/syndtr/goleveldb/leveldb" - commongethtypes "github.com/ethereum/go-ethereum/common" - gethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/rpc" - "github.com/status-im/status-go/db" - "github.com/status-im/status-go/multiaccounts/accounts" - "github.com/status-im/status-go/params" - "github.com/status-im/status-go/services/shhext/mailservers" - "github.com/status-im/status-go/services/wallet" - "github.com/status-im/status-go/signal" - - "github.com/syndtr/goleveldb/leveldb" - "go.uber.org/zap" - - coretypes "github.com/status-im/status-go/eth-node/core/types" "github.com/status-im/status-go/eth-node/types" - "github.com/status-im/status-go/protocol" - "github.com/status-im/status-go/protocol/transport" -) - -const ( - // defaultConnectionsTarget used in Service.Start if configured connection target is 0. - defaultConnectionsTarget = 1 - // defaultTimeoutWaitAdded is a timeout to use to establish initial connections. - defaultTimeoutWaitAdded = 5 * time.Second + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/ext" ) -// EnvelopeEventsHandler used for two different event types. -type EnvelopeEventsHandler interface { - EnvelopeSent([][]byte) - EnvelopeExpired([][]byte, error) - MailServerRequestCompleted(types.Hash, types.Hash, []byte, error) - MailServerRequestExpired(types.Hash) -} - -// Service is a service that provides some additional Whisper API. type Service struct { - apiName string - messenger *protocol.Messenger - identity *ecdsa.PrivateKey - cancelMessenger chan struct{} - storage db.TransactionalStorage - n types.Node - w types.Whisper - config params.ShhextConfig - mailMonitor *MailRequestMonitor - requestsRegistry *RequestsRegistry - historyUpdates *HistoryUpdateReactor - server *p2p.Server - nodeID *ecdsa.PrivateKey - peerStore *mailservers.PeerStore - cache *mailservers.Cache - connManager *mailservers.ConnectionManager - lastUsedMonitor *mailservers.LastUsedConnectionMonitor - accountsDB *accounts.Database + *ext.Service + w types.Whisper } -// Make sure that Service implements node.Service interface. -var _ node.Service = (*Service)(nil) - -// New returns a new shhext Service. -func New(n types.Node, ctx interface{}, apiName string, handler EnvelopeEventsHandler, ldb *leveldb.DB, config params.ShhextConfig) *Service { +func New(config params.ShhextConfig, n types.Node, ctx interface{}, handler ext.EnvelopeEventsHandler, ldb *leveldb.DB) *Service { w, err := n.GetWhisper(ctx) if err != nil { panic(err) } - cache := mailservers.NewCache(ldb) - ps := mailservers.NewPeerStore(cache) - delay := defaultRequestsDelay + delay := ext.DefaultRequestsDelay if config.RequestsDelay != 0 { delay = config.RequestsDelay } - requestsRegistry := NewRequestsRegistry(delay) - historyUpdates := NewHistoryUpdateReactor() - mailMonitor := &MailRequestMonitor{ - w: w, - handler: handler, - cache: map[types.Hash]EnvelopeState{}, - requestsRegistry: requestsRegistry, - } + requestsRegistry := ext.NewRequestsRegistry(delay) + mailMonitor := ext.NewMailRequestMonitor(w, handler, requestsRegistry) return &Service{ - apiName: apiName, - storage: db.NewLevelDBStorage(ldb), - n: n, - w: w, - config: config, - mailMonitor: mailMonitor, - requestsRegistry: requestsRegistry, - historyUpdates: historyUpdates, - peerStore: ps, - cache: cache, - } -} - -func (s *Service) InitProtocol(identity *ecdsa.PrivateKey, db *sql.DB) error { // nolint: gocyclo - if !s.config.PFSEnabled { - return nil - } - - // If Messenger has been already set up, we need to shut it down - // before we init it again. Otherwise, it will lead to goroutines leakage - // due to not stopped filters. - if s.messenger != nil { - if err := s.messenger.Shutdown(); err != nil { - return err - } - } - - s.identity = identity - - dataDir := filepath.Clean(s.config.BackupDisabledDataDir) - - if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { - return err - } - - // Create a custom zap.Logger which will forward logs from status-go/protocol to status-go logger. - zapLogger, err := logutils.NewZapLoggerWithAdapter(logutils.Logger()) - if err != nil { - return err - } - - envelopesMonitorConfig := &transport.EnvelopesMonitorConfig{ - MaxAttempts: s.config.MaxMessageDeliveryAttempts, - MailserverConfirmationsEnabled: s.config.MailServerConfirmations, - IsMailserver: func(peer types.EnodeID) bool { - return s.peerStore.Exist(peer) - }, - EnvelopeEventsHandler: EnvelopeSignalHandler{}, - Logger: zapLogger, - } - options := buildMessengerOptions(s.config, db, envelopesMonitorConfig, zapLogger) - - messenger, err := protocol.NewMessenger( - identity, - s.n, - s.config.InstallationID, - options..., - ) - if err != nil { - return err - } - s.accountsDB = accounts.NewDB(db) - s.messenger = messenger - // Start a loop that retrieves all messages and propagates them to status-react. - s.cancelMessenger = make(chan struct{}) - go s.retrieveMessagesLoop(time.Second, s.cancelMessenger) - go s.verifyTransactionLoop(30*time.Second, s.cancelMessenger) - - return s.messenger.Init() -} - -func (s *Service) retrieveMessagesLoop(tick time.Duration, cancel <-chan struct{}) { - ticker := time.NewTicker(tick) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - response, err := s.messenger.RetrieveAll() - if err != nil { - log.Error("failed to retrieve raw messages", "err", err) - continue - } - if !response.IsEmpty() { - PublisherSignalHandler{}.NewMessages(response) - } - case <-cancel: - return - } - } -} - -type verifyTransactionClient struct { - chainID *big.Int - url string -} - -func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, coretypes.TransactionStatus, error) { - signer := gethtypes.NewEIP155Signer(c.chainID) - client, err := ethclient.Dial(c.url) - if err != nil { - return coretypes.Message{}, coretypes.TransactionStatusPending, err - } - - transaction, pending, err := client.TransactionByHash(ctx, commongethtypes.BytesToHash(hash.Bytes())) - if err != nil { - return coretypes.Message{}, coretypes.TransactionStatusPending, err - } - - message, err := transaction.AsMessage(signer) - if err != nil { - return coretypes.Message{}, coretypes.TransactionStatusPending, err - } - from := types.BytesToAddress(message.From().Bytes()) - to := types.BytesToAddress(message.To().Bytes()) - - if pending { - return coretypes.NewMessage( - from, - &to, - message.Nonce(), - message.Value(), - message.Gas(), - message.GasPrice(), - message.Data(), - message.CheckNonce(), - ), coretypes.TransactionStatusPending, nil - } - - receipt, err := client.TransactionReceipt(ctx, commongethtypes.BytesToHash(hash.Bytes())) - if err != nil { - return coretypes.Message{}, coretypes.TransactionStatusPending, err - } - - coremessage := coretypes.NewMessage( - from, - &to, - message.Nonce(), - message.Value(), - message.Gas(), - message.GasPrice(), - message.Data(), - message.CheckNonce(), - ) - - // Token transfer, check the logs - if len(coremessage.Data()) != 0 { - if wallet.IsTokenTransfer(receipt.Logs) { - return coremessage, coretypes.TransactionStatus(receipt.Status), nil - } else { - return coremessage, coretypes.TransactionStatusFailed, nil - } - + Service: ext.New(config, n, ldb, mailMonitor, requestsRegistry, w), + w: w, } - - return coremessage, coretypes.TransactionStatus(receipt.Status), nil - } -func (s *Service) verifyTransactionLoop(tick time.Duration, cancel <-chan struct{}) { - if s.config.VerifyTransactionURL == "" { - log.Warn("not starting transaction loop") - return - } - - ticker := time.NewTicker(tick) - defer ticker.Stop() - - ctx, cancelVerifyTransaction := context.WithCancel(context.Background()) - - for { - select { - case <-ticker.C: - accounts, err := s.accountsDB.GetAccounts() - if err != nil { - log.Error("failed to retrieve accounts", "err", err) - } - var wallets []types.Address - for _, account := range accounts { - if account.Wallet { - wallets = append(wallets, types.BytesToAddress(account.Address.Bytes())) - } - } - - response, err := s.messenger.ValidateTransactions(ctx, wallets) - if err != nil { - log.Error("failed to validate transactions", "err", err) - continue - } - if !response.IsEmpty() { - PublisherSignalHandler{}.NewMessages(response) - } - case <-cancel: - cancelVerifyTransaction() - return - } - } -} - -func (s *Service) ConfirmMessagesProcessed(messageIDs [][]byte) error { - return s.messenger.ConfirmMessagesProcessed(messageIDs) -} - -func (s *Service) EnableInstallation(installationID string) error { - return s.messenger.EnableInstallation(installationID) -} - -// DisableInstallation disables an installation for multi-device sync. -func (s *Service) DisableInstallation(installationID string) error { - return s.messenger.DisableInstallation(installationID) -} - -// UpdateMailservers updates information about selected mail servers. -func (s *Service) UpdateMailservers(nodes []*enode.Node) error { - if err := s.peerStore.Update(nodes); err != nil { - return err - } - if s.connManager != nil { - s.connManager.Notify(nodes) - } - return nil -} - -// Protocols returns a new protocols list. In this case, there are none. -func (s *Service) Protocols() []p2p.Protocol { - return []p2p.Protocol{} +func (s *Service) PublicWhisperAPI() types.PublicWhisperAPI { + return s.w.PublicWhisperAPI() } // APIs returns a list of new APIs. func (s *Service) APIs() []rpc.API { apis := []rpc.API{ { - Namespace: s.apiName, + Namespace: "shhext", Version: "1.0", Service: NewPublicAPI(s), Public: true, @@ -341,67 +56,7 @@ func (s *Service) APIs() []rpc.API { return apis } -// Start is run when a service is started. -// It does nothing in this case but is required by `node.Service` interface. -func (s *Service) Start(server *p2p.Server) error { - if s.config.EnableConnectionManager { - connectionsTarget := s.config.ConnectionTarget - if connectionsTarget == 0 { - connectionsTarget = defaultConnectionsTarget - } - maxFailures := s.config.MaxServerFailures - // if not defined change server on first expired event - if maxFailures == 0 { - maxFailures = 1 - } - s.connManager = mailservers.NewConnectionManager(server, s.w, connectionsTarget, maxFailures, defaultTimeoutWaitAdded) - s.connManager.Start() - if err := mailservers.EnsureUsedRecordsAddedFirst(s.peerStore, s.connManager); err != nil { - return err - } - } - if s.config.EnableLastUsedMonitor { - s.lastUsedMonitor = mailservers.NewLastUsedConnectionMonitor(s.peerStore, s.cache, s.w) - s.lastUsedMonitor.Start() - } - s.mailMonitor.Start() - s.nodeID = server.PrivateKey - s.server = server - return nil -} - -// Stop is run when a service is stopped. -func (s *Service) Stop() error { - log.Info("Stopping shhext service") - if s.config.EnableConnectionManager { - s.connManager.Stop() - } - if s.config.EnableLastUsedMonitor { - s.lastUsedMonitor.Stop() - } - s.requestsRegistry.Clear() - s.mailMonitor.Stop() - - if s.cancelMessenger != nil { - select { - case <-s.cancelMessenger: - // channel already closed - default: - close(s.cancelMessenger) - s.cancelMessenger = nil - } - } - - if s.messenger != nil { - if err := s.messenger.Shutdown(); err != nil { - return err - } - } - - return nil -} - -func (s *Service) syncMessages(ctx context.Context, mailServerID []byte, r types.SyncMailRequest) (resp types.SyncEventResponse, err error) { +func (s *Service) SyncMessages(ctx context.Context, mailServerID []byte, r types.SyncMailRequest) (resp types.SyncEventResponse, err error) { err = s.w.SyncMessages(mailServerID, r) if err != nil { return @@ -443,52 +98,3 @@ func (s *Service) syncMessages(ctx context.Context, mailServerID []byte, r types } } } - -func onNegotiatedFilters(filters []*transport.Filter) { - var signalFilters []*signal.Filter - for _, filter := range filters { - - signalFilter := &signal.Filter{ - ChatID: filter.ChatID, - SymKeyID: filter.SymKeyID, - Listen: filter.Listen, - FilterID: filter.FilterID, - Identity: filter.Identity, - Topic: filter.Topic, - } - - signalFilters = append(signalFilters, signalFilter) - } - if len(filters) != 0 { - handler := PublisherSignalHandler{} - handler.WhisperFilterAdded(signalFilters) - } -} - -func buildMessengerOptions( - config params.ShhextConfig, - db *sql.DB, - envelopesMonitorConfig *transport.EnvelopesMonitorConfig, - logger *zap.Logger, -) []protocol.Option { - options := []protocol.Option{ - protocol.WithCustomLogger(logger), - protocol.WithDatabase(db), - protocol.WithEnvelopesMonitorConfig(envelopesMonitorConfig), - protocol.WithOnNegotiatedFilters(onNegotiatedFilters), - } - - if config.DataSyncEnabled { - options = append(options, protocol.WithDatasync()) - } - - if config.VerifyTransactionURL != "" { - client := &verifyTransactionClient{ - url: config.VerifyTransactionURL, - chainID: big.NewInt(config.VerifyTransactionChainID), - } - options = append(options, protocol.WithVerifyTransactionClient(client)) - } - - return options -} diff --git a/services/shhext/service_nimbus.go b/services/shhext/service_nimbus.go index 452dc93ec1..198207758d 100644 --- a/services/shhext/service_nimbus.go +++ b/services/shhext/service_nimbus.go @@ -4,358 +4,59 @@ package shhext import ( "context" - "crypto/ecdsa" - "database/sql" "fmt" - "os" - "path/filepath" "time" - "github.com/status-im/status-go/logutils" + "github.com/syndtr/goleveldb/leveldb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" - "github.com/status-im/status-go/db" - "github.com/status-im/status-go/params" - nimbussvc "github.com/status-im/status-go/services/nimbus" - "github.com/status-im/status-go/signal" - - "github.com/syndtr/goleveldb/leveldb" - "go.uber.org/zap" - "github.com/status-im/status-go/eth-node/types" - "github.com/status-im/status-go/protocol" - "github.com/status-im/status-go/protocol/transport" -) - -const ( - // defaultConnectionsTarget used in Service.Start if configured connection target is 0. - defaultConnectionsTarget = 1 - // defaultTimeoutWaitAdded is a timeout to use to establish initial connections. - defaultTimeoutWaitAdded = 5 * time.Second + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/ext" ) -// EnvelopeEventsHandler used for two different event types. -type EnvelopeEventsHandler interface { - EnvelopeSent([][]byte) - EnvelopeExpired([][]byte, error) - MailServerRequestCompleted(types.Hash, types.Hash, []byte, error) - MailServerRequestExpired(types.Hash) +type Service struct { + *ext.Service + w types.Whisper } -// NimbusService is a service that provides some additional Whisper API. -type NimbusService struct { - apiName string - messenger *protocol.Messenger - identity *ecdsa.PrivateKey - cancelMessenger chan struct{} - storage db.TransactionalStorage - n types.Node - w types.Whisper - config params.ShhextConfig - // mailMonitor *MailRequestMonitor - // requestsRegistry *RequestsRegistry - // historyUpdates *HistoryUpdateReactor - // server *p2p.Server - nodeID *ecdsa.PrivateKey - // peerStore *mailservers.PeerStore - // cache *mailservers.Cache - // connManager *mailservers.ConnectionManager - // lastUsedMonitor *mailservers.LastUsedConnectionMonitor - // accountsDB *accounts.Database -} - -// Make sure that NimbusService implements nimbussvc.Service interface. -var _ nimbussvc.Service = (*NimbusService)(nil) - -// NewNimbus returns a new shhext NimbusService. -func NewNimbus(n types.Node, ctx interface{}, apiName string, ldb *leveldb.DB, config params.ShhextConfig) *NimbusService { +func New(config params.ShhextConfig, n types.Node, ctx interface{}, handler ext.EnvelopeEventsHandler, ldb *leveldb.DB) *Service { w, err := n.GetWhisper(ctx) if err != nil { panic(err) } - // cache := mailservers.NewCache(ldb) - // ps := mailservers.NewPeerStore(cache) - // delay := defaultRequestsDelay - // if config.RequestsDelay != 0 { - // delay = config.RequestsDelay - // } - // requestsRegistry := NewRequestsRegistry(delay) - // historyUpdates := NewHistoryUpdateReactor() - // mailMonitor := &MailRequestMonitor{ - // w: w, - // handler: handler, - // cache: map[types.Hash]EnvelopeState{}, - // requestsRegistry: requestsRegistry, - // } - return &NimbusService{ - apiName: apiName, - storage: db.NewLevelDBStorage(ldb), - n: n, - w: w, - config: config, - // mailMonitor: mailMonitor, - // requestsRegistry: requestsRegistry, - // historyUpdates: historyUpdates, - // peerStore: ps, - // cache: cache, - } -} - -func (s *NimbusService) InitProtocol(identity *ecdsa.PrivateKey, db *sql.DB) error { // nolint: gocyclo - if !s.config.PFSEnabled { - return nil - } - - // If Messenger has been already set up, we need to shut it down - // before we init it again. Otherwise, it will lead to goroutines leakage - // due to not stopped filters. - if s.messenger != nil { - if err := s.messenger.Shutdown(); err != nil { - return err - } - } - - s.identity = identity - - dataDir := filepath.Clean(s.config.BackupDisabledDataDir) - - if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { - return err - } - - // Create a custom zap.Logger which will forward logs from status-go/protocol to status-go logger. - zapLogger, err := logutils.NewZapLoggerWithAdapter(logutils.Logger()) - if err != nil { - return err + delay := ext.DefaultRequestsDelay + if config.RequestsDelay != 0 { + delay = config.RequestsDelay } - - // envelopesMonitorConfig := &protocolwhisper.EnvelopesMonitorConfig{ - // MaxAttempts: s.config.MaxMessageDeliveryAttempts, - // MailserverConfirmationsEnabled: s.config.MailServerConfirmations, - // IsMailserver: func(peer types.EnodeID) bool { - // return s.peerStore.Exist(peer) - // }, - // EnvelopeEventsHandler: EnvelopeSignalHandler{}, - // Logger: zapLogger, - // } - options := buildMessengerOptions(s.config, db, nil, zapLogger) - - messenger, err := protocol.NewMessenger( - identity, - s.n, - s.config.InstallationID, - options..., - ) - if err != nil { - return err - } - // s.accountsDB = accounts.NewDB(db) - s.messenger = messenger - // Start a loop that retrieves all messages and propagates them to status-react. - s.cancelMessenger = make(chan struct{}) - go s.retrieveMessagesLoop(time.Second, s.cancelMessenger) - // go s.verifyTransactionLoop(30*time.Second, s.cancelMessenger) - - return s.messenger.Init() -} - -func (s *NimbusService) retrieveMessagesLoop(tick time.Duration, cancel <-chan struct{}) { - ticker := time.NewTicker(tick) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - response, err := s.messenger.RetrieveAll() - if err != nil { - log.Error("failed to retrieve raw messages", "err", err) - continue - } - if !response.IsEmpty() { - PublisherSignalHandler{}.NewMessages(response) - } - case <-cancel: - return - } + requestsRegistry := ext.NewRequestsRegistry(delay) + mailMonitor := ext.NewMailRequestMonitor(w, handler, requestsRegistry) + return &Service{ + Service: ext.New(config, n, ldb, mailMonitor, requestsRegistry, w), + w: w, } } -// type verifyTransactionClient struct { -// chainID *big.Int -// url string -// } - -// func (c *verifyTransactionClient) TransactionByHash(ctx context.Context, hash types.Hash) (coretypes.Message, bool, error) { -// signer := gethtypes.NewEIP155Signer(c.chainID) -// client, err := ethclient.Dial(c.url) -// if err != nil { -// return coretypes.Message{}, false, err -// } - -// transaction, pending, err := client.TransactionByHash(ctx, commongethtypes.BytesToHash(hash.Bytes())) -// if err != nil { -// return coretypes.Message{}, false, err -// } - -// message, err := transaction.AsMessage(signer) -// if err != nil { -// return coretypes.Message{}, false, err -// } -// from := types.BytesToAddress(message.From().Bytes()) -// to := types.BytesToAddress(message.To().Bytes()) - -// return coretypes.NewMessage( -// from, -// &to, -// message.Nonce(), -// message.Value(), -// message.Gas(), -// message.GasPrice(), -// message.Data(), -// message.CheckNonce(), -// ), pending, nil -// } - -// func (s *Service) verifyTransactionLoop(tick time.Duration, cancel <-chan struct{}) { -// if s.config.VerifyTransactionURL == "" { -// log.Warn("not starting transaction loop") -// return -// } - -// ticker := time.NewTicker(tick) -// defer ticker.Stop() - -// ctx, cancelVerifyTransaction := context.WithCancel(context.Background()) - -// for { -// select { -// case <-ticker.C: -// accounts, err := s.accountsDB.GetAccounts() -// if err != nil { -// log.Error("failed to retrieve accounts", "err", err) -// } -// var wallets []types.Address -// for _, account := range accounts { -// if account.Wallet { -// wallets = append(wallets, types.BytesToAddress(account.Address.Bytes())) -// } -// } - -// response, err := s.messenger.ValidateTransactions(ctx, wallets) -// if err != nil { -// log.Error("failed to validate transactions", "err", err) -// continue -// } -// if !response.IsEmpty() { -// PublisherSignalHandler{}.NewMessages(response) -// } -// case <-cancel: -// cancelVerifyTransaction() -// return -// } -// } -// } - -func (s *NimbusService) ConfirmMessagesProcessed(messageIDs [][]byte) error { - return s.messenger.ConfirmMessagesProcessed(messageIDs) -} - -func (s *NimbusService) EnableInstallation(installationID string) error { - return s.messenger.EnableInstallation(installationID) +func (s *Service) PublicWhisperAPI() types.PublicWhisperAPI { + return s.w.PublicWhisperAPI() } -// DisableInstallation disables an installation for multi-device sync. -func (s *NimbusService) DisableInstallation(installationID string) error { - return s.messenger.DisableInstallation(installationID) -} - -// UpdateMailservers updates information about selected mail servers. -// func (s *NimbusService) UpdateMailservers(nodes []*enode.Node) error { -// // if err := s.peerStore.Update(nodes); err != nil { -// // return err -// // } -// // if s.connManager != nil { -// // s.connManager.Notify(nodes) -// // } -// return nil -// } - // APIs returns a list of new APIs. -func (s *NimbusService) APIs() []rpc.API { +func (s *Service) APIs() []rpc.API { apis := []rpc.API{ { - Namespace: s.apiName, + Namespace: "shhext", Version: "1.0", - Service: NewNimbusPublicAPI(s), + Service: NewPublicAPI(s), Public: true, }, } return apis } -// Start is run when a service is started. -// It does nothing in this case but is required by `node.NimbusService` interface. -func (s *NimbusService) StartService() error { - if s.config.EnableConnectionManager { - // connectionsTarget := s.config.ConnectionTarget - // if connectionsTarget == 0 { - // connectionsTarget = defaultConnectionsTarget - // } - // maxFailures := s.config.MaxServerFailures - // // if not defined change server on first expired event - // if maxFailures == 0 { - // maxFailures = 1 - // } - // s.connManager = mailservers.NewConnectionManager(server, s.w, connectionsTarget, maxFailures, defaultTimeoutWaitAdded) - // s.connManager.Start() - // if err := mailservers.EnsureUsedRecordsAddedFirst(s.peerStore, s.connManager); err != nil { - // return err - // } - } - if s.config.EnableLastUsedMonitor { - // s.lastUsedMonitor = mailservers.NewLastUsedConnectionMonitor(s.peerStore, s.cache, s.w) - // s.lastUsedMonitor.Start() - } - // s.mailMonitor.Start() - // s.nodeID = server.PrivateKey - // s.server = server - return nil -} - -// Stop is run when a service is stopped. -func (s *NimbusService) Stop() error { - log.Info("Stopping shhext service") - // if s.config.EnableConnectionManager { - // s.connManager.Stop() - // } - // if s.config.EnableLastUsedMonitor { - // s.lastUsedMonitor.Stop() - // } - // s.requestsRegistry.Clear() - // s.mailMonitor.Stop() - - if s.cancelMessenger != nil { - select { - case <-s.cancelMessenger: - // channel already closed - default: - close(s.cancelMessenger) - s.cancelMessenger = nil - } - } - - if s.messenger != nil { - if err := s.messenger.Shutdown(); err != nil { - return err - } - } - - return nil -} - -func (s *NimbusService) syncMessages(ctx context.Context, mailServerID []byte, r types.SyncMailRequest) (resp types.SyncEventResponse, err error) { +func (s *Service) SyncMessages(ctx context.Context, mailServerID []byte, r types.SyncMailRequest) (resp types.SyncEventResponse, err error) { err = s.w.SyncMessages(mailServerID, r) if err != nil { return @@ -397,52 +98,3 @@ func (s *NimbusService) syncMessages(ctx context.Context, mailServerID []byte, r } } } - -func onNegotiatedFilters(filters []*transport.Filter) { - var signalFilters []*signal.Filter - for _, filter := range filters { - - signalFilter := &signal.Filter{ - ChatID: filter.ChatID, - SymKeyID: filter.SymKeyID, - Listen: filter.Listen, - FilterID: filter.FilterID, - Identity: filter.Identity, - Topic: filter.Topic, - } - - signalFilters = append(signalFilters, signalFilter) - } - if len(filters) != 0 { - handler := PublisherSignalHandler{} - handler.WhisperFilterAdded(signalFilters) - } -} - -func buildMessengerOptions( - config params.ShhextConfig, - db *sql.DB, - envelopesMonitorConfig *transport.EnvelopesMonitorConfig, - logger *zap.Logger, -) []protocol.Option { - options := []protocol.Option{ - protocol.WithCustomLogger(logger), - protocol.WithDatabase(db), - //protocol.WithEnvelopesMonitorConfig(envelopesMonitorConfig), - protocol.WithOnNegotiatedFilters(onNegotiatedFilters), - } - - if config.DataSyncEnabled { - options = append(options, protocol.WithDatasync()) - } - - // if config.VerifyTransactionURL != "" { - // client := &verifyTransactionClient{ - // url: config.VerifyTransactionURL, - // chainID: big.NewInt(config.VerifyTransactionChainID), - // } - // options = append(options, protocol.WithVerifyTransactionClient(client)) - // } - - return options -} diff --git a/services/shhext/service_test.go b/services/shhext/service_test.go deleted file mode 100644 index 5446c8d8f0..0000000000 --- a/services/shhext/service_test.go +++ /dev/null @@ -1,819 +0,0 @@ -package shhext - -import ( - "context" - "encoding/hex" - "errors" - "fmt" - "io/ioutil" - "math" - "net" - "os" - "testing" - "time" - - "github.com/stretchr/testify/suite" - "github.com/syndtr/goleveldb/leveldb" - "github.com/syndtr/goleveldb/leveldb/storage" - "go.uber.org/zap" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - - gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" - "github.com/status-im/status-go/eth-node/types" - enstypes "github.com/status-im/status-go/eth-node/types/ens" - "github.com/status-im/status-go/mailserver" - "github.com/status-im/status-go/params" - "github.com/status-im/status-go/sqlite" - "github.com/status-im/status-go/t/helpers" - "github.com/status-im/status-go/t/utils" - "github.com/status-im/status-go/whisper/v6" -) - -const ( - // internal whisper protocol codes - statusCode = 0 - p2pRequestCompleteCode = 125 -) - -type failureMessage struct { - IDs [][]byte - Error error -} - -func newHandlerMock(buf int) handlerMock { - return handlerMock{ - confirmations: make(chan [][]byte, buf), - expirations: make(chan failureMessage, buf), - requestsCompleted: make(chan types.Hash, buf), - requestsExpired: make(chan types.Hash, buf), - requestsFailed: make(chan types.Hash, buf), - } -} - -type handlerMock struct { - confirmations chan [][]byte - expirations chan failureMessage - requestsCompleted chan types.Hash - requestsExpired chan types.Hash - requestsFailed chan types.Hash -} - -func (t handlerMock) EnvelopeSent(ids [][]byte) { - t.confirmations <- ids -} - -func (t handlerMock) EnvelopeExpired(ids [][]byte, err error) { - t.expirations <- failureMessage{IDs: ids, Error: err} -} - -func (t handlerMock) MailServerRequestCompleted(requestID types.Hash, lastEnvelopeHash types.Hash, cursor []byte, err error) { - if err == nil { - t.requestsCompleted <- requestID - } else { - t.requestsFailed <- requestID - } -} - -func (t handlerMock) MailServerRequestExpired(hash types.Hash) { - t.requestsExpired <- hash -} - -func TestShhExtSuite(t *testing.T) { - suite.Run(t, new(ShhExtSuite)) -} - -type ShhExtSuite struct { - suite.Suite - - nodes []*node.Node - services []*Service - whisperWrapper []types.Whisper - whisper []*whisper.Whisper -} - -func (s *ShhExtSuite) SetupTest() { - s.nodes = make([]*node.Node, 2) - s.services = make([]*Service, 2) - s.whisper = make([]*whisper.Whisper, 2) - s.whisperWrapper = make([]types.Whisper, 2) - - directory, err := ioutil.TempDir("", "status-go-testing") - s.Require().NoError(err) - - for i := range s.nodes { - i := i // bind i to be usable in service constructors - cfg := &node.Config{ - Name: fmt.Sprintf("node-%d", i), - P2P: p2p.Config{ - NoDiscovery: true, - MaxPeers: 1, - ListenAddr: ":0", - }, - NoUSB: true, - } - stack, err := node.New(cfg) - s.NoError(err) - s.whisper[i] = whisper.New(nil) - s.whisperWrapper[i] = gethbridge.NewGethWhisperWrapper(s.whisper[i]) - - privateKey, err := crypto.GenerateKey() - s.NoError(err) - - s.NoError(stack.Register(func(n *node.ServiceContext) (node.Service, error) { - return gethbridge.GetGethWhisperFrom(s.whisperWrapper[i]), nil - })) - - config := params.ShhextConfig{ - InstallationID: "1", - BackupDisabledDataDir: directory, - PFSEnabled: true, - MailServerConfirmations: true, - ConnectionTarget: 10, - } - db, err := leveldb.Open(storage.NewMemStorage(), nil) - s.Require().NoError(err) - nodeWrapper := &testNodeWrapper{w: s.whisperWrapper[i]} - s.services[i] = New(nodeWrapper, nil, "shhext", nil, db, config) - - tmpdir, err := ioutil.TempDir("", "test-shhext-service") - s.Require().NoError(err) - - sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/%d", tmpdir, i), "password") - s.Require().NoError(err) - - s.Require().NoError(s.services[i].InitProtocol(privateKey, sqlDB)) - s.NoError(stack.Register(func(n *node.ServiceContext) (node.Service, error) { - return s.services[i], nil - })) - s.Require().NoError(stack.Start()) - s.nodes[i] = stack - } -} - -func (s *ShhExtSuite) TestInitProtocol() { - directory, err := ioutil.TempDir("", "status-go-testing") - s.Require().NoError(err) - - config := params.ShhextConfig{ - InstallationID: "2", - BackupDisabledDataDir: directory, - PFSEnabled: true, - MailServerConfirmations: true, - ConnectionTarget: 10, - } - db, err := leveldb.Open(storage.NewMemStorage(), nil) - s.Require().NoError(err) - - shh := gethbridge.NewGethWhisperWrapper(whisper.New(nil)) - privateKey, err := crypto.GenerateKey() - s.Require().NoError(err) - - nodeWrapper := &testNodeWrapper{w: shh} - service := New(nodeWrapper, nil, "shhext", nil, db, config) - - tmpdir, err := ioutil.TempDir("", "test-shhext-service-init-protocol") - s.Require().NoError(err) - - sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") - s.Require().NoError(err) - - err = service.InitProtocol(privateKey, sqlDB) - s.NoError(err) -} - -func (s *ShhExtSuite) TestRequestMessagesErrors() { - var err error - - shh := gethbridge.NewGethWhisperWrapper(whisper.New(nil)) - aNode, err := node.New(&node.Config{ - P2P: p2p.Config{ - MaxPeers: math.MaxInt32, - NoDiscovery: true, - }, - NoUSB: true, - }) // in-memory node as no data dir - s.NoError(err) - err = aNode.Register(func(*node.ServiceContext) (node.Service, error) { - return gethbridge.GetGethWhisperFrom(shh), nil - }) - s.NoError(err) - - err = aNode.Start() - s.NoError(err) - defer func() { s.NoError(aNode.Stop()) }() - - mock := newHandlerMock(1) - config := params.ShhextConfig{ - InstallationID: "1", - BackupDisabledDataDir: os.TempDir(), - PFSEnabled: true, - } - nodeWrapper := &testNodeWrapper{w: shh} - service := New(nodeWrapper, nil, "shhext", mock, nil, config) - api := NewPublicAPI(service) - - const ( - mailServerPeer = "enode://b7e65e1bedc2499ee6cbd806945af5e7df0e59e4070c96821570bd581473eade24a489f5ec95d060c0db118c879403ab88d827d3766978f28708989d35474f87@[::]:51920" - ) - - var hash []byte - - // invalid MailServer enode address - hash, err = api.RequestMessages(context.TODO(), MessagesRequest{MailServerPeer: "invalid-address"}) - s.Nil(hash) - s.EqualError(err, "invalid mailServerPeer value: invalid URL scheme, want \"enode\"") - - // non-existent symmetric key - hash, err = api.RequestMessages(context.TODO(), MessagesRequest{ - MailServerPeer: mailServerPeer, - SymKeyID: "invalid-sym-key-id", - }) - s.Nil(hash) - s.EqualError(err, "invalid symKeyID value: non-existent key ID") - - // with a symmetric key - symKeyID, symKeyErr := shh.AddSymKeyFromPassword("some-pass") - s.NoError(symKeyErr) - hash, err = api.RequestMessages(context.TODO(), MessagesRequest{ - MailServerPeer: mailServerPeer, - SymKeyID: symKeyID, - }) - s.Nil(hash) - s.Contains(err.Error(), "Could not find peer with ID") - - // from is greater than to - hash, err = api.RequestMessages(context.TODO(), MessagesRequest{ - From: 10, - To: 5, - }) - s.Nil(hash) - s.Contains(err.Error(), "Query range is invalid: from > to (10 > 5)") -} - -func (s *ShhExtSuite) TestMultipleRequestMessagesWithoutForce() { - waitErr := helpers.WaitForPeerAsync(s.nodes[0].Server(), s.nodes[1].Server().Self().URLv4(), p2p.PeerEventTypeAdd, time.Second) - s.nodes[0].Server().AddPeer(s.nodes[1].Server().Self()) - s.Require().NoError(<-waitErr) - client, err := s.nodes[0].Attach() - s.NoError(err) - s.NoError(client.Call(nil, "shhext_requestMessages", MessagesRequest{ - MailServerPeer: s.nodes[1].Server().Self().URLv4(), - Topics: []types.TopicType{{1}}, - })) - s.EqualError(client.Call(nil, "shhext_requestMessages", MessagesRequest{ - MailServerPeer: s.nodes[1].Server().Self().URLv4(), - Topics: []types.TopicType{{1}}, - }), "another request with the same topics was sent less than 3s ago. Please wait for a bit longer, or set `force` to true in request parameters") - s.NoError(client.Call(nil, "shhext_requestMessages", MessagesRequest{ - MailServerPeer: s.nodes[1].Server().Self().URLv4(), - Topics: []types.TopicType{{2}}, - })) -} - -func (s *ShhExtSuite) TestFailedRequestUnregistered() { - waitErr := helpers.WaitForPeerAsync(s.nodes[0].Server(), s.nodes[1].Server().Self().URLv4(), p2p.PeerEventTypeAdd, time.Second) - s.nodes[0].Server().AddPeer(s.nodes[1].Server().Self()) - s.Require().NoError(<-waitErr) - client, err := s.nodes[0].Attach() - topics := []types.TopicType{{1}} - s.NoError(err) - s.EqualError(client.Call(nil, "shhext_requestMessages", MessagesRequest{ - MailServerPeer: "enode://19872f94b1e776da3a13e25afa71b47dfa99e658afd6427ea8d6e03c22a99f13590205a8826443e95a37eee1d815fc433af7a8ca9a8d0df7943d1f55684045b7@0.0.0.0:30305", - Topics: topics, - }), "Could not find peer with ID: 10841e6db5c02fc331bf36a8d2a9137a1696d9d3b6b1f872f780e02aa8ec5bba") - s.NoError(client.Call(nil, "shhext_requestMessages", MessagesRequest{ - MailServerPeer: s.nodes[1].Server().Self().URLv4(), - Topics: topics, - })) -} - -func (s *ShhExtSuite) TestRequestMessagesSuccess() { - var err error - - shh := gethbridge.NewGethWhisperWrapper(whisper.New(nil)) - privateKey, err := crypto.GenerateKey() - s.Require().NoError(err) - aNode, err := node.New(&node.Config{ - P2P: p2p.Config{ - MaxPeers: math.MaxInt32, - NoDiscovery: true, - }, - NoUSB: true, - }) // in-memory node as no data dir - s.Require().NoError(err) - err = aNode.Register(func(*node.ServiceContext) (node.Service, error) { return gethbridge.GetGethWhisperFrom(shh), nil }) - s.Require().NoError(err) - - err = aNode.Start() - s.Require().NoError(err) - defer func() { err := aNode.Stop(); s.NoError(err) }() - - mock := newHandlerMock(1) - config := params.ShhextConfig{ - InstallationID: "1", - BackupDisabledDataDir: os.TempDir(), - PFSEnabled: true, - } - nodeWrapper := &testNodeWrapper{w: shh} - service := New(nodeWrapper, nil, "shhext", mock, nil, config) - - tmpdir, err := ioutil.TempDir("", "test-shhext-service-request-messages") - s.Require().NoError(err) - - sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") - s.Require().NoError(err) - - s.Require().NoError(service.InitProtocol(privateKey, sqlDB)) - s.Require().NoError(service.Start(aNode.Server())) - api := NewPublicAPI(service) - - // with a peer acting as a mailserver - // prepare a node first - mailNode, err := node.New(&node.Config{ - P2P: p2p.Config{ - MaxPeers: math.MaxInt32, - NoDiscovery: true, - ListenAddr: ":0", - }, - NoUSB: true, - }) // in-memory node as no data dir - s.Require().NoError(err) - err = mailNode.Register(func(*node.ServiceContext) (node.Service, error) { - return whisper.New(nil), nil - }) - s.NoError(err) - err = mailNode.Start() - s.Require().NoError(err) - defer func() { s.NoError(mailNode.Stop()) }() - - // add mailPeer as a peer - waitErr := helpers.WaitForPeerAsync(aNode.Server(), mailNode.Server().Self().URLv4(), p2p.PeerEventTypeAdd, time.Second) - aNode.Server().AddPeer(mailNode.Server().Self()) - s.Require().NoError(<-waitErr) - - var hash []byte - - // send a request with a symmetric key - symKeyID, symKeyErr := shh.AddSymKeyFromPassword("some-pass") - s.Require().NoError(symKeyErr) - hash, err = api.RequestMessages(context.TODO(), MessagesRequest{ - MailServerPeer: mailNode.Server().Self().URLv4(), - SymKeyID: symKeyID, - Force: true, - }) - s.Require().NoError(err) - s.Require().NotNil(hash) - // Send a request without a symmetric key. In this case, - // a public key extracted from MailServerPeer will be used. - hash, err = api.RequestMessages(context.TODO(), MessagesRequest{ - MailServerPeer: mailNode.Server().Self().URLv4(), - Force: true, - }) - s.Require().NoError(err) - s.Require().NotNil(hash) -} - -func (s *ShhExtSuite) TearDown() { - for _, n := range s.nodes { - s.NoError(n.Stop()) - } -} - -type testNodeWrapper struct { - w types.Whisper -} - -func (w *testNodeWrapper) NewENSVerifier(_ *zap.Logger) enstypes.ENSVerifier { - panic("not implemented") -} - -func (w *testNodeWrapper) GetWhisper(_ interface{}) (types.Whisper, error) { - return w.w, nil -} - -func (w *testNodeWrapper) GetWaku(_ interface{}) (types.Waku, error) { - return nil, errors.New("not implemented") -} - -func (w *testNodeWrapper) AddPeer(url string) error { - panic("not implemented") -} - -func (w *testNodeWrapper) RemovePeer(url string) error { - panic("not implemented") -} - -type WhisperNodeMockSuite struct { - suite.Suite - - localWhisperAPI *whisper.PublicWhisperAPI - localAPI *PublicAPI - localNode *enode.Node - remoteRW *p2p.MsgPipeRW - - localService *Service -} - -func (s *WhisperNodeMockSuite) SetupTest() { - db, err := leveldb.Open(storage.NewMemStorage(), nil) - s.Require().NoError(err) - conf := &whisper.Config{ - MinimumAcceptedPOW: 0, - MaxMessageSize: 100 << 10, - } - w := whisper.New(conf) - s.Require().NoError(w.Start(nil)) - pkey, err := crypto.GenerateKey() - s.Require().NoError(err) - node := enode.NewV4(&pkey.PublicKey, net.ParseIP("127.0.0.1"), 1, 1) - peer := p2p.NewPeer(node.ID(), "1", []p2p.Cap{{"shh", 6}}) - rw1, rw2 := p2p.MsgPipe() - errorc := make(chan error, 1) - go func() { - err := w.HandlePeer(peer, rw2) - errorc <- err - }() - whisperWrapper := gethbridge.NewGethWhisperWrapper(w) - s.Require().NoError(p2p.ExpectMsg(rw1, statusCode, []interface{}{ - whisper.ProtocolVersion, - math.Float64bits(whisperWrapper.MinPow()), - whisperWrapper.BloomFilter(), - false, - true, - whisper.RateLimits{}, - })) - s.Require().NoError(p2p.SendItems( - rw1, - statusCode, - whisper.ProtocolVersion, - whisper.ProtocolVersion, - math.Float64bits(whisperWrapper.MinPow()), - whisperWrapper.BloomFilter(), - true, - true, - whisper.RateLimits{}, - )) - - nodeWrapper := &testNodeWrapper{w: whisperWrapper} - s.localService = New( - nodeWrapper, - nil, - "shhext", - nil, - db, - params.ShhextConfig{MailServerConfirmations: true, MaxMessageDeliveryAttempts: 3}, - ) - s.Require().NoError(s.localService.UpdateMailservers([]*enode.Node{node})) - - s.localWhisperAPI = whisper.NewPublicWhisperAPI(w) - s.localAPI = NewPublicAPI(s.localService) - s.localNode = node - s.remoteRW = rw1 -} - -func TestRequestMessagesSync(t *testing.T) { - suite.Run(t, new(RequestMessagesSyncSuite)) -} - -type RequestMessagesSyncSuite struct { - WhisperNodeMockSuite -} - -func (s *RequestMessagesSyncSuite) TestExpired() { - // intentionally discarding all requests, so that request will timeout - go func() { - msg, err := s.remoteRW.ReadMsg() - s.Require().NoError(err) - s.Require().NoError(msg.Discard()) - }() - _, err := s.localAPI.RequestMessagesSync( - RetryConfig{ - BaseTimeout: time.Second, - }, - MessagesRequest{ - MailServerPeer: s.localNode.String(), - }, - ) - s.Require().EqualError(err, "failed to request messages after 1 retries") -} - -func (s *RequestMessagesSyncSuite) testCompletedFromAttempt(target int) { - const cursorSize = 36 // taken from mailserver_response.go from whisper package - cursor := [cursorSize]byte{} - cursor[0] = 0x01 - - go func() { - attempt := 0 - for { - attempt++ - msg, err := s.remoteRW.ReadMsg() - s.Require().NoError(err) - if attempt < target { - s.Require().NoError(msg.Discard()) - continue - } - var e whisper.Envelope - s.Require().NoError(msg.Decode(&e)) - s.Require().NoError(p2p.Send(s.remoteRW, p2pRequestCompleteCode, whisper.CreateMailServerRequestCompletedPayload(e.Hash(), common.Hash{}, cursor[:]))) - } - }() - resp, err := s.localAPI.RequestMessagesSync( - RetryConfig{ - BaseTimeout: time.Second, - MaxRetries: target, - }, - MessagesRequest{ - MailServerPeer: s.localNode.String(), - Force: true, // force true is convenient here because timeout is less then default delay (3s) - }, - ) - s.Require().NoError(err) - s.Require().Equal(MessagesResponse{Cursor: hex.EncodeToString(cursor[:])}, resp) -} - -func (s *RequestMessagesSyncSuite) TestCompletedFromFirstAttempt() { - s.testCompletedFromAttempt(1) -} - -func (s *RequestMessagesSyncSuite) TestCompletedFromSecondAttempt() { - s.testCompletedFromAttempt(2) -} - -func TestWhisperConfirmations(t *testing.T) { - suite.Run(t, new(WhisperConfirmationSuite)) -} - -type WhisperConfirmationSuite struct { - WhisperNodeMockSuite -} - -func TestWhisperRetriesSuite(t *testing.T) { - suite.Run(t, new(WhisperRetriesSuite)) -} - -type WhisperRetriesSuite struct { - WhisperNodeMockSuite -} - -func TestRequestWithTrackingHistorySuite(t *testing.T) { - suite.Run(t, new(RequestWithTrackingHistorySuite)) -} - -type RequestWithTrackingHistorySuite struct { - suite.Suite - - envelopeSymkey string - envelopeSymkeyID string - - localWhisperAPI types.PublicWhisperAPI - localAPI *PublicAPI - localService *Service - localContext Context - mailSymKey string - - remoteMailserver *mailserver.WhisperMailServer - remoteNode *enode.Node - remoteWhisper *whisper.Whisper -} - -func (s *RequestWithTrackingHistorySuite) SetupTest() { - db, err := leveldb.Open(storage.NewMemStorage(), nil) - s.Require().NoError(err) - conf := &whisper.Config{ - MinimumAcceptedPOW: 0, - MaxMessageSize: 100 << 10, - } - localSHH := whisper.New(conf) - local := gethbridge.NewGethWhisperWrapper(localSHH) - s.Require().NoError(localSHH.Start(nil)) - - s.localWhisperAPI = local.PublicWhisperAPI() - nodeWrapper := &testNodeWrapper{w: local} - s.localService = New(nodeWrapper, nil, "shhext", nil, db, params.ShhextConfig{}) - s.localContext = NewContextFromService(context.Background(), s.localService, s.localService.storage) - localPkey, err := crypto.GenerateKey() - s.Require().NoError(err) - - tmpdir, err := ioutil.TempDir("", "test-shhext-service") - s.Require().NoError(err) - - sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") - s.Require().NoError(err) - - s.Require().NoError(s.localService.InitProtocol(nil, sqlDB)) - s.Require().NoError(s.localService.Start(&p2p.Server{Config: p2p.Config{PrivateKey: localPkey}})) - s.localAPI = NewPublicAPI(s.localService) - - remoteSHH := whisper.New(conf) - s.remoteWhisper = remoteSHH - s.Require().NoError(remoteSHH.Start(nil)) - s.remoteMailserver = &mailserver.WhisperMailServer{} - remoteSHH.RegisterMailServer(s.remoteMailserver) - password := "test" - tmpdir, err = ioutil.TempDir("", "tracking-history-tests-") - s.Require().NoError(err) - s.Require().NoError(s.remoteMailserver.Init(remoteSHH, ¶ms.WhisperConfig{ - DataDir: tmpdir, - MailServerPassword: password, - })) - - pkey, err := crypto.GenerateKey() - s.Require().NoError(err) - // we need proper enode for a remote node. it will be used when mail server request is made - s.remoteNode = enode.NewV4(&pkey.PublicKey, net.ParseIP("127.0.0.1"), 1, 1) - remotePeer := p2p.NewPeer(s.remoteNode.ID(), "1", []p2p.Cap{{"shh", 6}}) - localPeer := p2p.NewPeer(enode.ID{2}, "2", []p2p.Cap{{"shh", 6}}) - // FIXME close this in tear down - rw1, rw2 := p2p.MsgPipe() - go func() { - err := localSHH.HandlePeer(remotePeer, rw1) - s.Require().NoError(err) - }() - go func() { - err := remoteSHH.HandlePeer(localPeer, rw2) - s.Require().NoError(err) - }() - s.mailSymKey, err = s.localWhisperAPI.GenerateSymKeyFromPassword(context.Background(), password) - s.Require().NoError(err) - - s.envelopeSymkey = "topics" - s.envelopeSymkeyID, err = s.localWhisperAPI.GenerateSymKeyFromPassword(context.Background(), s.envelopeSymkey) - s.Require().NoError(err) -} - -func (s *RequestWithTrackingHistorySuite) postEnvelopes(topics ...types.TopicType) []hexutil.Bytes { - var ( - rst = make([]hexutil.Bytes, len(topics)) - err error - ) - for i, t := range topics { - rst[i], err = s.localWhisperAPI.Post(context.Background(), types.NewMessage{ - SymKeyID: s.envelopeSymkeyID, - TTL: 10, - Topic: t, - }) - s.Require().NoError(err) - } - return rst - -} - -func (s *RequestWithTrackingHistorySuite) waitForArchival(hexes []hexutil.Bytes) { - events := make(chan whisper.EnvelopeEvent, 2) - sub := s.remoteWhisper.SubscribeEnvelopeEvents(events) - defer sub.Unsubscribe() - s.Require().NoError(waitForArchival(events, 2*time.Second, hexes...)) -} - -func (s *RequestWithTrackingHistorySuite) createEmptyFilter(topics ...types.TopicType) string { - filterid, err := s.localWhisperAPI.NewMessageFilter(types.Criteria{ - SymKeyID: s.envelopeSymkeyID, - Topics: topics, - AllowP2P: true, - }) - s.Require().NoError(err) - s.Require().NotNil(filterid) - - messages, err := s.localWhisperAPI.GetFilterMessages(filterid) - s.Require().NoError(err) - s.Require().Empty(messages) - return filterid -} - -func (s *RequestWithTrackingHistorySuite) initiateHistoryRequest(topics ...TopicRequest) []types.HexBytes { - requests, err := s.localAPI.InitiateHistoryRequests(context.Background(), InitiateHistoryRequestParams{ - Peer: s.remoteNode.String(), - SymKeyID: s.mailSymKey, - Timeout: 10 * time.Second, - Requests: topics, - }) - s.Require().NoError(err) - return requests -} - -func (s *RequestWithTrackingHistorySuite) waitMessagesDelivered(filterid string, hexes ...hexutil.Bytes) { - var received int - s.Require().NoError(utils.Eventually(func() error { - messages, err := s.localWhisperAPI.GetFilterMessages(filterid) - if err != nil { - return err - } - received += len(messages) - if received != len(hexes) { - return fmt.Errorf("expecting to receive %d messages, received %d", len(hexes), received) - } - return nil - }, 2*time.Second, 200*time.Millisecond)) -} - -func (s *RequestWithTrackingHistorySuite) waitNoRequests() { - store := s.localContext.HistoryStore() - s.Require().NoError(utils.Eventually(func() error { - reqs, err := store.GetAllRequests() - if err != nil { - return err - } - if len(reqs) != 0 { - return fmt.Errorf("not all requests were removed. count %d", len(reqs)) - } - return nil - }, 2*time.Second, 200*time.Millisecond)) -} - -func (s *RequestWithTrackingHistorySuite) TestMultipleMergeIntoOne() { - topic1 := types.TopicType{1, 1, 1, 1} - topic2 := types.TopicType{2, 2, 2, 2} - topic3 := types.TopicType{3, 3, 3, 3} - hexes := s.postEnvelopes(topic1, topic2, topic3) - s.waitForArchival(hexes) - - filterid := s.createEmptyFilter(topic1, topic2, topic3) - requests := s.initiateHistoryRequest( - TopicRequest{Topic: topic1, Duration: time.Hour}, - TopicRequest{Topic: topic2, Duration: time.Hour}, - TopicRequest{Topic: topic3, Duration: 10 * time.Hour}, - ) - // since we are using different duration for 3rd topic there will be 2 requests - s.Require().Len(requests, 2) - s.Require().NotEqual(requests[0], requests[1]) - s.waitMessagesDelivered(filterid, hexes...) - - s.Require().NoError(s.localService.historyUpdates.UpdateTopicHistory(s.localContext, topic1, time.Now())) - s.Require().NoError(s.localService.historyUpdates.UpdateTopicHistory(s.localContext, topic2, time.Now())) - s.Require().NoError(s.localService.historyUpdates.UpdateTopicHistory(s.localContext, topic3, time.Now())) - for _, r := range requests { - s.Require().NoError(s.localAPI.CompleteRequest(context.TODO(), r.String())) - } - s.waitNoRequests() - - requests = s.initiateHistoryRequest( - TopicRequest{Topic: topic1, Duration: time.Hour}, - TopicRequest{Topic: topic2, Duration: time.Hour}, - TopicRequest{Topic: topic3, Duration: 10 * time.Hour}, - ) - s.Len(requests, 1) -} - -func (s *RequestWithTrackingHistorySuite) TestSingleRequest() { - topic1 := types.TopicType{1, 1, 1, 1} - topic2 := types.TopicType{255, 255, 255, 255} - hexes := s.postEnvelopes(topic1, topic2) - s.waitForArchival(hexes) - - filterid := s.createEmptyFilter(topic1, topic2) - requests := s.initiateHistoryRequest( - TopicRequest{Topic: topic1, Duration: time.Hour}, - TopicRequest{Topic: topic2, Duration: time.Hour}, - ) - s.Require().Len(requests, 1) - s.waitMessagesDelivered(filterid, hexes...) -} - -func (s *RequestWithTrackingHistorySuite) TestPreviousRequestReplaced() { - topic1 := types.TopicType{1, 1, 1, 1} - topic2 := types.TopicType{255, 255, 255, 255} - - requests := s.initiateHistoryRequest( - TopicRequest{Topic: topic1, Duration: time.Hour}, - TopicRequest{Topic: topic2, Duration: time.Hour}, - ) - s.Require().Len(requests, 1) - s.localService.requestsRegistry.Clear() - replaced := s.initiateHistoryRequest( - TopicRequest{Topic: topic1, Duration: time.Hour}, - TopicRequest{Topic: topic2, Duration: time.Hour}, - ) - s.Require().Len(replaced, 1) - s.Require().NotEqual(requests[0], replaced[0]) -} - -func waitForArchival(events chan whisper.EnvelopeEvent, duration time.Duration, hashes ...hexutil.Bytes) error { - waiting := map[common.Hash]struct{}{} - for _, hash := range hashes { - waiting[common.BytesToHash(hash)] = struct{}{} - } - timeout := time.After(duration) - for { - select { - case <-timeout: - return errors.New("timed out while waiting for mailserver to archive envelopes") - case ev := <-events: - if ev.Event != whisper.EventMailServerEnvelopeArchived { - continue - } - if _, exist := waiting[ev.Hash]; exist { - delete(waiting, ev.Hash) - if len(waiting) == 0 { - return nil - } - } - } - } - -} diff --git a/services/shhext_wakuext_test.go b/services/shhext_wakuext_test.go new file mode 100644 index 0000000000..9c54d293d2 --- /dev/null +++ b/services/shhext_wakuext_test.go @@ -0,0 +1,69 @@ +package services + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/ext" + "github.com/status-im/status-go/services/shhext" + "github.com/status-im/status-go/services/wakuext" + "github.com/status-im/status-go/waku" + "github.com/status-im/status-go/whisper/v6" +) + +func TestShhextAndWakuextInSingleNode(t *testing.T) { + aNode, err := node.New(&node.Config{ + P2P: p2p.Config{ + MaxPeers: math.MaxInt32, + NoDiscovery: true, + }, + NoUSB: true, + }) // in-memory node as no data dir + require.NoError(t, err) + + // register waku and whisper services + wakuWrapper := gethbridge.NewGethWakuWrapper(waku.New(nil, nil)) + err = aNode.Register(func(*node.ServiceContext) (node.Service, error) { + return gethbridge.GetGethWakuFrom(wakuWrapper), nil + }) + require.NoError(t, err) + whisperWrapper := gethbridge.NewGethWhisperWrapper(whisper.New(nil)) + err = aNode.Register(func(*node.ServiceContext) (node.Service, error) { + return gethbridge.GetGethWhisperFrom(whisperWrapper), nil + }) + require.NoError(t, err) + + nodeWrapper := ext.NewTestNodeWrapper(whisperWrapper, wakuWrapper) + + // register ext services + err = aNode.Register(func(ctx *node.ServiceContext) (node.Service, error) { + return wakuext.New(params.ShhextConfig{}, nodeWrapper, ctx, ext.EnvelopeSignalHandler{}, nil), nil + }) + require.NoError(t, err) + err = aNode.Register(func(ctx *node.ServiceContext) (node.Service, error) { + return shhext.New(params.ShhextConfig{}, nodeWrapper, ctx, ext.EnvelopeSignalHandler{}, nil), nil + }) + require.NoError(t, err) + + // start node + err = aNode.Start() + require.NoError(t, err) + defer func() { require.NoError(t, aNode.Stop()) }() + + // verify the services are available + rpc, err := aNode.Attach() + require.NoError(t, err) + var result string + err = rpc.Call(&result, "shhext_echo", "shhext test") + require.NoError(t, err) + require.Equal(t, "shhext test", result) + err = rpc.Call(&result, "wakuext_echo", "wakuext test") + require.NoError(t, err) + require.Equal(t, "wakuext test", result) +} diff --git a/services/wakuext/api.go b/services/wakuext/api.go new file mode 100644 index 0000000000..a47d866b54 --- /dev/null +++ b/services/wakuext/api.go @@ -0,0 +1,173 @@ +package wakuext + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/log" + gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/services/ext" + "github.com/status-im/status-go/waku" +) + +const ( + // defaultWorkTime is a work time reported in messages sent to MailServer nodes. + defaultWorkTime = 5 +) + +// PublicAPI extends waku public API. +type PublicAPI struct { + *ext.PublicAPI + service *Service + publicAPI types.PublicWakuAPI + log log.Logger +} + +// NewPublicAPI returns instance of the public API. +func NewPublicAPI(s *Service) *PublicAPI { + return &PublicAPI{ + PublicAPI: ext.NewPublicAPI(s.Service, s.w), + service: s, + publicAPI: s.w.PublicWakuAPI(), + log: log.New("package", "status-go/services/wakuext.PublicAPI"), + } +} + +// makeEnvelop makes an envelop for a historic messages request. +// Symmetric key is used to authenticate to MailServer. +// PK is the current node ID. +// DEPRECATED +func makeEnvelop( + payload []byte, + symKey []byte, + publicKey *ecdsa.PublicKey, + nodeID *ecdsa.PrivateKey, + pow float64, + now time.Time, +) (types.Envelope, error) { + params := waku.MessageParams{ + PoW: pow, + Payload: payload, + WorkTime: defaultWorkTime, + Src: nodeID, + } + // Either symKey or public key is required. + // This condition is verified in `message.Wrap()` method. + if len(symKey) > 0 { + params.KeySym = symKey + } else if publicKey != nil { + params.Dst = publicKey + } + message, err := waku.NewSentMessage(¶ms) + if err != nil { + return nil, err + } + envelope, err := message.Wrap(¶ms, now) + if err != nil { + return nil, err + } + return gethbridge.NewWakuEnvelope(envelope), nil +} + +// RequestMessages sends a request for historic messages to a MailServer. +func (api *PublicAPI) RequestMessages(_ context.Context, r ext.MessagesRequest) (types.HexBytes, error) { + api.log.Info("RequestMessages", "request", r) + + now := api.service.w.GetCurrentTime() + r.SetDefaults(now) + + if r.From > r.To { + return nil, fmt.Errorf("Query range is invalid: from > to (%d > %d)", r.From, r.To) + } + + mailServerNode, err := api.service.GetPeer(r.MailServerPeer) + if err != nil { + return nil, fmt.Errorf("%v: %v", ext.ErrInvalidMailServerPeer, err) + } + + var ( + symKey []byte + publicKey *ecdsa.PublicKey + ) + + if r.SymKeyID != "" { + symKey, err = api.service.w.GetSymKey(r.SymKeyID) + if err != nil { + return nil, fmt.Errorf("%v: %v", ext.ErrInvalidSymKeyID, err) + } + } else { + publicKey = mailServerNode.Pubkey() + } + + payload, err := ext.MakeMessagesRequestPayload(r) + if err != nil { + return nil, err + } + + envelope, err := makeEnvelop( + payload, + symKey, + publicKey, + api.service.NodeID(), + api.service.w.MinPow(), + now, + ) + if err != nil { + return nil, err + } + hash := envelope.Hash() + + if !r.Force { + err = api.service.RequestsRegistry().Register(hash, r.Topics) + if err != nil { + return nil, err + } + } + + if err := api.service.w.RequestHistoricMessagesWithTimeout(mailServerNode.ID().Bytes(), envelope, r.Timeout*time.Second); err != nil { + if !r.Force { + api.service.RequestsRegistry().Unregister(hash) + } + return nil, err + } + + return hash[:], nil +} + +// RequestMessagesSync repeats MessagesRequest using configuration in retry conf. +func (api *PublicAPI) RequestMessagesSync(conf ext.RetryConfig, r ext.MessagesRequest) (ext.MessagesResponse, error) { + var resp ext.MessagesResponse + + events := make(chan types.EnvelopeEvent, 10) + var ( + requestID types.HexBytes + err error + retries int + ) + for retries <= conf.MaxRetries { + sub := api.service.w.SubscribeEnvelopeEvents(events) + r.Timeout = conf.BaseTimeout + conf.StepTimeout*time.Duration(retries) + timeout := r.Timeout + // FIXME this weird conversion is required because MessagesRequest expects seconds but defines time.Duration + r.Timeout = time.Duration(int(r.Timeout.Seconds())) + requestID, err = api.RequestMessages(context.Background(), r) + if err != nil { + sub.Unsubscribe() + return resp, err + } + mailServerResp, err := ext.WaitForExpiredOrCompleted(types.BytesToHash(requestID), events, timeout) + sub.Unsubscribe() + if err == nil { + resp.Cursor = hex.EncodeToString(mailServerResp.Cursor) + resp.Error = mailServerResp.Error + return resp, nil + } + retries++ + api.log.Error("[RequestMessagesSync] failed", "err", err, "retries", retries) + } + return resp, fmt.Errorf("failed to request messages after %d retries", retries) +} diff --git a/services/wakuext/api_test.go b/services/wakuext/api_test.go new file mode 100644 index 0000000000..939529661c --- /dev/null +++ b/services/wakuext/api_test.go @@ -0,0 +1,411 @@ +package wakuext + +import ( + "context" + "encoding/hex" + "fmt" + "io/ioutil" + "math" + "net" + "os" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/storage" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" + gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/ext" + "github.com/status-im/status-go/sqlite" + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/waku" +) + +func TestRequestMessagesErrors(t *testing.T) { + var err error + + waku := gethbridge.NewGethWakuWrapper(waku.New(nil, nil)) + aNode, err := node.New(&node.Config{ + P2P: p2p.Config{ + MaxPeers: math.MaxInt32, + NoDiscovery: true, + }, + NoUSB: true, + }) // in-memory node as no data dir + require.NoError(t, err) + err = aNode.Register(func(*node.ServiceContext) (node.Service, error) { + return gethbridge.GetGethWakuFrom(waku), nil + }) + require.NoError(t, err) + + err = aNode.Start() + require.NoError(t, err) + defer func() { require.NoError(t, aNode.Stop()) }() + + handler := ext.NewHandlerMock(1) + config := params.ShhextConfig{ + InstallationID: "1", + BackupDisabledDataDir: os.TempDir(), + PFSEnabled: true, + } + nodeWrapper := ext.NewTestNodeWrapper(nil, waku) + service := New(config, nodeWrapper, nil, handler, nil) + api := NewPublicAPI(service) + + const mailServerPeer = "enode://b7e65e1bedc2499ee6cbd806945af5e7df0e59e4070c96821570bd581473eade24a489f5ec95d060c0db118c879403ab88d827d3766978f28708989d35474f87@[::]:51920" + + var hash []byte + + // invalid MailServer enode address + hash, err = api.RequestMessages(context.TODO(), ext.MessagesRequest{MailServerPeer: "invalid-address"}) + require.Nil(t, hash) + require.EqualError(t, err, "invalid mailServerPeer value: invalid URL scheme, want \"enode\"") + + // non-existent symmetric key + hash, err = api.RequestMessages(context.TODO(), ext.MessagesRequest{ + MailServerPeer: mailServerPeer, + SymKeyID: "invalid-sym-key-id", + }) + require.Nil(t, hash) + require.EqualError(t, err, "invalid symKeyID value: non-existent key ID") + + // with a symmetric key + symKeyID, symKeyErr := waku.AddSymKeyFromPassword("some-pass") + require.NoError(t, symKeyErr) + hash, err = api.RequestMessages(context.TODO(), ext.MessagesRequest{ + MailServerPeer: mailServerPeer, + SymKeyID: symKeyID, + }) + require.Nil(t, hash) + require.Contains(t, err.Error(), "could not find peer with ID") + + // from is greater than to + hash, err = api.RequestMessages(context.TODO(), ext.MessagesRequest{ + From: 10, + To: 5, + }) + require.Nil(t, hash) + require.Contains(t, err.Error(), "Query range is invalid: from > to (10 > 5)") +} + +func TestInitProtocol(t *testing.T) { + directory, err := ioutil.TempDir("", "status-go-testing") + require.NoError(t, err) + + config := params.ShhextConfig{ + InstallationID: "2", + BackupDisabledDataDir: directory, + PFSEnabled: true, + MailServerConfirmations: true, + ConnectionTarget: 10, + } + db, err := leveldb.Open(storage.NewMemStorage(), nil) + require.NoError(t, err) + + waku := gethbridge.NewGethWakuWrapper(waku.New(nil, nil)) + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + + nodeWrapper := ext.NewTestNodeWrapper(nil, waku) + service := New(config, nodeWrapper, nil, nil, db) + + tmpdir, err := ioutil.TempDir("", "test-shhext-service-init-protocol") + require.NoError(t, err) + + sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") + require.NoError(t, err) + + err = service.InitProtocol(privateKey, sqlDB) + require.NoError(t, err) +} + +func TestShhExtSuite(t *testing.T) { + suite.Run(t, new(ShhExtSuite)) +} + +type ShhExtSuite struct { + suite.Suite + + dir string + nodes []*node.Node + wakus []types.Waku + services []*Service +} + +func (s *ShhExtSuite) createAndAddNode() { + idx := len(s.nodes) + + // create a node + cfg := &node.Config{ + Name: strconv.Itoa(idx), + P2P: p2p.Config{ + MaxPeers: math.MaxInt32, + NoDiscovery: true, + ListenAddr: ":0", + }, + NoUSB: true, + } + stack, err := node.New(cfg) + s.NoError(err) + w := waku.New(nil, nil) + err = stack.Register(func(n *node.ServiceContext) (node.Service, error) { + return w, nil + }) + s.NoError(err) + + // set up protocol + config := params.ShhextConfig{ + InstallationID: "1", + BackupDisabledDataDir: s.dir, + PFSEnabled: true, + MailServerConfirmations: true, + ConnectionTarget: 10, + } + db, err := leveldb.Open(storage.NewMemStorage(), nil) + s.Require().NoError(err) + nodeWrapper := ext.NewTestNodeWrapper(nil, gethbridge.NewGethWakuWrapper(w)) + service := New(config, nodeWrapper, nil, nil, db) + sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/%d", s.dir, idx), "password") + s.Require().NoError(err) + privateKey, err := crypto.GenerateKey() + s.NoError(err) + err = service.InitProtocol(privateKey, sqlDB) + s.NoError(err) + err = stack.Register(func(n *node.ServiceContext) (node.Service, error) { + return service, nil + }) + s.NoError(err) + + // start the node + err = stack.Start() + s.Require().NoError(err) + + // store references + s.nodes = append(s.nodes, stack) + s.wakus = append(s.wakus, gethbridge.NewGethWakuWrapper(w)) + s.services = append(s.services, service) +} + +func (s *ShhExtSuite) SetupTest() { + var err error + s.dir, err = ioutil.TempDir("", "status-go-testing") + s.Require().NoError(err) +} + +func (s *ShhExtSuite) TearDownTest() { + for _, n := range s.nodes { + s.NoError(n.Stop()) + } + s.nodes = nil + s.wakus = nil + s.services = nil +} + +func (s *ShhExtSuite) TestRequestMessagesSuccess() { + // two nodes needed: client and mailserver + s.createAndAddNode() + s.createAndAddNode() + + waitErr := helpers.WaitForPeerAsync(s.nodes[0].Server(), s.nodes[1].Server().Self().URLv4(), p2p.PeerEventTypeAdd, time.Second) + s.nodes[0].Server().AddPeer(s.nodes[1].Server().Self()) + s.Require().NoError(<-waitErr) + + api := NewPublicAPI(s.services[0]) + + _, err := api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: s.nodes[1].Server().Self().URLv4(), + Topics: []types.TopicType{{1}}, + }) + s.NoError(err) +} + +func (s *ShhExtSuite) TestMultipleRequestMessagesWithoutForce() { + // two nodes needed: client and mailserver + s.createAndAddNode() + s.createAndAddNode() + + waitErr := helpers.WaitForPeerAsync(s.nodes[0].Server(), s.nodes[1].Server().Self().URLv4(), p2p.PeerEventTypeAdd, time.Second) + s.nodes[0].Server().AddPeer(s.nodes[1].Server().Self()) + s.Require().NoError(<-waitErr) + + api := NewPublicAPI(s.services[0]) + + _, err := api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: s.nodes[1].Server().Self().URLv4(), + Topics: []types.TopicType{{1}}, + }) + s.NoError(err) + _, err = api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: s.nodes[1].Server().Self().URLv4(), + Topics: []types.TopicType{{1}}, + }) + s.EqualError(err, "another request with the same topics was sent less than 3s ago. Please wait for a bit longer, or set `force` to true in request parameters") + _, err = api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: s.nodes[1].Server().Self().URLv4(), + Topics: []types.TopicType{{2}}, + }) + s.NoError(err) +} + +func (s *ShhExtSuite) TestFailedRequestWithUnknownMailServerPeer() { + s.createAndAddNode() + + api := NewPublicAPI(s.services[0]) + + _, err := api.RequestMessages(context.Background(), ext.MessagesRequest{ + MailServerPeer: "enode://19872f94b1e776da3a13e25afa71b47dfa99e658afd6427ea8d6e03c22a99f13590205a8826443e95a37eee1d815fc433af7a8ca9a8d0df7943d1f55684045b7@0.0.0.0:30305", + Topics: []types.TopicType{{1}}, + }) + s.EqualError(err, "could not find peer with ID: 10841e6db5c02fc331bf36a8d2a9137a1696d9d3b6b1f872f780e02aa8ec5bba") +} + +const ( + // internal waku protocol codes + statusCode = 0 + p2pRequestCompleteCode = 125 +) + +type WakuNodeMockSuite struct { + suite.Suite + + localWakuAPI *waku.PublicWakuAPI + localAPI *PublicAPI + localNode *enode.Node + remoteRW *p2p.MsgPipeRW + + localService *Service +} + +func (s *WakuNodeMockSuite) SetupTest() { + db, err := leveldb.Open(storage.NewMemStorage(), nil) + s.Require().NoError(err) + conf := &waku.Config{ + MinimumAcceptedPoW: 0, + MaxMessageSize: 100 << 10, + EnableConfirmations: true, + } + w := waku.New(conf, nil) + s.Require().NoError(w.Start(nil)) + pkey, err := crypto.GenerateKey() + s.Require().NoError(err) + node := enode.NewV4(&pkey.PublicKey, net.ParseIP("127.0.0.1"), 1, 1) + peer := p2p.NewPeer(node.ID(), "1", []p2p.Cap{{"shh", 6}}) + rw1, rw2 := p2p.MsgPipe() + errorc := make(chan error, 1) + go func() { + err := w.HandlePeer(peer, rw2) + errorc <- err + }() + wakuWrapper := gethbridge.NewGethWakuWrapper(w) + s.Require().NoError(p2p.ExpectMsg(rw1, statusCode, []interface{}{ + waku.ProtocolVersion, + math.Float64bits(wakuWrapper.MinPow()), + wakuWrapper.BloomFilter(), + false, + true, + waku.RateLimits{}, + })) + s.Require().NoError(p2p.SendItems( + rw1, + statusCode, + waku.ProtocolVersion, + math.Float64bits(wakuWrapper.MinPow()), + wakuWrapper.BloomFilter(), + true, + true, + waku.RateLimits{}, + )) + + nodeWrapper := ext.NewTestNodeWrapper(nil, wakuWrapper) + s.localService = New( + params.ShhextConfig{MailServerConfirmations: true, MaxMessageDeliveryAttempts: 3}, + nodeWrapper, + nil, + nil, + db, + ) + s.Require().NoError(s.localService.UpdateMailservers([]*enode.Node{node})) + + s.localWakuAPI = waku.NewPublicWakuAPI(w) + s.localAPI = NewPublicAPI(s.localService) + s.localNode = node + s.remoteRW = rw1 +} + +func TestRequestMessagesSync(t *testing.T) { + suite.Run(t, new(RequestMessagesSyncSuite)) +} + +type RequestMessagesSyncSuite struct { + WakuNodeMockSuite +} + +func (s *RequestMessagesSyncSuite) TestExpired() { + // intentionally discarding all requests, so that request will timeout + go func() { + msg, err := s.remoteRW.ReadMsg() + s.Require().NoError(err) + s.Require().NoError(msg.Discard()) + }() + _, err := s.localAPI.RequestMessagesSync( + ext.RetryConfig{ + BaseTimeout: time.Second, + }, + ext.MessagesRequest{ + MailServerPeer: s.localNode.String(), + }, + ) + s.Require().EqualError(err, "failed to request messages after 1 retries") +} + +func (s *RequestMessagesSyncSuite) testCompletedFromAttempt(target int) { + const cursorSize = 36 // taken from mailserver_response.go from waku package + cursor := [cursorSize]byte{} + cursor[0] = 0x01 + + go func() { + attempt := 0 + for { + attempt++ + msg, err := s.remoteRW.ReadMsg() + s.Require().NoError(err) + if attempt < target { + s.Require().NoError(msg.Discard()) + continue + } + var e waku.Envelope + s.Require().NoError(msg.Decode(&e)) + s.Require().NoError(p2p.Send(s.remoteRW, p2pRequestCompleteCode, waku.CreateMailServerRequestCompletedPayload(e.Hash(), common.Hash{}, cursor[:]))) + } + }() + resp, err := s.localAPI.RequestMessagesSync( + ext.RetryConfig{ + BaseTimeout: time.Second, + MaxRetries: target, + }, + ext.MessagesRequest{ + MailServerPeer: s.localNode.String(), + Force: true, // force true is convenient here because timeout is less then default delay (3s) + }, + ) + s.Require().NoError(err) + s.Require().Equal(ext.MessagesResponse{Cursor: hex.EncodeToString(cursor[:])}, resp) +} + +func (s *RequestMessagesSyncSuite) TestCompletedFromFirstAttempt() { + s.testCompletedFromAttempt(1) +} + +func (s *RequestMessagesSyncSuite) TestCompletedFromSecondAttempt() { + s.testCompletedFromAttempt(2) +} diff --git a/services/wakuext/service.go b/services/wakuext/service.go new file mode 100644 index 0000000000..7aae6e329d --- /dev/null +++ b/services/wakuext/service.go @@ -0,0 +1,50 @@ +package wakuext + +import ( + "github.com/syndtr/goleveldb/leveldb" + + "github.com/ethereum/go-ethereum/rpc" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/ext" +) + +type Service struct { + *ext.Service + w types.Waku +} + +func New(config params.ShhextConfig, n types.Node, ctx interface{}, handler ext.EnvelopeEventsHandler, ldb *leveldb.DB) *Service { + w, err := n.GetWaku(ctx) + if err != nil { + panic(err) + } + delay := ext.DefaultRequestsDelay + if config.RequestsDelay != 0 { + delay = config.RequestsDelay + } + requestsRegistry := ext.NewRequestsRegistry(delay) + mailMonitor := ext.NewMailRequestMonitor(w, handler, requestsRegistry) + return &Service{ + Service: ext.New(config, n, ldb, mailMonitor, requestsRegistry, w), + w: w, + } +} + +func (s *Service) PublicWakuAPI() types.PublicWakuAPI { + return s.w.PublicWakuAPI() +} + +// APIs returns a list of new APIs. +func (s *Service) APIs() []rpc.API { + apis := []rpc.API{ + { + Namespace: "wakuext", + Version: "1.0", + Service: NewPublicAPI(s), + Public: true, + }, + } + return apis +} diff --git a/signal/events_shhext.go b/signal/events_shhext.go index 61c611c545..e0da0ad6dd 100644 --- a/signal/events_shhext.go +++ b/signal/events_shhext.go @@ -18,9 +18,6 @@ const ( // to any peer EventEnvelopeExpired = "envelope.expired" - // EventEnvelopeDiscarded is triggerd when envelope was discarded by a peer for some reason. - EventEnvelopeDiscarded = "envelope.discarded" - // EventMailServerRequestCompleted is triggered when whisper receives a message ack from the mailserver EventMailServerRequestCompleted = "mailserver.request.completed" diff --git a/t/benchmarks/mailserver_test.go b/t/benchmarks/mailserver_test.go index da8fe0040e..46960fc7e2 100644 --- a/t/benchmarks/mailserver_test.go +++ b/t/benchmarks/mailserver_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "github.com/status-im/status-go/services/shhext" + "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/node" @@ -14,8 +16,8 @@ import ( gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/params" + "github.com/status-im/status-go/services/ext" "github.com/status-im/status-go/services/nodebridge" - "github.com/status-im/status-go/services/shhext" "github.com/status-im/status-go/whisper/v6" ) @@ -73,7 +75,7 @@ func testMailserverPeer(t *testing.T) { require.NoError(t, err) // register mail service as well err = n.Register(func(ctx *node.ServiceContext) (node.Service, error) { - mailService := shhext.New(gethbridge.NewNodeBridge(n), ctx, nil, nil, config) + mailService := shhext.New(config, gethbridge.NewNodeBridge(n), ctx, nil, nil) return mailService, nil }) require.NoError(t, err) @@ -109,7 +111,7 @@ func testMailserverPeer(t *testing.T) { ok, err := shhAPI.MarkTrustedPeer(context.TODO(), *peerURL) require.NoError(t, err) require.True(t, ok) - requestID, err := shhextAPI.RequestMessages(context.TODO(), shhext.MessagesRequest{ + requestID, err := shhextAPI.RequestMessages(context.TODO(), ext.MessagesRequest{ MailServerPeer: *peerURL, SymKeyID: symKeyID, Topic: types.TopicType(topic), diff --git a/t/e2e/whisper/whisper_mailbox_test.go b/t/e2e/whisper/whisper_mailbox_test.go index e53f6b68fa..dfed915e42 100644 --- a/t/e2e/whisper/whisper_mailbox_test.go +++ b/t/e2e/whisper/whisper_mailbox_test.go @@ -13,6 +13,8 @@ import ( "testing" "time" + "github.com/status-im/status-go/services/shhext" + "github.com/stretchr/testify/suite" "golang.org/x/crypto/sha3" @@ -25,7 +27,6 @@ import ( "github.com/status-im/status-go/mailserver" "github.com/status-im/status-go/params" "github.com/status-im/status-go/rpc" - "github.com/status-im/status-go/services/shhext" "github.com/status-im/status-go/t/helpers" "github.com/status-im/status-go/t/utils" "github.com/status-im/status-go/whisper/v6" diff --git a/vendor/github.com/status-im/status-go/protocol/messenger.go b/vendor/github.com/status-im/status-go/protocol/messenger.go index 4baec271eb..f0afa68e43 100644 --- a/vendor/github.com/status-im/status-go/protocol/messenger.go +++ b/vendor/github.com/status-im/status-go/protocol/messenger.go @@ -283,8 +283,7 @@ func NewMessenger( // Initialize transport layer. var transp transport.Transport - - if shh, err := node.GetWhisper(nil); err == nil { + if shh, err := node.GetWhisper(nil); err == nil && shh != nil { transp, err = shhtransp.NewWhisperServiceTransport( shh, identity, @@ -296,10 +295,10 @@ func NewMessenger( if err != nil { return nil, errors.Wrap(err, "failed to create WhisperServiceTransport") } - } else if err != nil { + } else { logger.Info("failed to find Whisper service; trying Waku", zap.Error(err)) waku, err := node.GetWaku(nil) - if err != nil { + if err != nil || waku == nil { return nil, errors.Wrap(err, "failed to find Whisper and Waku services") } transp, err = wakutransp.NewWakuServiceTransport( diff --git a/vendor/github.com/status-im/status-go/whisper/v6/whisper.go b/vendor/github.com/status-im/status-go/whisper/v6/whisper.go index 693fede2d3..8bd55ff817 100644 --- a/vendor/github.com/status-im/status-go/whisper/v6/whisper.go +++ b/vendor/github.com/status-im/status-go/whisper/v6/whisper.go @@ -1202,18 +1202,14 @@ func (whisper *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error { log.Warn("failed to decode response message, peer will be disconnected", "peer", p.peer.ID(), "err", err) return errors.New("invalid request response message") } - event, err := CreateMailServerEvent(p.peer.ID(), payload) - if err != nil { log.Warn("error while parsing request complete code, peer will be disconnected", "peer", p.peer.ID(), "err", err) return err } - if event != nil { whisper.postP2P(*event) } - } default: // New message types might be implemented in the future versions of Whisper. diff --git a/whisper/whisper.go b/whisper/whisper.go index 693fede2d3..8bd55ff817 100644 --- a/whisper/whisper.go +++ b/whisper/whisper.go @@ -1202,18 +1202,14 @@ func (whisper *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error { log.Warn("failed to decode response message, peer will be disconnected", "peer", p.peer.ID(), "err", err) return errors.New("invalid request response message") } - event, err := CreateMailServerEvent(p.peer.ID(), payload) - if err != nil { log.Warn("error while parsing request complete code, peer will be disconnected", "peer", p.peer.ID(), "err", err) return err } - if event != nil { whisper.postP2P(*event) } - } default: // New message types might be implemented in the future versions of Whisper. From 3b81bd287864fdb63c89bbb9f47f4e541120e496 Mon Sep 17 00:00:00 2001 From: Adam Babik Date: Mon, 20 Jan 2020 21:59:32 +0100 Subject: [PATCH 9/9] bump to 0.39.9 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 93605679e9..43df43b3d5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.39.8 +0.39.9