lotus/chain/messagepool/messagepool.go

660 lines
15 KiB
Go
Raw Normal View History

2019-12-01 23:11:43 +00:00
package messagepool
2019-07-05 14:46:21 +00:00
import (
2019-11-23 19:01:56 +00:00
"bytes"
2019-11-17 07:44:06 +00:00
"context"
"errors"
"sort"
2019-07-05 14:46:21 +00:00
"sync"
"time"
2019-07-08 12:51:45 +00:00
lru "github.com/hashicorp/golang-lru"
"github.com/ipfs/go-cid"
2019-11-23 19:01:56 +00:00
"github.com/ipfs/go-datastore"
"github.com/ipfs/go-datastore/namespace"
"github.com/ipfs/go-datastore/query"
2019-12-01 23:11:43 +00:00
logging "github.com/ipfs/go-log"
pubsub "github.com/libp2p/go-libp2p-pubsub"
2019-11-17 07:44:06 +00:00
lps "github.com/whyrusleeping/pubsub"
"go.uber.org/multierr"
2019-09-20 09:01:49 +00:00
"golang.org/x/xerrors"
2019-11-17 07:44:06 +00:00
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/build"
2019-12-01 23:11:43 +00:00
"github.com/filecoin-project/lotus/chain"
"github.com/filecoin-project/lotus/chain/address"
"github.com/filecoin-project/lotus/chain/stmgr"
"github.com/filecoin-project/lotus/chain/store"
"github.com/filecoin-project/lotus/chain/types"
2019-11-23 19:01:56 +00:00
"github.com/filecoin-project/lotus/node/modules/dtypes"
2019-07-05 14:46:21 +00:00
)
2019-12-01 23:11:43 +00:00
var log = logging.Logger("messagepool")
var (
ErrMessageTooBig = errors.New("message too big")
ErrMessageValueTooHigh = errors.New("cannot send more filecoin than will ever exist")
ErrNonceTooLow = errors.New("message nonce too low")
ErrNotEnoughFunds = errors.New("not enough funds to execute transaction")
ErrInvalidToAddr = errors.New("message had invalid to address")
)
const (
msgTopic = "/fil/messages"
2019-11-17 07:44:06 +00:00
2019-11-23 19:01:56 +00:00
localMsgsDs = "/mpool/local"
2019-11-17 07:44:06 +00:00
localUpdates = "update"
)
2019-07-05 14:46:21 +00:00
type MessagePool struct {
lk sync.Mutex
closer chan struct{}
repubTk *time.Ticker
localAddrs map[address.Address]struct{}
pending map[address.Address]*msgSet
pendingCount int
2019-07-05 14:46:21 +00:00
2019-12-03 19:33:29 +00:00
curTsLk sync.Mutex // DO NOT LOCK INSIDE lk
curTs *types.TipSet
2019-09-16 14:17:08 +00:00
2019-12-02 19:39:50 +00:00
api Provider
minGasPrice types.BigInt
maxTxPoolSize int
blsSigCache *lru.TwoQueueCache
2019-11-17 07:44:06 +00:00
changes *lps.PubSub
2019-11-23 19:01:56 +00:00
localMsgs datastore.Datastore
2019-07-05 14:46:21 +00:00
}
type msgSet struct {
msgs map[uint64]*types.SignedMessage
nextNonce uint64
curBalance types.BigInt
2019-07-05 14:46:21 +00:00
}
func newMsgSet() *msgSet {
return &msgSet{
msgs: make(map[uint64]*types.SignedMessage),
2019-07-05 14:46:21 +00:00
}
}
2019-09-20 09:01:49 +00:00
func (ms *msgSet) add(m *types.SignedMessage) error {
if len(ms.msgs) == 0 || m.Message.Nonce >= ms.nextNonce {
ms.nextNonce = m.Message.Nonce + 1
}
if _, has := ms.msgs[m.Message.Nonce]; has {
if m.Cid() != ms.msgs[m.Message.Nonce].Cid() {
log.Error("Add with duplicate nonce")
return xerrors.Errorf("message to %s with nonce %d already in mpool")
}
2019-07-05 14:46:21 +00:00
}
ms.msgs[m.Message.Nonce] = m
2019-09-20 09:01:49 +00:00
return nil
2019-07-05 14:46:21 +00:00
}
2019-12-02 19:39:50 +00:00
type Provider interface {
2019-12-03 19:33:29 +00:00
SubscribeHeadChanges(func(rev, app []*types.TipSet) error) *types.TipSet
PutMessage(m store.ChainMsg) (cid.Cid, error)
PubSubPublish(string, []byte) error
StateGetActor(address.Address, *types.TipSet) (*types.Actor, error)
MessagesForBlock(*types.BlockHeader) ([]*types.Message, []*types.SignedMessage, error)
MessagesForTipset(*types.TipSet) ([]store.ChainMsg, error)
2019-12-02 20:46:25 +00:00
LoadTipSet(cids []cid.Cid) (*types.TipSet, error)
}
type mpoolProvider struct {
sm *stmgr.StateManager
ps *pubsub.PubSub
}
2019-12-02 19:39:50 +00:00
func NewProvider(sm *stmgr.StateManager, ps *pubsub.PubSub) Provider {
return &mpoolProvider{sm, ps}
}
2019-12-03 19:33:29 +00:00
func (mpp *mpoolProvider) SubscribeHeadChanges(cb func(rev, app []*types.TipSet) error) *types.TipSet {
mpp.sm.ChainStore().SubscribeHeadChanges(cb)
2019-12-03 19:33:29 +00:00
return mpp.sm.ChainStore().GetHeaviestTipSet()
}
func (mpp *mpoolProvider) PutMessage(m store.ChainMsg) (cid.Cid, error) {
return mpp.sm.ChainStore().PutMessage(m)
}
func (mpp *mpoolProvider) PubSubPublish(k string, v []byte) error {
return mpp.ps.Publish(k, v)
}
func (mpp *mpoolProvider) StateGetActor(addr address.Address, ts *types.TipSet) (*types.Actor, error) {
return mpp.sm.GetActor(addr, ts)
}
func (mpp *mpoolProvider) MessagesForBlock(h *types.BlockHeader) ([]*types.Message, []*types.SignedMessage, error) {
return mpp.sm.ChainStore().MessagesForBlock(h)
}
func (mpp *mpoolProvider) MessagesForTipset(ts *types.TipSet) ([]store.ChainMsg, error) {
return mpp.sm.ChainStore().MessagesForTipset(ts)
}
2019-12-02 20:46:25 +00:00
func (mpp *mpoolProvider) LoadTipSet(cids []cid.Cid) (*types.TipSet, error) {
return mpp.sm.ChainStore().LoadTipSet(cids)
}
2019-12-02 19:39:50 +00:00
func New(api Provider, ds dtypes.MetadataDS) (*MessagePool, error) {
cache, _ := lru.New2Q(build.BlsSignatureCacheSize)
2019-07-05 14:46:21 +00:00
mp := &MessagePool{
closer: make(chan struct{}),
repubTk: time.NewTicker(build.BlockDelay * 10 * time.Second),
localAddrs: make(map[address.Address]struct{}),
pending: make(map[address.Address]*msgSet),
minGasPrice: types.NewInt(0),
2019-11-23 01:26:32 +00:00
maxTxPoolSize: 5000,
blsSigCache: cache,
2019-11-18 21:39:07 +00:00
changes: lps.New(50),
2019-11-23 19:01:56 +00:00
localMsgs: namespace.Wrap(ds, datastore.NewKey(localMsgsDs)),
api: api,
2019-11-23 19:01:56 +00:00
}
if err := mp.loadLocal(); err != nil {
return nil, xerrors.Errorf("loading local messages: %w", err)
2019-07-05 14:46:21 +00:00
}
2019-11-23 19:01:56 +00:00
go mp.repubLocal()
2019-12-03 19:33:29 +00:00
mp.curTs = api.SubscribeHeadChanges(func(rev, app []*types.TipSet) error {
err := mp.HeadChange(rev, app)
if err != nil {
log.Errorf("mpool head notif handler error: %+v", err)
}
return err
})
2019-07-05 14:46:21 +00:00
2019-11-23 19:01:56 +00:00
return mp, nil
2019-07-05 14:46:21 +00:00
}
func (mp *MessagePool) Close() error {
close(mp.closer)
return nil
}
func (mp *MessagePool) repubLocal() {
for {
select {
case <-mp.repubTk.C:
mp.lk.Lock()
msgs := make([]*types.SignedMessage, 0)
for a := range mp.localAddrs {
msgs = append(msgs, mp.pendingFor(a)...)
}
mp.lk.Unlock()
var errout error
for _, msg := range msgs {
msgb, err := msg.Serialize()
if err != nil {
2019-11-23 19:01:56 +00:00
errout = multierr.Append(errout, xerrors.Errorf("could not serialize: %w", err))
continue
}
err = mp.api.PubSubPublish(msgTopic, msgb)
if err != nil {
2019-11-23 19:01:56 +00:00
errout = multierr.Append(errout, xerrors.Errorf("could not publish: %w", err))
continue
}
}
if errout != nil {
log.Errorf("errors while republishing: %+v", errout)
}
case <-mp.closer:
mp.repubTk.Stop()
return
}
}
}
2019-11-23 19:01:56 +00:00
func (mp *MessagePool) addLocal(m *types.SignedMessage, msgb []byte) error {
mp.localAddrs[m.Message.From] = struct{}{}
if err := mp.localMsgs.Put(datastore.NewKey(string(m.Cid().Bytes())), msgb); err != nil {
return xerrors.Errorf("persisting local message: %w", err)
}
return nil
}
2019-09-16 14:17:08 +00:00
func (mp *MessagePool) Push(m *types.SignedMessage) error {
msgb, err := m.Serialize()
if err != nil {
return err
}
if err := mp.Add(m); err != nil {
return err
}
mp.lk.Lock()
2019-11-23 19:01:56 +00:00
if err := mp.addLocal(m, msgb); err != nil {
mp.lk.Unlock()
return err
}
mp.lk.Unlock()
return mp.api.PubSubPublish(msgTopic, msgb)
2019-09-16 14:17:08 +00:00
}
func (mp *MessagePool) Add(m *types.SignedMessage) error {
2019-12-03 19:33:29 +00:00
mp.curTsLk.Lock()
defer mp.curTsLk.Unlock()
return mp.addTs(m, mp.curTs)
}
func (mp *MessagePool) addTs(m *types.SignedMessage, curTs *types.TipSet) error {
// big messages are bad, anti DOS
if m.Size() > 32*1024 {
return xerrors.Errorf("mpool message too large (%dB): %w", m.Size(), ErrMessageTooBig)
}
if m.Message.To == address.Undef {
return ErrInvalidToAddr
}
if !m.Message.Value.LessThan(types.TotalFilecoinInt) {
return ErrMessageValueTooHigh
}
if err := m.Signature.Verify(m.Message.From, m.Message.Cid().Bytes()); err != nil {
log.Warnf("mpooladd signature verification failed: %s", err)
return err
}
2019-12-03 19:33:29 +00:00
snonce, err := mp.getStateNonce(m.Message.From, curTs)
if err != nil {
return xerrors.Errorf("failed to look up actor state nonce: %w", err)
}
if snonce > m.Message.Nonce {
return xerrors.Errorf("minimum expected nonce is %d: %w", snonce, ErrNonceTooLow)
}
balance, err := mp.getStateBalance(m.Message.From)
if err != nil {
return xerrors.Errorf("failed to check sender balance: %w", err)
}
if balance.LessThan(m.Message.RequiredFunds()) {
return xerrors.Errorf("not enough funds (required: %s, balance: %s): %w", types.FIL(m.Message.RequiredFunds()), types.FIL(balance), ErrNotEnoughFunds)
}
2019-07-05 14:46:21 +00:00
mp.lk.Lock()
defer mp.lk.Unlock()
2019-09-16 14:17:08 +00:00
return mp.addLocked(m)
}
func (mp *MessagePool) addLocked(m *types.SignedMessage) error {
log.Debugf("mpooladd: %s %s", m.Message.From, m.Message.Nonce)
if m.Signature.Type == types.KTBLS {
mp.blsSigCache.Add(m.Cid(), m.Signature)
}
2019-08-09 15:59:12 +00:00
if _, err := mp.api.PutMessage(m); err != nil {
2019-09-20 09:01:49 +00:00
log.Warnf("mpooladd cs.PutMessage failed: %s", err)
2019-07-05 14:46:21 +00:00
return err
}
if _, err := mp.api.PutMessage(&m.Message); err != nil {
2019-11-24 16:35:50 +00:00
log.Warnf("mpooladd cs.PutMessage failed: %s", err)
return err
}
2019-07-05 14:46:21 +00:00
mset, ok := mp.pending[m.Message.From]
if !ok {
mset = newMsgSet()
mp.pending[m.Message.From] = mset
}
2019-11-23 19:01:56 +00:00
if err := mset.add(m); err != nil {
log.Error(err)
}
2019-11-17 07:44:06 +00:00
mp.changes.Pub(api.MpoolUpdate{
Type: api.MpoolAdd,
Message: m,
}, localUpdates)
2019-07-05 14:46:21 +00:00
return nil
}
2019-07-17 06:05:11 +00:00
func (mp *MessagePool) GetNonce(addr address.Address) (uint64, error) {
2019-12-03 19:33:29 +00:00
mp.curTsLk.Lock()
2019-12-03 21:09:39 +00:00
defer mp.curTsLk.Unlock()
2019-12-03 19:33:29 +00:00
2019-07-17 06:05:11 +00:00
mp.lk.Lock()
defer mp.lk.Unlock()
2019-12-03 19:33:29 +00:00
return mp.getNonceLocked(addr, mp.curTs)
2019-09-16 14:17:08 +00:00
}
2019-12-03 19:33:29 +00:00
func (mp *MessagePool) getNonceLocked(addr address.Address, curTs *types.TipSet) (uint64, error) {
stateNonce, err := mp.getStateNonce(addr, curTs) // sanity check
2019-11-23 01:26:32 +00:00
if err != nil {
return 0, err
}
2019-07-17 06:05:11 +00:00
mset, ok := mp.pending[addr]
if ok {
2019-11-23 01:26:32 +00:00
if stateNonce > mset.nextNonce {
2019-11-24 16:35:50 +00:00
log.Errorf("state nonce was larger than mset.nextNonce (%d > %d)", stateNonce, mset.nextNonce)
2019-11-23 01:26:32 +00:00
return stateNonce, nil
}
2019-09-20 09:01:49 +00:00
return mset.nextNonce, nil
2019-07-17 06:05:11 +00:00
}
2019-11-23 01:26:32 +00:00
return stateNonce, nil
}
2019-12-03 19:33:29 +00:00
func (mp *MessagePool) getStateNonce(addr address.Address, curTs *types.TipSet) (uint64, error) {
// TODO: this method probably should be cached
2019-12-02 20:46:25 +00:00
act, err := mp.api.StateGetActor(addr, curTs)
2019-07-17 06:05:11 +00:00
if err != nil {
return 0, err
}
baseNonce := act.Nonce
// TODO: the correct thing to do here is probably to set curTs to chain.head
// but since we have an accurate view of the world until a head change occurs,
// this should be fine
2019-12-02 20:46:25 +00:00
if curTs == nil {
return baseNonce, nil
}
2019-12-02 20:46:25 +00:00
msgs, err := mp.api.MessagesForTipset(curTs)
if err != nil {
return 0, xerrors.Errorf("failed to check messages for tipset: %w", err)
}
for _, m := range msgs {
msg := m.VMMessage()
if msg.From == addr {
if msg.Nonce != baseNonce {
return 0, xerrors.Errorf("tipset %s has bad nonce ordering (%d != %d)", curTs.Cids(), msg.Nonce, baseNonce)
}
baseNonce++
}
}
return baseNonce, nil
2019-07-17 06:05:11 +00:00
}
func (mp *MessagePool) getStateBalance(addr address.Address) (types.BigInt, error) {
act, err := mp.api.StateGetActor(addr, nil)
if err != nil {
return types.EmptyInt, err
}
return act.Balance, nil
}
2019-09-17 08:15:26 +00:00
func (mp *MessagePool) PushWithNonce(addr address.Address, cb func(uint64) (*types.SignedMessage, error)) (*types.SignedMessage, error) {
2019-12-03 19:33:29 +00:00
mp.curTsLk.Lock()
2019-12-03 21:09:39 +00:00
defer mp.curTsLk.Unlock()
2019-12-03 19:33:29 +00:00
2019-09-16 14:17:08 +00:00
mp.lk.Lock()
defer mp.lk.Unlock()
if addr.Protocol() == address.ID {
log.Warnf("Called pushWithNonce with ID address (%s) this might not be handled properly yet", addr)
}
2019-09-16 14:17:08 +00:00
2019-12-03 19:33:29 +00:00
nonce, err := mp.getNonceLocked(addr, mp.curTs)
2019-09-16 14:17:08 +00:00
if err != nil {
2019-09-17 08:15:26 +00:00
return nil, err
2019-09-16 14:17:08 +00:00
}
msg, err := cb(nonce)
if err != nil {
2019-09-17 08:15:26 +00:00
return nil, err
2019-09-16 14:17:08 +00:00
}
msgb, err := msg.Serialize()
if err != nil {
2019-09-17 08:15:26 +00:00
return nil, err
2019-09-16 14:17:08 +00:00
}
if err := mp.addLocked(msg); err != nil {
2019-09-17 08:15:26 +00:00
return nil, err
2019-09-16 14:17:08 +00:00
}
2019-11-24 16:35:50 +00:00
if err := mp.addLocal(msg, msgb); err != nil {
log.Errorf("addLocal failed: %+v", err)
}
2019-09-16 14:17:08 +00:00
return msg, mp.api.PubSubPublish(msgTopic, msgb)
2019-09-16 14:17:08 +00:00
}
2019-08-14 04:43:29 +00:00
func (mp *MessagePool) Remove(from address.Address, nonce uint64) {
2019-07-05 14:46:21 +00:00
mp.lk.Lock()
defer mp.lk.Unlock()
2019-08-14 04:43:29 +00:00
mset, ok := mp.pending[from]
2019-07-05 14:46:21 +00:00
if !ok {
return
}
2019-11-19 21:26:25 +00:00
if m, ok := mset.msgs[nonce]; ok {
mp.changes.Pub(api.MpoolUpdate{
Type: api.MpoolRemove,
Message: m,
}, localUpdates)
}
2019-11-17 07:44:06 +00:00
2019-07-05 14:46:21 +00:00
// NB: This deletes any message with the given nonce. This makes sense
// as two messages with the same sender cannot have the same nonce
2019-08-14 04:43:29 +00:00
delete(mset.msgs, nonce)
2019-07-05 14:46:21 +00:00
if len(mset.msgs) == 0 {
2019-11-23 01:26:32 +00:00
delete(mp.pending, from)
2019-09-20 09:01:49 +00:00
} else {
var max uint64
for nonce := range mset.msgs {
if max < nonce {
max = nonce
}
}
2019-11-27 12:18:22 +00:00
if max < nonce {
2019-11-24 16:35:50 +00:00
max = nonce // we could have not seen the removed message before
}
2019-09-20 09:01:49 +00:00
mset.nextNonce = max + 1
2019-07-05 14:46:21 +00:00
}
}
2019-12-03 19:33:29 +00:00
func (mp *MessagePool) Pending() ([]*types.SignedMessage, *types.TipSet) {
mp.curTsLk.Lock()
defer mp.curTsLk.Unlock()
2019-07-05 14:46:21 +00:00
mp.lk.Lock()
defer mp.lk.Unlock()
2019-12-03 19:33:29 +00:00
2019-09-06 22:32:42 +00:00
out := make([]*types.SignedMessage, 0)
for a := range mp.pending {
out = append(out, mp.pendingFor(a)...)
}
2019-09-20 09:01:49 +00:00
2019-12-03 19:33:29 +00:00
return out, mp.curTs
}
2019-09-20 09:01:49 +00:00
func (mp *MessagePool) pendingFor(a address.Address) []*types.SignedMessage {
mset := mp.pending[a]
if mset == nil || len(mset.msgs) == 0 {
return nil
2019-07-05 14:46:21 +00:00
}
set := make([]*types.SignedMessage, 0, len(mset.msgs))
for _, m := range mset.msgs {
set = append(set, m)
}
sort.Slice(set, func(i, j int) bool {
return set[i].Message.Nonce < set[j].Message.Nonce
})
return set
2019-07-05 14:46:21 +00:00
}
2019-07-26 04:54:22 +00:00
func (mp *MessagePool) HeadChange(revert []*types.TipSet, apply []*types.TipSet) error {
2019-12-03 19:33:29 +00:00
mp.curTsLk.Lock()
defer mp.curTsLk.Unlock()
2019-07-05 14:46:21 +00:00
for _, ts := range revert {
2019-12-02 20:46:25 +00:00
pts, err := mp.api.LoadTipSet(ts.Parents())
if err != nil {
return err
}
2019-12-03 19:33:29 +00:00
msgs, err := mp.MessagesForBlocks(ts.Blocks())
if err != nil {
return err
}
2019-12-03 21:09:39 +00:00
mp.curTs = pts
2019-12-03 19:33:29 +00:00
for _, msg := range msgs {
if err := mp.addTs(msg, pts); err != nil {
log.Error(err) // TODO: probably lots of spam in multi-block tsets
}
2019-07-05 14:46:21 +00:00
}
}
for _, ts := range apply {
for _, b := range ts.Blocks() {
bmsgs, smsgs, err := mp.api.MessagesForBlock(b)
2019-07-05 14:46:21 +00:00
if err != nil {
return xerrors.Errorf("failed to get messages for apply block %s(height %d) (msgroot = %s): %w", b.Cid(), b.Height, b.Messages, err)
2019-07-05 14:46:21 +00:00
}
for _, msg := range smsgs {
2019-08-14 04:43:29 +00:00
mp.Remove(msg.Message.From, msg.Message.Nonce)
2019-07-05 14:46:21 +00:00
}
for _, msg := range bmsgs {
2019-08-14 04:43:29 +00:00
mp.Remove(msg.From, msg.Nonce)
}
2019-07-05 14:46:21 +00:00
}
2019-12-03 19:33:29 +00:00
mp.curTs = ts
2019-07-05 14:46:21 +00:00
}
return nil
}
2019-12-03 19:33:29 +00:00
func (mp *MessagePool) MessagesForBlocks(blks []*types.BlockHeader) ([]*types.SignedMessage, error) {
out := make([]*types.SignedMessage, 0)
for _, b := range blks {
bmsgs, smsgs, err := mp.api.MessagesForBlock(b)
if err != nil {
return nil, xerrors.Errorf("failed to get messages for apply block %s(height %d) (msgroot = %s): %w", b.Cid(), b.Height, b.Messages, err)
}
for _, msg := range smsgs {
out = append(out, msg)
}
for _, msg := range bmsgs {
smsg := mp.RecoverSig(msg)
if smsg != nil {
out = append(out, smsg)
} else {
log.Warnf("could not recover signature for bls message %s", msg.Cid())
}
}
}
return out, nil
}
func (mp *MessagePool) RecoverSig(msg *types.Message) *types.SignedMessage {
val, ok := mp.blsSigCache.Get(msg.Cid())
if !ok {
return nil
}
2019-11-12 11:42:19 +00:00
sig, ok := val.(types.Signature)
if !ok {
2019-11-24 16:35:50 +00:00
log.Errorf("value in signature cache was not a signature (got %T)", val)
return nil
}
return &types.SignedMessage{
Message: *msg,
2019-11-12 11:42:19 +00:00
Signature: sig,
}
}
2019-11-17 07:44:06 +00:00
func (mp *MessagePool) Updates(ctx context.Context) (<-chan api.MpoolUpdate, error) {
out := make(chan api.MpoolUpdate, 20)
sub := mp.changes.Sub(localUpdates)
go func() {
2019-12-01 23:11:43 +00:00
defer mp.changes.Unsub(sub, chain.LocalIncoming)
2019-11-19 19:49:11 +00:00
2019-11-17 07:44:06 +00:00
for {
select {
case u := <-sub:
select {
case out <- u.(api.MpoolUpdate):
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}()
return out, nil
}
2019-11-23 19:01:56 +00:00
func (mp *MessagePool) loadLocal() error {
res, err := mp.localMsgs.Query(query.Query{})
if err != nil {
return xerrors.Errorf("query local messages: %w", err)
}
for r := range res.Next() {
if r.Error != nil {
return xerrors.Errorf("r.Error: %w", r.Error)
}
var sm types.SignedMessage
if err := sm.UnmarshalCBOR(bytes.NewReader(r.Value)); err != nil {
return xerrors.Errorf("unmarshaling local message: %w", err)
}
if err := mp.Add(&sm); err != nil {
if xerrors.Is(err, ErrNonceTooLow) {
continue // todo: drop the message from local cache (if above certain confidence threshold)
}
2019-11-24 16:35:50 +00:00
return xerrors.Errorf("adding local message: %w", err)
2019-11-23 19:01:56 +00:00
}
}
return nil
}