Notify users that envelope was discarded and retry sending it (#1446)
API for querying mail servers that skips already received data on subsequent requests
This commit is contained in:
parent
28ec255d77
commit
218a35e609
100
db/db.go
100
db/db.go
|
@ -6,8 +6,10 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type storagePrefix byte
|
type storagePrefix byte
|
||||||
|
@ -20,8 +22,35 @@ const (
|
||||||
DeduplicatorCache
|
DeduplicatorCache
|
||||||
// MailserversCache is a list of mail servers provided by users.
|
// MailserversCache is a list of mail servers provided by users.
|
||||||
MailserversCache
|
MailserversCache
|
||||||
|
// TopicHistoryBucket isolated bucket for storing history metadata.
|
||||||
|
TopicHistoryBucket
|
||||||
|
// HistoryRequestBucket isolated bucket for storing list of pending requests.
|
||||||
|
HistoryRequestBucket
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewMemoryDB returns leveldb with memory backend prefixed with a bucket.
|
||||||
|
func NewMemoryDB() (*leveldb.DB, error) {
|
||||||
|
return leveldb.Open(storage.NewMemStorage(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDBNamespace returns instance that ensures isolated operations.
|
||||||
|
func NewDBNamespace(db *leveldb.DB, prefix storagePrefix) LevelDBNamespace {
|
||||||
|
return LevelDBNamespace{
|
||||||
|
db: db,
|
||||||
|
prefix: prefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemoryDBNamespace wraps in memory leveldb with provided bucket.
|
||||||
|
// Mostly used for tests. Including tests in other packages.
|
||||||
|
func NewMemoryDBNamespace(prefix storagePrefix) (pdb LevelDBNamespace, err error) {
|
||||||
|
db, err := NewMemoryDB()
|
||||||
|
if err != nil {
|
||||||
|
return pdb, err
|
||||||
|
}
|
||||||
|
return NewDBNamespace(db, prefix), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Key creates a DB key for a specified service with specified data
|
// Key creates a DB key for a specified service with specified data
|
||||||
func Key(prefix storagePrefix, data ...[]byte) []byte {
|
func Key(prefix storagePrefix, data ...[]byte) []byte {
|
||||||
keyLength := 1
|
keyLength := 1
|
||||||
|
@ -59,3 +88,74 @@ func Open(path string, opts *opt.Options) (db *leveldb.DB, err error) {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LevelDBNamespace database where all operations will be prefixed with a certain bucket.
|
||||||
|
type LevelDBNamespace struct {
|
||||||
|
db *leveldb.DB
|
||||||
|
prefix storagePrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db LevelDBNamespace) prefixedKey(key []byte) []byte {
|
||||||
|
endkey := make([]byte, len(key)+1)
|
||||||
|
endkey[0] = byte(db.prefix)
|
||||||
|
copy(endkey[1:], key)
|
||||||
|
return endkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db LevelDBNamespace) Put(key, value []byte) error {
|
||||||
|
return db.db.Put(db.prefixedKey(key), value, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db LevelDBNamespace) Get(key []byte) ([]byte, error) {
|
||||||
|
return db.db.Get(db.prefixedKey(key), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range returns leveldb util.Range prefixed with a single byte.
|
||||||
|
// If prefix is nil range will iterate over all records in a given bucket.
|
||||||
|
func (db LevelDBNamespace) Range(prefix, limit []byte) *util.Range {
|
||||||
|
if limit == nil {
|
||||||
|
return util.BytesPrefix(db.prefixedKey(prefix))
|
||||||
|
}
|
||||||
|
return &util.Range{Start: db.prefixedKey(prefix), Limit: db.prefixedKey(limit)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes key from database.
|
||||||
|
func (db LevelDBNamespace) Delete(key []byte) error {
|
||||||
|
return db.db.Delete(db.prefixedKey(key), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIterator returns iterator for a given slice.
|
||||||
|
func (db LevelDBNamespace) NewIterator(slice *util.Range) NamespaceIterator {
|
||||||
|
return NamespaceIterator{db.db.NewIterator(slice, nil)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamespaceIterator wraps leveldb iterator, works mostly the same way.
|
||||||
|
// The only difference is that first byte of the key is dropped.
|
||||||
|
type NamespaceIterator struct {
|
||||||
|
iter iterator.Iterator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns key of the current item.
|
||||||
|
func (iter NamespaceIterator) Key() []byte {
|
||||||
|
return iter.iter.Key()[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns actual value of the current item.
|
||||||
|
func (iter NamespaceIterator) Value() []byte {
|
||||||
|
return iter.iter.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns accumulated error.
|
||||||
|
func (iter NamespaceIterator) Error() error {
|
||||||
|
return iter.iter.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prev moves cursor backward.
|
||||||
|
func (iter NamespaceIterator) Prev() bool {
|
||||||
|
return iter.iter.Prev()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next moves cursor forward.
|
||||||
|
func (iter NamespaceIterator) Next() bool {
|
||||||
|
return iter.iter.Next()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
whisper "github.com/status-im/whisper/whisperv6"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrEmptyKey returned if key is not expected to be empty.
|
||||||
|
ErrEmptyKey = errors.New("TopicHistoryKey is empty")
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB is a common interface for DB operations.
|
||||||
|
type DB interface {
|
||||||
|
Get([]byte) ([]byte, error)
|
||||||
|
Put([]byte, []byte) error
|
||||||
|
Delete([]byte) error
|
||||||
|
Range([]byte, []byte) *util.Range
|
||||||
|
NewIterator(*util.Range) NamespaceIterator
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopicHistoryKey defines bytes that are used as unique key for TopicHistory.
|
||||||
|
// first 4 bytes are whisper.TopicType bytes
|
||||||
|
// next 8 bytes are time.Duration encoded in big endian notation.
|
||||||
|
type TopicHistoryKey [12]byte
|
||||||
|
|
||||||
|
// LoadTopicHistoryFromKey unmarshalls key into topic and duration and loads value of topic history
|
||||||
|
// from given database.
|
||||||
|
func LoadTopicHistoryFromKey(db DB, key TopicHistoryKey) (th TopicHistory, err error) {
|
||||||
|
if (key == TopicHistoryKey{}) {
|
||||||
|
return th, ErrEmptyKey
|
||||||
|
}
|
||||||
|
topic := whisper.TopicType{}
|
||||||
|
copy(topic[:], key[:4])
|
||||||
|
duration := binary.BigEndian.Uint64(key[4:])
|
||||||
|
th = TopicHistory{db: db, Topic: topic, Duration: time.Duration(duration)}
|
||||||
|
return th, th.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TopicHistory stores necessary information.
|
||||||
|
type TopicHistory struct {
|
||||||
|
db DB
|
||||||
|
// whisper topic
|
||||||
|
Topic whisper.TopicType
|
||||||
|
|
||||||
|
Duration time.Duration
|
||||||
|
// Timestamp that was used for the first request with this topic.
|
||||||
|
// Used to identify overlapping ranges.
|
||||||
|
First time.Time
|
||||||
|
// Timestamp of the last synced envelope.
|
||||||
|
Current time.Time
|
||||||
|
End time.Time
|
||||||
|
|
||||||
|
RequestID common.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns unique identifier for this TopicHistory.
|
||||||
|
func (t TopicHistory) Key() TopicHistoryKey {
|
||||||
|
key := TopicHistoryKey{}
|
||||||
|
copy(key[:], t.Topic[:])
|
||||||
|
binary.BigEndian.PutUint64(key[4:], uint64(t.Duration))
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value marshalls TopicHistory into bytes.
|
||||||
|
func (t TopicHistory) Value() ([]byte, error) {
|
||||||
|
return json.Marshal(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load TopicHistory from db using key and unmarshalls it.
|
||||||
|
func (t *TopicHistory) Load() error {
|
||||||
|
key := t.Key()
|
||||||
|
if (key == TopicHistoryKey{}) {
|
||||||
|
return errors.New("key is empty")
|
||||||
|
}
|
||||||
|
value, err := t.db.Get(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(value, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save persists TopicHistory on disk.
|
||||||
|
func (t TopicHistory) Save() error {
|
||||||
|
key := t.Key()
|
||||||
|
val, err := t.Value()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return t.db.Put(key[:], val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes topic history from database.
|
||||||
|
func (t TopicHistory) Delete() error {
|
||||||
|
key := t.Key()
|
||||||
|
return t.db.Delete(key[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// SameRange returns true if topic has same range, which means:
|
||||||
|
// true if Current is zero and Duration is the same
|
||||||
|
// and true if Current is the same
|
||||||
|
func (t TopicHistory) SameRange(other TopicHistory) bool {
|
||||||
|
zero := time.Time{}
|
||||||
|
if t.Current == zero && other.Current == zero {
|
||||||
|
return t.Duration == other.Duration
|
||||||
|
}
|
||||||
|
return t.Current == other.Current
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pending returns true if this topic was requested from a mail server.
|
||||||
|
func (t TopicHistory) Pending() bool {
|
||||||
|
return t.RequestID != common.Hash{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoryRequest is kept in the database while request is in the progress.
|
||||||
|
// Stores necessary information to identify topics with associated ranges included in the request.
|
||||||
|
type HistoryRequest struct {
|
||||||
|
requestDB DB
|
||||||
|
topicDB DB
|
||||||
|
|
||||||
|
histories []TopicHistory
|
||||||
|
|
||||||
|
// Generated ID
|
||||||
|
ID common.Hash
|
||||||
|
// List of the topics
|
||||||
|
TopicHistoryKeys []TopicHistoryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHistory adds instance to internal list of instance and add instance key to the list
|
||||||
|
// which will be persisted on disk.
|
||||||
|
func (req *HistoryRequest) AddHistory(history TopicHistory) {
|
||||||
|
req.histories = append(req.histories, history)
|
||||||
|
req.TopicHistoryKeys = append(req.TopicHistoryKeys, history.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Histories returns internal lsit of topic histories.
|
||||||
|
func (req *HistoryRequest) Histories() []TopicHistory {
|
||||||
|
// TODO Lazy load from database on first access
|
||||||
|
return req.histories
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns content of HistoryRequest as bytes.
|
||||||
|
func (req HistoryRequest) Value() ([]byte, error) {
|
||||||
|
return json.Marshal(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save persists all attached histories and request itself on the disk.
|
||||||
|
func (req HistoryRequest) Save() error {
|
||||||
|
for i := range req.histories {
|
||||||
|
th := &req.histories[i]
|
||||||
|
th.RequestID = req.ID
|
||||||
|
if err := th.Save(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val, err := req.Value()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return req.requestDB.Put(req.ID.Bytes(), val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete HistoryRequest from store and update every topic.
|
||||||
|
func (req HistoryRequest) Delete() error {
|
||||||
|
return req.requestDB.Delete(req.ID.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads request and topic histories content from disk and unmarshalls them.
|
||||||
|
func (req *HistoryRequest) Load() error {
|
||||||
|
val, err := req.requestDB.Get(req.ID.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return req.RawUnmarshall(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req *HistoryRequest) loadHistories() error {
|
||||||
|
for _, hk := range req.TopicHistoryKeys {
|
||||||
|
th, err := LoadTopicHistoryFromKey(req.topicDB, hk)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.histories = append(req.histories, th)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawUnmarshall unmarshall given bytes into the structure.
|
||||||
|
// Used in range queries to unmarshall content of the iter.Value directly into request struct.
|
||||||
|
func (req *HistoryRequest) RawUnmarshall(val []byte) error {
|
||||||
|
err := json.Unmarshal(val, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return req.loadHistories()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Includes checks if TopicHistory is included into the request.
|
||||||
|
func (req *HistoryRequest) Includes(history TopicHistory) bool {
|
||||||
|
key := history.Key()
|
||||||
|
for i := range req.TopicHistoryKeys {
|
||||||
|
if key == req.TopicHistoryKeys[i] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
whisper "github.com/status-im/whisper/whisperv6"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHistoryStore returns HistoryStore instance.
|
||||||
|
func NewHistoryStore(db *leveldb.DB) HistoryStore {
|
||||||
|
return HistoryStore{
|
||||||
|
topicDB: NewDBNamespace(db, TopicHistoryBucket),
|
||||||
|
requestDB: NewDBNamespace(db, HistoryRequestBucket),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistoryStore provides utility methods for quering history and requests store.
|
||||||
|
type HistoryStore struct {
|
||||||
|
topicDB DB
|
||||||
|
requestDB DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHistory creates history instance and loads history from database.
|
||||||
|
// Returns instance populated with topic and duration if history is not found in database.
|
||||||
|
func (h HistoryStore) GetHistory(topic whisper.TopicType, duration time.Duration) (TopicHistory, error) {
|
||||||
|
thist := h.NewHistory(topic, duration)
|
||||||
|
err := thist.Load()
|
||||||
|
if err != nil && err != errors.ErrNotFound {
|
||||||
|
return TopicHistory{}, err
|
||||||
|
}
|
||||||
|
return thist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequest returns instance of the HistoryRequest.
|
||||||
|
func (h HistoryStore) NewRequest() HistoryRequest {
|
||||||
|
return HistoryRequest{requestDB: h.requestDB, topicDB: h.topicDB}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHistory creates TopicHistory object with required values.
|
||||||
|
func (h HistoryStore) NewHistory(topic whisper.TopicType, duration time.Duration) TopicHistory {
|
||||||
|
return TopicHistory{db: h.topicDB, Duration: duration, Topic: topic}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRequest loads HistoryRequest from database.
|
||||||
|
func (h HistoryStore) GetRequest(id common.Hash) (HistoryRequest, error) {
|
||||||
|
req := HistoryRequest{requestDB: h.requestDB, topicDB: h.topicDB, ID: id}
|
||||||
|
err := req.Load()
|
||||||
|
if err != nil {
|
||||||
|
return HistoryRequest{}, err
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllRequests loads all not-finished history requests from database.
|
||||||
|
func (h HistoryStore) GetAllRequests() ([]HistoryRequest, error) {
|
||||||
|
rst := []HistoryRequest{}
|
||||||
|
iter := h.requestDB.NewIterator(h.requestDB.Range(nil, nil))
|
||||||
|
for iter.Next() {
|
||||||
|
req := HistoryRequest{
|
||||||
|
requestDB: h.requestDB,
|
||||||
|
topicDB: h.topicDB,
|
||||||
|
}
|
||||||
|
err := req.RawUnmarshall(iter.Value())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rst = append(rst, req)
|
||||||
|
}
|
||||||
|
return rst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHistoriesByTopic returns all histories with a given topic.
|
||||||
|
// This is needed when we will have multiple range per single topic.
|
||||||
|
// TODO explain
|
||||||
|
func (h HistoryStore) GetHistoriesByTopic(topic whisper.TopicType) ([]TopicHistory, error) {
|
||||||
|
rst := []TopicHistory{}
|
||||||
|
iter := h.topicDB.NewIterator(h.topicDB.Range(topic[:], nil))
|
||||||
|
for iter.Next() {
|
||||||
|
key := TopicHistoryKey{}
|
||||||
|
copy(key[:], iter.Key())
|
||||||
|
th, err := LoadTopicHistoryFromKey(h.topicDB, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rst = append(rst, th)
|
||||||
|
}
|
||||||
|
return rst, nil
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
whisper "github.com/status-im/whisper/whisperv6"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createInMemStore(t *testing.T) HistoryStore {
|
||||||
|
db, err := NewMemoryDB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
return NewHistoryStore(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNewHistory(t *testing.T) {
|
||||||
|
topic := whisper.TopicType{1}
|
||||||
|
duration := time.Hour
|
||||||
|
store := createInMemStore(t)
|
||||||
|
th, err := store.GetHistory(topic, duration)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, duration, th.Duration)
|
||||||
|
require.Equal(t, topic, th.Topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetExistingHistory(t *testing.T) {
|
||||||
|
topic := whisper.TopicType{1}
|
||||||
|
duration := time.Hour
|
||||||
|
store := createInMemStore(t)
|
||||||
|
th, err := store.GetHistory(topic, duration)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
th.Current = now
|
||||||
|
require.NoError(t, th.Save())
|
||||||
|
|
||||||
|
th, err = store.GetHistory(topic, duration)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, now.Unix(), th.Current.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHistoryRequest(t *testing.T) {
|
||||||
|
store := createInMemStore(t)
|
||||||
|
id := common.Hash{1}
|
||||||
|
req, err := store.GetRequest(id)
|
||||||
|
require.Error(t, err)
|
||||||
|
req = store.NewRequest()
|
||||||
|
req.ID = id
|
||||||
|
|
||||||
|
th, err := store.GetHistory(whisper.TopicType{1}, time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.AddHistory(th)
|
||||||
|
require.NoError(t, req.Save())
|
||||||
|
|
||||||
|
req, err = store.GetRequest(id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, req.Histories(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAllRequests(t *testing.T) {
|
||||||
|
store := createInMemStore(t)
|
||||||
|
idOne := common.Hash{1}
|
||||||
|
idTwo := common.Hash{2}
|
||||||
|
|
||||||
|
req := store.NewRequest()
|
||||||
|
req.ID = idOne
|
||||||
|
require.NoError(t, req.Save())
|
||||||
|
|
||||||
|
all, err := store.GetAllRequests()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, all, 1)
|
||||||
|
|
||||||
|
req = store.NewRequest()
|
||||||
|
req.ID = idTwo
|
||||||
|
require.NoError(t, req.Save())
|
||||||
|
|
||||||
|
all, err = store.GetAllRequests()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, all, 2)
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
whisper "github.com/status-im/whisper/whisperv6"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTopicHistoryStoreLoadFromKey(t *testing.T) {
|
||||||
|
db, err := NewMemoryDBNamespace(TopicHistoryBucket)
|
||||||
|
require.NoError(t, err)
|
||||||
|
th := TopicHistory{
|
||||||
|
db: db,
|
||||||
|
Topic: whisper.TopicType{1, 1, 1},
|
||||||
|
Duration: 10 * time.Hour,
|
||||||
|
}
|
||||||
|
require.NoError(t, th.Save())
|
||||||
|
now := time.Now()
|
||||||
|
th.Current = now
|
||||||
|
require.NoError(t, th.Save())
|
||||||
|
|
||||||
|
th, err = LoadTopicHistoryFromKey(db, th.Key())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, now.Unix(), th.Current.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTopicHistorySameRange(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
testCases := []struct {
|
||||||
|
description string
|
||||||
|
result bool
|
||||||
|
histories [2]TopicHistory
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "SameDurationCurrentNotSet",
|
||||||
|
result: true,
|
||||||
|
histories: [2]TopicHistory{
|
||||||
|
{Duration: time.Minute}, {Duration: time.Minute},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "DifferentDurationCurrentNotset",
|
||||||
|
result: false,
|
||||||
|
histories: [2]TopicHistory{
|
||||||
|
{Duration: time.Minute}, {Duration: time.Hour},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "SameCurrent",
|
||||||
|
result: true,
|
||||||
|
histories: [2]TopicHistory{
|
||||||
|
{Current: now}, {Current: now},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "DifferentCurrent",
|
||||||
|
result: false,
|
||||||
|
histories: [2]TopicHistory{
|
||||||
|
{Current: now}, {Current: now.Add(time.Hour)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
require.Equal(t, tc.result, tc.histories[0].SameRange(tc.histories[1]))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddHistory(t *testing.T) {
|
||||||
|
topic := whisper.TopicType{1, 1, 1}
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
topicdb, err := NewMemoryDBNamespace(TopicHistoryBucket)
|
||||||
|
require.NoError(t, err)
|
||||||
|
requestdb, err := NewMemoryDBNamespace(HistoryRequestBucket)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
th := TopicHistory{db: topicdb, Topic: topic, Current: now}
|
||||||
|
id := common.Hash{1}
|
||||||
|
|
||||||
|
req := HistoryRequest{requestDB: requestdb, topicDB: topicdb, ID: id}
|
||||||
|
req.AddHistory(th)
|
||||||
|
require.NoError(t, req.Save())
|
||||||
|
|
||||||
|
req = HistoryRequest{requestDB: requestdb, topicDB: topicdb, ID: id}
|
||||||
|
require.NoError(t, req.Load())
|
||||||
|
|
||||||
|
require.Len(t, req.Histories(), 1)
|
||||||
|
require.Equal(t, th.Topic, req.Histories()[0].Topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestIncludesMethod(t *testing.T) {
|
||||||
|
topicOne := whisper.TopicType{1}
|
||||||
|
topicTwo := whisper.TopicType{2}
|
||||||
|
testCases := []struct {
|
||||||
|
description string
|
||||||
|
result bool
|
||||||
|
topics []TopicHistory
|
||||||
|
input TopicHistory
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "EmptyTopic",
|
||||||
|
result: false,
|
||||||
|
input: TopicHistory{Topic: topicOne},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "MatchesTopic",
|
||||||
|
result: true,
|
||||||
|
topics: []TopicHistory{{Topic: topicOne}},
|
||||||
|
input: TopicHistory{Topic: topicOne},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "NotMatchesTopic",
|
||||||
|
result: false,
|
||||||
|
topics: []TopicHistory{{Topic: topicOne}},
|
||||||
|
input: TopicHistory{Topic: topicTwo},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
req := HistoryRequest{}
|
||||||
|
for _, t := range tc.topics {
|
||||||
|
req.AddHistory(t)
|
||||||
|
}
|
||||||
|
require.Equal(t, tc.result, req.Includes(tc.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -367,6 +367,10 @@ type ShhextConfig struct {
|
||||||
|
|
||||||
// MaxMessageDeliveryAttempts defines how many times we will try to deliver not-acknowledged envelopes.
|
// MaxMessageDeliveryAttempts defines how many times we will try to deliver not-acknowledged envelopes.
|
||||||
MaxMessageDeliveryAttempts int
|
MaxMessageDeliveryAttempts int
|
||||||
|
|
||||||
|
// WhisperCacheDir is a folder where whisper filters may persist messages before delivering them
|
||||||
|
// to a client.
|
||||||
|
WhisperCacheDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the ShhextConfig struct and returns an error if inconsistent values are found
|
// Validate validates the ShhextConfig struct and returns an error if inconsistent values are found
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||||
|
"github.com/status-im/status-go/db"
|
||||||
"github.com/status-im/status-go/mailserver"
|
"github.com/status-im/status-go/mailserver"
|
||||||
"github.com/status-im/status-go/services/shhext/chat"
|
"github.com/status-im/status-go/services/shhext/chat"
|
||||||
"github.com/status-im/status-go/services/shhext/dedup"
|
"github.com/status-im/status-go/services/shhext/dedup"
|
||||||
|
@ -154,6 +155,15 @@ type SyncMessagesResponse struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitiateHistoryRequestParams type for initiating history requests from a peer.
|
||||||
|
type InitiateHistoryRequestParams struct {
|
||||||
|
Peer string
|
||||||
|
SymKeyID string
|
||||||
|
Requests []TopicRequest
|
||||||
|
Force bool
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
// -----
|
// -----
|
||||||
// PUBLIC API
|
// PUBLIC API
|
||||||
// -----
|
// -----
|
||||||
|
@ -418,6 +428,14 @@ func (api *PublicAPI) GetNewFilterMessages(filterID string) ([]dedup.Deduplicate
|
||||||
// ConfirmMessagesProcessed is a method to confirm that messages was consumed by
|
// ConfirmMessagesProcessed is a method to confirm that messages was consumed by
|
||||||
// the client side.
|
// the client side.
|
||||||
func (api *PublicAPI) ConfirmMessagesProcessed(messages []*whisper.Message) error {
|
func (api *PublicAPI) ConfirmMessagesProcessed(messages []*whisper.Message) error {
|
||||||
|
for _, msg := range messages {
|
||||||
|
if msg.P2P {
|
||||||
|
err := api.service.historyUpdates.UpdateTopicHistory(msg.Topic, time.Unix(int64(msg.Timestamp), 0))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return api.service.deduplicator.AddMessages(messages)
|
return api.service.deduplicator.AddMessages(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,6 +511,103 @@ func (api *PublicAPI) SendDirectMessage(ctx context.Context, msg chat.SendDirect
|
||||||
return api.Post(ctx, whisperMessage)
|
return api.Post(ctx, whisperMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *PublicAPI) requestMessagesUsingPayload(request db.HistoryRequest, peer, symkeyID string, payload []byte, force bool, timeout time.Duration, topics []whisper.TopicType) (hash common.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()
|
||||||
|
|
||||||
|
request.ID = hash
|
||||||
|
err = request.Save()
|
||||||
|
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 {
|
||||||
|
err = request.Delete()
|
||||||
|
if err != nil {
|
||||||
|
return hash, err
|
||||||
|
}
|
||||||
|
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(request InitiateHistoryRequestParams) ([]hexutil.Bytes, error) {
|
||||||
|
rst := []hexutil.Bytes{}
|
||||||
|
requests, err := api.service.historyUpdates.CreateRequests(request.Requests)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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[:])
|
||||||
|
}
|
||||||
|
return rst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteRequest client must mark request completed when all envelopes were processed.
|
||||||
|
func (api *PublicAPI) CompleteRequest(ctx context.Context, hex string) error {
|
||||||
|
return api.service.historyUpdates.UpdateFinishedRequest(common.HexToHash(hex))
|
||||||
|
}
|
||||||
|
|
||||||
// DEPRECATED: use SendDirectMessage with DH flag
|
// DEPRECATED: use SendDirectMessage with DH flag
|
||||||
// SendPairingMessage sends a 1:1 chat message to our own devices to initiate a pairing session
|
// SendPairingMessage sends a 1:1 chat message to our own devices to initiate a pairing session
|
||||||
func (api *PublicAPI) SendPairingMessage(ctx context.Context, msg chat.SendDirectMessageRPC) ([]hexutil.Bytes, error) {
|
func (api *PublicAPI) SendPairingMessage(ctx context.Context, msg chat.SendDirectMessageRPC) ([]hexutil.Bytes, error) {
|
||||||
|
|
|
@ -196,7 +196,7 @@ func (m *EnvelopesMonitor) handleAcknowledgedBatch(event whisper.EnvelopeEvent)
|
||||||
log.Debug("received a confirmation", "batch", event.Batch, "peer", event.Peer)
|
log.Debug("received a confirmation", "batch", event.Batch, "peer", event.Peer)
|
||||||
envelopeErrors, ok := event.Data.([]whisper.EnvelopeError)
|
envelopeErrors, ok := event.Data.([]whisper.EnvelopeError)
|
||||||
if event.Data != nil && !ok {
|
if event.Data != nil && !ok {
|
||||||
log.Warn("received unexpected data for the confirmation event", "batch", event.Batch)
|
log.Error("received unexpected data in the the confirmation event", "batch", event.Batch)
|
||||||
}
|
}
|
||||||
failedEnvelopes := map[common.Hash]struct{}{}
|
failedEnvelopes := map[common.Hash]struct{}{}
|
||||||
for i := range envelopeErrors {
|
for i := range envelopeErrors {
|
||||||
|
|
|
@ -0,0 +1,360 @@
|
||||||
|
package shhext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/status-im/status-go/db"
|
||||||
|
"github.com/status-im/status-go/mailserver"
|
||||||
|
whisper "github.com/status-im/whisper/whisperv6"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeSource is a function that returns current time.
|
||||||
|
type TimeSource func() time.Time
|
||||||
|
|
||||||
|
// NewHistoryUpdateReactor creates HistoryUpdateReactor instance.
|
||||||
|
func NewHistoryUpdateReactor(store db.HistoryStore, registry *RequestsRegistry, timeSource TimeSource) *HistoryUpdateReactor {
|
||||||
|
return &HistoryUpdateReactor{
|
||||||
|
store: store,
|
||||||
|
registry: registry,
|
||||||
|
timeSource: timeSource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
store db.HistoryStore
|
||||||
|
registry *RequestsRegistry
|
||||||
|
timeSource TimeSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFinishedRequest removes successfully finished request and updates every topic
|
||||||
|
// attached to the request.
|
||||||
|
func (reactor *HistoryUpdateReactor) UpdateFinishedRequest(id common.Hash) error {
|
||||||
|
reactor.mu.Lock()
|
||||||
|
defer reactor.mu.Unlock()
|
||||||
|
req, err := reactor.store.GetRequest(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range req.Histories() {
|
||||||
|
th := &req.Histories()[i]
|
||||||
|
th.RequestID = common.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(topic whisper.TopicType, timestamp time.Time) error {
|
||||||
|
reactor.mu.Lock()
|
||||||
|
defer reactor.mu.Unlock()
|
||||||
|
histories, err := reactor.store.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 whisper.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(topicRequests []TopicRequest) ([]db.HistoryRequest, error) {
|
||||||
|
reactor.mu.Lock()
|
||||||
|
defer reactor.mu.Unlock()
|
||||||
|
seen := map[whisper.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[whisper.TopicType]db.TopicHistory{}
|
||||||
|
for i := range topicRequests {
|
||||||
|
th, err := reactor.store.GetHistory(topicRequests[i].Topic, topicRequests[i].Duration)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
histories[th.Topic] = th
|
||||||
|
}
|
||||||
|
requests, err := reactor.store.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 !reactor.registry.Has(req.ID) {
|
||||||
|
filtered = append(filtered, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjusted, err := adjustRequestedHistories(reactor.store, mapToList(histories))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filtered = append(filtered,
|
||||||
|
GroupHistoriesByRequestTimespan(reactor.store, adjusted)...)
|
||||||
|
return RenewRequests(filtered, reactor.timeSource()), 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[whisper.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 whisper.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([]whisper.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() []whisper.TopicType {
|
||||||
|
rst := make([]whisper.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: 10000,
|
||||||
|
}
|
||||||
|
return rlp.EncodeToBytes(payload)
|
||||||
|
}
|
|
@ -0,0 +1,350 @@
|
||||||
|
package shhext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
"github.com/status-im/status-go/db"
|
||||||
|
"github.com/status-im/status-go/mailserver"
|
||||||
|
whisper "github.com/status-im/whisper/whisperv6"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createInMemStore(t *testing.T) db.HistoryStore {
|
||||||
|
mdb, err := db.NewMemoryDB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
return db.NewHistoryStore(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 := whisper.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: whisper.TopicType{1}, Range: Range{Start: 1, End: 10}},
|
||||||
|
{Topic: whisper.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: 10000,
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
now := time.Now()
|
||||||
|
reactor := NewHistoryUpdateReactor(
|
||||||
|
createInMemStore(t), NewRequestsRegistry(0),
|
||||||
|
func() time.Time { return now })
|
||||||
|
requests, err := reactor.CreateRequests([]TopicRequest{
|
||||||
|
{Topic: whisper.TopicType{1}, Duration: time.Hour},
|
||||||
|
{Topic: whisper.TopicType{2}, Duration: time.Hour},
|
||||||
|
{Topic: whisper.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) {
|
||||||
|
store := createInMemStore(t)
|
||||||
|
req := store.NewRequest()
|
||||||
|
req.ID = common.Hash{1}
|
||||||
|
th := store.NewHistory(whisper.TopicType{1}, time.Hour)
|
||||||
|
req.AddHistory(th)
|
||||||
|
require.NoError(t, req.Save())
|
||||||
|
reactor := NewHistoryUpdateReactor(store, NewRequestsRegistry(0), time.Now)
|
||||||
|
requests, err := reactor.CreateRequests([]TopicRequest{
|
||||||
|
{Topic: whisper.TopicType{1}, Duration: time.Hour},
|
||||||
|
{Topic: whisper.TopicType{2}, Duration: time.Hour},
|
||||||
|
{Topic: whisper.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) {
|
||||||
|
now := time.Now()
|
||||||
|
reactor := NewHistoryUpdateReactor(
|
||||||
|
createInMemStore(t), NewRequestsRegistry(0),
|
||||||
|
func() time.Time { return now })
|
||||||
|
topic := whisper.TopicType{1}
|
||||||
|
requests, err := reactor.CreateRequests([]TopicRequest{
|
||||||
|
{Topic: topic, Duration: time.Hour},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, requests, 1)
|
||||||
|
requests[0].ID = common.Hash{1}
|
||||||
|
require.NoError(t, requests[0].Save())
|
||||||
|
|
||||||
|
// duration changed. request wasn't finished
|
||||||
|
requests, err = reactor.CreateRequests([]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 = common.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(r.ID))
|
||||||
|
}
|
||||||
|
requests, err = reactor.CreateRequests([]TopicRequest{
|
||||||
|
{Topic: topic, Duration: 10 * time.Hour},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, requests, 1)
|
||||||
|
|
||||||
|
topics, err := reactor.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) {
|
||||||
|
store := createInMemStore(t)
|
||||||
|
req := store.NewRequest()
|
||||||
|
req.ID = common.Hash{1}
|
||||||
|
now := time.Now()
|
||||||
|
thOne := store.NewHistory(whisper.TopicType{1}, time.Hour)
|
||||||
|
thOne.End = now
|
||||||
|
thTwo := store.NewHistory(whisper.TopicType{2}, time.Hour)
|
||||||
|
thTwo.End = now
|
||||||
|
req.AddHistory(thOne)
|
||||||
|
req.AddHistory(thTwo)
|
||||||
|
require.NoError(t, req.Save())
|
||||||
|
|
||||||
|
reactor := NewHistoryUpdateReactor(store, NewRequestsRegistry(0), time.Now)
|
||||||
|
require.NoError(t, reactor.UpdateTopicHistory(thOne.Topic, now.Add(-time.Minute)))
|
||||||
|
require.NoError(t, reactor.UpdateFinishedRequest(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) {
|
||||||
|
reqID := common.Hash{1}
|
||||||
|
store := createInMemStore(t)
|
||||||
|
request := store.NewRequest()
|
||||||
|
request.ID = reqID
|
||||||
|
now := time.Now()
|
||||||
|
require.NoError(t, request.Save())
|
||||||
|
th := store.NewHistory(whisper.TopicType{1}, time.Hour)
|
||||||
|
th.RequestID = request.ID
|
||||||
|
th.End = now
|
||||||
|
require.NoError(t, th.Save())
|
||||||
|
reactor := NewHistoryUpdateReactor(store, NewRequestsRegistry(0), time.Now)
|
||||||
|
timestamp := now.Add(-time.Minute)
|
||||||
|
|
||||||
|
require.NoError(t, reactor.UpdateTopicHistory(th.Topic, timestamp))
|
||||||
|
require.NoError(t, th.Load())
|
||||||
|
require.Equal(t, timestamp.Unix(), th.Current.Unix())
|
||||||
|
|
||||||
|
require.NoError(t, reactor.UpdateTopicHistory(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: whisper.TopicType{1}, Duration: time.Hour},
|
||||||
|
{Topic: whisper.TopicType{2}, Duration: time.Hour},
|
||||||
|
{Topic: whisper.TopicType{3}, Duration: 2 * time.Hour},
|
||||||
|
{Topic: whisper.TopicType{4}, Duration: 2 * time.Hour},
|
||||||
|
{Topic: whisper.TopicType{5}, Duration: 3 * time.Hour},
|
||||||
|
{Topic: whisper.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(whisper.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 := whisper.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 := whisper.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 := whisper.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 := whisper.TopicType{1}
|
||||||
|
hour := store.NewHistory(topic, time.Hour)
|
||||||
|
hour.RequestID = common.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)
|
||||||
|
}
|
|
@ -57,6 +57,14 @@ func (r *RequestsRegistry) Register(uid common.Hash, topics []whisper.TopicType)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Has returns true if given uid is stored in registry.
|
||||||
|
func (r *RequestsRegistry) Has(uid common.Hash) bool {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
_, exist := r.uidToTopics[uid]
|
||||||
|
return exist
|
||||||
|
}
|
||||||
|
|
||||||
// Unregister removes request with given UID from registry.
|
// Unregister removes request with given UID from registry.
|
||||||
func (r *RequestsRegistry) Unregister(uid common.Hash) {
|
func (r *RequestsRegistry) Unregister(uid common.Hash) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
"github.com/status-im/status-go/db"
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/services/shhext/chat"
|
"github.com/status-im/status-go/services/shhext/chat"
|
||||||
"github.com/status-im/status-go/services/shhext/dedup"
|
"github.com/status-im/status-go/services/shhext/dedup"
|
||||||
|
@ -46,6 +47,7 @@ type Service struct {
|
||||||
envelopesMonitor *EnvelopesMonitor
|
envelopesMonitor *EnvelopesMonitor
|
||||||
mailMonitor *MailRequestMonitor
|
mailMonitor *MailRequestMonitor
|
||||||
requestsRegistry *RequestsRegistry
|
requestsRegistry *RequestsRegistry
|
||||||
|
historyUpdates *HistoryUpdateReactor
|
||||||
server *p2p.Server
|
server *p2p.Server
|
||||||
nodeID *ecdsa.PrivateKey
|
nodeID *ecdsa.PrivateKey
|
||||||
deduplicator *dedup.Deduplicator
|
deduplicator *dedup.Deduplicator
|
||||||
|
@ -53,7 +55,6 @@ type Service struct {
|
||||||
dataDir string
|
dataDir string
|
||||||
installationID string
|
installationID string
|
||||||
pfsEnabled bool
|
pfsEnabled bool
|
||||||
|
|
||||||
peerStore *mailservers.PeerStore
|
peerStore *mailservers.PeerStore
|
||||||
cache *mailservers.Cache
|
cache *mailservers.Cache
|
||||||
connManager *mailservers.ConnectionManager
|
connManager *mailservers.ConnectionManager
|
||||||
|
@ -64,14 +65,15 @@ type Service struct {
|
||||||
var _ node.Service = (*Service)(nil)
|
var _ node.Service = (*Service)(nil)
|
||||||
|
|
||||||
// New returns a new Service. dataDir is a folder path to a network-independent location
|
// New returns a new Service. dataDir is a folder path to a network-independent location
|
||||||
func New(w *whisper.Whisper, handler EnvelopeEventsHandler, db *leveldb.DB, config params.ShhextConfig) *Service {
|
func New(w *whisper.Whisper, handler EnvelopeEventsHandler, ldb *leveldb.DB, config params.ShhextConfig) *Service {
|
||||||
cache := mailservers.NewCache(db)
|
cache := mailservers.NewCache(ldb)
|
||||||
ps := mailservers.NewPeerStore(cache)
|
ps := mailservers.NewPeerStore(cache)
|
||||||
delay := defaultRequestsDelay
|
delay := defaultRequestsDelay
|
||||||
if config.RequestsDelay != 0 {
|
if config.RequestsDelay != 0 {
|
||||||
delay = config.RequestsDelay
|
delay = config.RequestsDelay
|
||||||
}
|
}
|
||||||
requestsRegistry := NewRequestsRegistry(delay)
|
requestsRegistry := NewRequestsRegistry(delay)
|
||||||
|
historyUpdates := NewHistoryUpdateReactor(db.NewHistoryStore(ldb), requestsRegistry, w.GetCurrentTime)
|
||||||
mailMonitor := &MailRequestMonitor{
|
mailMonitor := &MailRequestMonitor{
|
||||||
w: w,
|
w: w,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
|
@ -85,7 +87,8 @@ func New(w *whisper.Whisper, handler EnvelopeEventsHandler, db *leveldb.DB, conf
|
||||||
envelopesMonitor: envelopesMonitor,
|
envelopesMonitor: envelopesMonitor,
|
||||||
mailMonitor: mailMonitor,
|
mailMonitor: mailMonitor,
|
||||||
requestsRegistry: requestsRegistry,
|
requestsRegistry: requestsRegistry,
|
||||||
deduplicator: dedup.NewDeduplicator(w, db),
|
historyUpdates: historyUpdates,
|
||||||
|
deduplicator: dedup.NewDeduplicator(w, ldb),
|
||||||
dataDir: config.BackupDisabledDataDir,
|
dataDir: config.BackupDisabledDataDir,
|
||||||
installationID: config.InstallationID,
|
installationID: config.InstallationID,
|
||||||
pfsEnabled: config.PFSEnabled,
|
pfsEnabled: config.PFSEnabled,
|
||||||
|
|
|
@ -3,6 +3,7 @@ package shhext
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
|
@ -13,10 +14,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/node"
|
"github.com/ethereum/go-ethereum/node"
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||||
|
"github.com/status-im/status-go/mailserver"
|
||||||
"github.com/status-im/status-go/params"
|
"github.com/status-im/status-go/params"
|
||||||
"github.com/status-im/status-go/t/helpers"
|
"github.com/status-im/status-go/t/helpers"
|
||||||
"github.com/status-im/status-go/t/utils"
|
"github.com/status-im/status-go/t/utils"
|
||||||
|
@ -681,3 +684,230 @@ func (s *WhisperRetriesSuite) TestDeliveredFromFirstAttempt() {
|
||||||
func (s *WhisperRetriesSuite) TestDeliveredFromSecondAttempt() {
|
func (s *WhisperRetriesSuite) TestDeliveredFromSecondAttempt() {
|
||||||
s.testDelivery(2)
|
s.testDelivery(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequestWithTrackingHistorySuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(RequestWithTrackingHistorySuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestWithTrackingHistorySuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
envelopeSymkey string
|
||||||
|
envelopeSymkeyID string
|
||||||
|
|
||||||
|
localWhisperAPI *whisper.PublicWhisperAPI
|
||||||
|
localAPI *PublicAPI
|
||||||
|
localService *Service
|
||||||
|
mailSymKey string
|
||||||
|
|
||||||
|
remoteMailserver *mailserver.WMailServer
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
local := whisper.New(conf)
|
||||||
|
s.Require().NoError(local.Start(nil))
|
||||||
|
|
||||||
|
s.localWhisperAPI = whisper.NewPublicWhisperAPI(local)
|
||||||
|
s.localService = New(local, nil, db, params.ShhextConfig{})
|
||||||
|
localPkey, err := crypto.GenerateKey()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().NoError(s.localService.Start(&p2p.Server{Config: p2p.Config{PrivateKey: localPkey}}))
|
||||||
|
s.localAPI = NewPublicAPI(s.localService)
|
||||||
|
|
||||||
|
remote := whisper.New(conf)
|
||||||
|
s.remoteWhisper = remote
|
||||||
|
s.Require().NoError(remote.Start(nil))
|
||||||
|
s.remoteMailserver = &mailserver.WMailServer{}
|
||||||
|
remote.RegisterServer(s.remoteMailserver)
|
||||||
|
password := "test"
|
||||||
|
tmpdir, err := ioutil.TempDir("", "tracking-history-tests-")
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().NoError(s.remoteMailserver.Init(remote, ¶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 := local.HandlePeer(remotePeer, rw1)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
err := remote.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 ...whisper.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(), whisper.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 ...whisper.TopicType) string {
|
||||||
|
filterid, err := s.localWhisperAPI.NewMessageFilter(whisper.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) []hexutil.Bytes {
|
||||||
|
requests, err := s.localAPI.InitiateHistoryRequests(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.localService.historyUpdates.store
|
||||||
|
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 := whisper.TopicType{1, 1, 1, 1}
|
||||||
|
topic2 := whisper.TopicType{2, 2, 2, 2}
|
||||||
|
topic3 := whisper.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.waitMessagesDelivered(filterid, hexes...)
|
||||||
|
|
||||||
|
s.Require().NoError(s.localService.historyUpdates.UpdateTopicHistory(topic1, time.Now()))
|
||||||
|
s.Require().NoError(s.localService.historyUpdates.UpdateTopicHistory(topic2, time.Now()))
|
||||||
|
s.Require().NoError(s.localService.historyUpdates.UpdateTopicHistory(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 := whisper.TopicType{1, 1, 1, 1}
|
||||||
|
topic2 := whisper.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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,9 @@ const (
|
||||||
// to any peer
|
// to any peer
|
||||||
EventEnvelopeExpired = "envelope.expired"
|
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 is triggered when whisper receives a message ack from the mailserver
|
||||||
EventMailServerRequestCompleted = "mailserver.request.completed"
|
EventMailServerRequestCompleted = "mailserver.request.completed"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue