557 lines
16 KiB
Go
557 lines
16 KiB
Go
package shhext
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math"
|
|
"net"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"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"
|
|
"github.com/status-im/status-go/params"
|
|
"github.com/status-im/status-go/t/helpers"
|
|
whisper "github.com/status-im/whisper/whisperv6"
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/syndtr/goleveldb/leveldb"
|
|
"github.com/syndtr/goleveldb/leveldb/storage"
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
const (
|
|
// internal whisper protocol codes
|
|
statusCode = 0
|
|
p2pRequestCompleteCode = 125
|
|
)
|
|
|
|
func newHandlerMock(buf int) handlerMock {
|
|
return handlerMock{
|
|
confirmations: make(chan common.Hash, buf),
|
|
expirations: make(chan common.Hash, buf),
|
|
requestsCompleted: make(chan common.Hash, buf),
|
|
requestsExpired: make(chan common.Hash, buf),
|
|
requestsFailed: make(chan common.Hash, buf),
|
|
}
|
|
}
|
|
|
|
type handlerMock struct {
|
|
confirmations chan common.Hash
|
|
expirations chan common.Hash
|
|
requestsCompleted chan common.Hash
|
|
requestsExpired chan common.Hash
|
|
requestsFailed chan common.Hash
|
|
}
|
|
|
|
func (t handlerMock) EnvelopeSent(hash common.Hash) {
|
|
t.confirmations <- hash
|
|
}
|
|
|
|
func (t handlerMock) EnvelopeExpired(hash common.Hash) {
|
|
t.expirations <- hash
|
|
}
|
|
|
|
func (t handlerMock) MailServerRequestCompleted(requestID common.Hash, lastEnvelopeHash common.Hash, cursor []byte, err error) {
|
|
if err == nil {
|
|
t.requestsCompleted <- requestID
|
|
} else {
|
|
t.requestsFailed <- requestID
|
|
}
|
|
}
|
|
|
|
func (t handlerMock) MailServerRequestExpired(hash common.Hash) {
|
|
t.requestsExpired <- hash
|
|
}
|
|
|
|
func TestShhExtSuite(t *testing.T) {
|
|
suite.Run(t, new(ShhExtSuite))
|
|
}
|
|
|
|
type ShhExtSuite struct {
|
|
suite.Suite
|
|
|
|
nodes []*node.Node
|
|
services []*Service
|
|
whisper []*whisper.Whisper
|
|
}
|
|
|
|
func (s *ShhExtSuite) SetupTest() {
|
|
s.nodes = make([]*node.Node, 2)
|
|
s.services = make([]*Service, 2)
|
|
s.whisper = make([]*whisper.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.NoError(stack.Register(func(n *node.ServiceContext) (node.Service, error) {
|
|
return s.whisper[i], nil
|
|
}))
|
|
|
|
config := params.ShhextConfig{
|
|
InstallationID: "1",
|
|
BackupDisabledDataDir: directory,
|
|
DebugAPIEnabled: true,
|
|
PFSEnabled: true,
|
|
MailServerConfirmations: true,
|
|
ConnectionTarget: 10,
|
|
}
|
|
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
|
s.Require().NoError(err)
|
|
s.services[i] = New(s.whisper[i], nil, db, config)
|
|
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
|
|
}
|
|
s.services[0].tracker.handler = newHandlerMock(1)
|
|
}
|
|
|
|
func (s *ShhExtSuite) TestInitProtocol() {
|
|
err := s.services[0].InitProtocolWithPassword("example-address", "`090///\nhtaa\rhta9x8923)$$'23")
|
|
s.NoError(err)
|
|
|
|
digest := sha3.Sum256([]byte("`090///\nhtaa\rhta9x8923)$$'23"))
|
|
encKey := fmt.Sprintf("%x", digest)
|
|
err = s.services[0].InitProtocolWithEncyptionKey("example-address", encKey)
|
|
s.NoError(err)
|
|
}
|
|
|
|
func (s *ShhExtSuite) TestPostMessageWithConfirmation() {
|
|
mock := newHandlerMock(1)
|
|
s.services[0].tracker.handler = mock
|
|
s.Require().NoError(s.services[0].UpdateMailservers([]*enode.Node{s.nodes[1].Server().Self()}))
|
|
s.nodes[0].Server().AddPeer(s.nodes[1].Server().Self())
|
|
symID, err := s.whisper[0].GenerateSymKey()
|
|
s.NoError(err)
|
|
client, err := s.nodes[0].Attach()
|
|
s.NoError(err)
|
|
var hash common.Hash
|
|
s.NoError(client.Call(&hash, "shhext_post", whisper.NewMessage{
|
|
SymKeyID: symID,
|
|
PowTarget: whisper.DefaultMinimumPoW,
|
|
PowTime: 200,
|
|
Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01},
|
|
Payload: []byte("hello"),
|
|
}))
|
|
s.NoError(err)
|
|
select {
|
|
case confirmed := <-mock.confirmations:
|
|
s.Equal(hash, confirmed)
|
|
case <-time.After(5 * time.Second):
|
|
s.Fail("timed out while waiting for confirmation")
|
|
}
|
|
}
|
|
|
|
func (s *ShhExtSuite) TestWaitMessageExpired() {
|
|
mock := newHandlerMock(1)
|
|
s.services[0].tracker.handler = mock
|
|
symID, err := s.whisper[0].GenerateSymKey()
|
|
s.NoError(err)
|
|
client, err := s.nodes[0].Attach()
|
|
s.NoError(err)
|
|
var hash common.Hash
|
|
s.NoError(client.Call(&hash, "shhext_post", whisper.NewMessage{
|
|
SymKeyID: symID,
|
|
PowTarget: whisper.DefaultMinimumPoW,
|
|
PowTime: 200,
|
|
TTL: 1,
|
|
Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01},
|
|
Payload: []byte("hello"),
|
|
}))
|
|
s.NoError(err)
|
|
select {
|
|
case expired := <-mock.expirations:
|
|
s.Equal(hash, expired)
|
|
case confirmed := <-mock.confirmations:
|
|
s.Fail("unexpected confirmation for hash", confirmed)
|
|
case <-time.After(10 * time.Second):
|
|
s.Fail("timed out while waiting for confirmation")
|
|
}
|
|
}
|
|
|
|
func (s *ShhExtSuite) TestRequestMessagesErrors() {
|
|
var err error
|
|
|
|
shh := 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 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,
|
|
}
|
|
service := New(shh, 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().String(), 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().String(),
|
|
Topics: []whisper.TopicType{{1}},
|
|
}))
|
|
s.EqualError(client.Call(nil, "shhext_requestMessages", MessagesRequest{
|
|
MailServerPeer: s.nodes[1].Server().Self().String(),
|
|
Topics: []whisper.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().String(),
|
|
Topics: []whisper.TopicType{{2}},
|
|
}))
|
|
}
|
|
|
|
func (s *ShhExtSuite) TestRequestMessagesSuccess() {
|
|
var err error
|
|
|
|
shh := 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.Require().NoError(err)
|
|
err = aNode.Register(func(*node.ServiceContext) (node.Service, error) { return 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,
|
|
}
|
|
service := New(shh, mock, nil, config)
|
|
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().String(), 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().String(),
|
|
SymKeyID: symKeyID,
|
|
Force: true,
|
|
})
|
|
s.Require().NoError(err)
|
|
s.Require().NotNil(hash)
|
|
s.Require().NoError(waitForHashInTracker(api.service.tracker, common.BytesToHash(hash), MailServerRequestSent, time.Second))
|
|
// 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().String(),
|
|
Force: true,
|
|
})
|
|
s.Require().NoError(err)
|
|
s.Require().NotNil(hash)
|
|
s.Require().NoError(waitForHashInTracker(api.service.tracker, common.BytesToHash(hash), MailServerRequestSent, time.Second))
|
|
}
|
|
|
|
func (s *ShhExtSuite) TestDebugPostSync() {
|
|
mock := newHandlerMock(1)
|
|
s.services[0].tracker.handler = mock
|
|
symID, err := s.whisper[0].GenerateSymKey()
|
|
s.NoError(err)
|
|
s.nodes[0].Server().AddPeer(s.nodes[1].Server().Self())
|
|
client, err := s.nodes[0].Attach()
|
|
s.NoError(err)
|
|
var hash common.Hash
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
msg whisper.NewMessage
|
|
postSyncTimeout time.Duration
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "timeout",
|
|
msg: whisper.NewMessage{
|
|
SymKeyID: symID,
|
|
PowTarget: whisper.DefaultMinimumPoW,
|
|
PowTime: 200,
|
|
Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01},
|
|
Payload: []byte("hello"),
|
|
},
|
|
postSyncTimeout: postSyncTimeout,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "invalid message",
|
|
msg: whisper.NewMessage{
|
|
PowTarget: whisper.DefaultMinimumPoW,
|
|
PowTime: 200,
|
|
Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01},
|
|
Payload: []byte("hello"),
|
|
},
|
|
postSyncTimeout: postSyncTimeout,
|
|
expectedErr: whisper.ErrSymAsym,
|
|
},
|
|
{
|
|
name: "context deadline exceeded",
|
|
msg: whisper.NewMessage{
|
|
SymKeyID: symID,
|
|
PowTarget: whisper.DefaultMinimumPoW,
|
|
PowTime: 10,
|
|
Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01},
|
|
TTL: 100,
|
|
Payload: []byte("hello"),
|
|
},
|
|
postSyncTimeout: 1 * time.Millisecond,
|
|
expectedErr: errors.New("context deadline exceeded"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.T().Run(tc.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), tc.postSyncTimeout)
|
|
defer cancel()
|
|
err := client.CallContext(ctx, &hash, "debug_postSync", tc.msg)
|
|
|
|
if tc.expectedErr != nil {
|
|
s.Equal(tc.expectedErr.Error(), err.Error())
|
|
} else {
|
|
s.NoError(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *ShhExtSuite) TestEnvelopeExpiredOnDebugPostSync() {
|
|
mock := newHandlerMock(1)
|
|
s.services[0].tracker.handler = mock
|
|
symID, err := s.whisper[0].GenerateSymKey()
|
|
s.NoError(err)
|
|
client, err := s.nodes[0].Attach()
|
|
s.NoError(err)
|
|
var hash common.Hash
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), postSyncTimeout)
|
|
defer cancel()
|
|
err = client.CallContext(ctx, &hash, "debug_postSync", whisper.NewMessage{
|
|
SymKeyID: symID,
|
|
PowTarget: whisper.DefaultMinimumPoW,
|
|
PowTime: 200,
|
|
Topic: whisper.TopicType{0x01, 0x01, 0x01, 0x01},
|
|
Payload: []byte("hello"),
|
|
TTL: 1,
|
|
})
|
|
|
|
s.Equal(errEnvelopeExpired.Error(), err.Error())
|
|
}
|
|
|
|
func (s *ShhExtSuite) TearDown() {
|
|
for _, n := range s.nodes {
|
|
s.NoError(n.Stop())
|
|
}
|
|
}
|
|
|
|
func waitForHashInTracker(track *tracker, hash common.Hash, state EnvelopeState, deadline time.Duration) error {
|
|
after := time.After(deadline)
|
|
ticker := time.Tick(100 * time.Millisecond)
|
|
for {
|
|
select {
|
|
case <-after:
|
|
return fmt.Errorf("failed while waiting for %s to get into state %d", hash, state)
|
|
case <-ticker:
|
|
if track.GetState(hash) == state {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRequestMessagesSync(t *testing.T) {
|
|
suite.Run(t, new(RequestMessagesSyncSuite))
|
|
}
|
|
|
|
type RequestMessagesSyncSuite struct {
|
|
suite.Suite
|
|
|
|
localAPI *PublicAPI
|
|
localNode *enode.Node
|
|
remoteRW *p2p.MsgPipeRW
|
|
}
|
|
|
|
func (s *RequestMessagesSyncSuite) SetupTest() {
|
|
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
|
s.Require().NoError(err)
|
|
conf := &whisper.Config{
|
|
MinimumAcceptedPOW: 0,
|
|
MaxMessageSize: 100 << 10,
|
|
}
|
|
w := whisper.New(conf)
|
|
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
|
|
}()
|
|
s.Require().NoError(p2p.ExpectMsg(rw1, statusCode, []interface{}{whisper.ProtocolVersion, math.Float64bits(w.MinPow()), w.BloomFilter(), false, true}))
|
|
s.Require().NoError(p2p.SendItems(rw1, statusCode, whisper.ProtocolVersion, whisper.ProtocolVersion, math.Float64bits(w.MinPow()), w.BloomFilter(), true, true))
|
|
|
|
service := New(w, nil, db, params.ShhextConfig{})
|
|
|
|
s.localAPI = NewPublicAPI(service)
|
|
s.localNode = node
|
|
s.remoteRW = rw1
|
|
}
|
|
|
|
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())
|
|
}()
|
|
s.Require().EqualError(s.localAPI.RequestMessagesSync(RetryConfig{
|
|
BaseTimeout: time.Second,
|
|
}, MessagesRequest{
|
|
MailServerPeer: s.localNode.String(),
|
|
}), "failed to request messages after 1 retries")
|
|
}
|
|
|
|
func (s *RequestMessagesSyncSuite) testCompletedFromAttempt(target int) {
|
|
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{}, []byte{})))
|
|
}
|
|
}()
|
|
s.Require().NoError(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)
|
|
}))
|
|
}
|
|
|
|
func (s *RequestMessagesSyncSuite) TestCompletedFromFirstAttempt() {
|
|
s.testCompletedFromAttempt(1)
|
|
}
|
|
|
|
func (s *RequestMessagesSyncSuite) TestCompletedFromSecondAttempt() {
|
|
s.testCompletedFromAttempt(2)
|
|
}
|