discovery: adhere to proper channel chunk splitting for ReplyChannelRange
This commit is contained in:
7 files changed
+340
-145
No files matched your search
+128
-87
@@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -128,6 +130,14 @@ const (
|
||||
// maxUndelayedQueryReplies queries.
|
||||
DefaultDelayedQueryReplyInterval = 5 * time.Second
|
||||
|
||||
// maxQueryChanRangeReplies specifies the default limit of replies to
|
||||
// process for a single QueryChannelRange request.
|
||||
maxQueryChanRangeReplies = 500
|
||||
|
||||
// maxQueryChanRangeRepliesZlibFactor specifies the factor applied to
|
||||
// the maximum number of replies allowed for zlib encoded replies.
|
||||
maxQueryChanRangeRepliesZlibFactor = 4
|
||||
|
||||
// chanRangeQueryBuffer is the number of blocks back that we'll go when
|
||||
// asking the remote peer for their any channels they know of beyond
|
||||
// our highest known channel ID.
|
||||
@@ -240,6 +250,10 @@ type gossipSyncerCfg struct {
|
||||
|
||||
// bestHeight returns the latest height known of the chain.
|
||||
bestHeight func() uint32
|
||||
|
||||
// maxQueryChanRangeReplies is the maximum number of replies we'll allow
|
||||
// for a single QueryChannelRange request.
|
||||
maxQueryChanRangeReplies uint32
|
||||
}
|
||||
|
||||
// GossipSyncer is a struct that handles synchronizing the channel graph state
|
||||
@@ -316,6 +330,11 @@ type GossipSyncer struct {
|
||||
// buffer all the chunked response to our query.
|
||||
bufferedChanRangeReplies []lnwire.ShortChannelID
|
||||
|
||||
// numChanRangeRepliesRcvd is used to track the number of replies
|
||||
// received as part of a QueryChannelRange. This field is primarily used
|
||||
// within the waitingQueryChanReply state.
|
||||
numChanRangeRepliesRcvd uint32
|
||||
|
||||
// newChansToQuery is used to pass the set of channels we should query
|
||||
// for from the waitingQueryChanReply state to the queryNewChannels
|
||||
// state.
|
||||
@@ -741,17 +760,27 @@ func (g *GossipSyncer) processChanRangeReply(msg *lnwire.ReplyChannelRange) erro
|
||||
g.bufferedChanRangeReplies = append(
|
||||
g.bufferedChanRangeReplies, msg.ShortChanIDs...,
|
||||
)
|
||||
switch g.cfg.encodingType {
|
||||
case lnwire.EncodingSortedPlain:
|
||||
g.numChanRangeRepliesRcvd++
|
||||
case lnwire.EncodingSortedZlib:
|
||||
g.numChanRangeRepliesRcvd += maxQueryChanRangeRepliesZlibFactor
|
||||
default:
|
||||
return fmt.Errorf("unhandled encoding type %v", g.cfg.encodingType)
|
||||
}
|
||||
|
||||
log.Infof("GossipSyncer(%x): buffering chan range reply of size=%v",
|
||||
g.cfg.peerPub[:], len(msg.ShortChanIDs))
|
||||
|
||||
// If this isn't the last response, then we can exit as we've already
|
||||
// buffered the latest portion of the streaming reply.
|
||||
// If this isn't the last response and we can continue to receive more,
|
||||
// then we can exit as we've already buffered the latest portion of the
|
||||
// streaming reply.
|
||||
maxReplies := g.cfg.maxQueryChanRangeReplies
|
||||
switch {
|
||||
// If we're communicating with a legacy node, we'll need to look at the
|
||||
// complete field.
|
||||
case isLegacyReplyChannelRange(g.curQueryRangeMsg, msg):
|
||||
if msg.Complete == 0 {
|
||||
if msg.Complete == 0 && g.numChanRangeRepliesRcvd < maxReplies {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -763,7 +792,8 @@ func (g *GossipSyncer) processChanRangeReply(msg *lnwire.ReplyChannelRange) erro
|
||||
// TODO(wilmer): This might require some padding if the remote
|
||||
// node is not aware of the last height we sent them, i.e., is
|
||||
// behind a few blocks from us.
|
||||
if replyLastHeight < queryLastHeight {
|
||||
if replyLastHeight < queryLastHeight &&
|
||||
g.numChanRangeRepliesRcvd < maxReplies {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -786,6 +816,7 @@ func (g *GossipSyncer) processChanRangeReply(msg *lnwire.ReplyChannelRange) erro
|
||||
g.curQueryRangeMsg = nil
|
||||
g.prevReplyChannelRange = nil
|
||||
g.bufferedChanRangeReplies = nil
|
||||
g.numChanRangeRepliesRcvd = 0
|
||||
|
||||
// If there aren't any channels that we don't know of, then we can
|
||||
// switch straight to our terminal state.
|
||||
@@ -930,7 +961,7 @@ func (g *GossipSyncer) replyChanRangeQuery(query *lnwire.QueryChannelRange) erro
|
||||
// channel ID's that match their query.
|
||||
startBlock := query.FirstBlockHeight
|
||||
endBlock := query.LastBlockHeight()
|
||||
channelRange, err := g.cfg.channelSeries.FilterChannelRange(
|
||||
channelRanges, err := g.cfg.channelSeries.FilterChannelRange(
|
||||
query.ChainHash, startBlock, endBlock,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -940,102 +971,98 @@ func (g *GossipSyncer) replyChanRangeQuery(query *lnwire.QueryChannelRange) erro
|
||||
// TODO(roasbeef): means can't send max uint above?
|
||||
// * or make internal 64
|
||||
|
||||
// In the base case (no actual response) the first block and last block
|
||||
// will match those of the query. In the loop below, we'll update these
|
||||
// two variables incrementally with each chunk to properly compute the
|
||||
// starting block for each response and the number of blocks in a
|
||||
// response.
|
||||
firstBlockHeight := startBlock
|
||||
lastBlockHeight := endBlock
|
||||
// We'll send our response in a streaming manner, chunk-by-chunk. We do
|
||||
// this as there's a transport message size limit which we'll need to
|
||||
// adhere to. We also need to make sure all of our replies cover the
|
||||
// expected range of the query.
|
||||
sendReplyForChunk := func(channelChunk []lnwire.ShortChannelID,
|
||||
firstHeight, lastHeight uint32, finalChunk bool) error {
|
||||
|
||||
numChannels := int32(len(channelRange))
|
||||
numChansSent := int32(0)
|
||||
for {
|
||||
// We'll send our this response in a streaming manner,
|
||||
// chunk-by-chunk. We do this as there's a transport message
|
||||
// size limit which we'll need to adhere to.
|
||||
var channelChunk []lnwire.ShortChannelID
|
||||
|
||||
// We know this is the final chunk, if the difference between
|
||||
// the total number of channels, and the number of channels
|
||||
// we've sent is less-than-or-equal to the chunk size.
|
||||
isFinalChunk := (numChannels - numChansSent) <= g.cfg.chunkSize
|
||||
|
||||
// If this is indeed the last chunk, then we'll send the
|
||||
// remainder of the channels.
|
||||
if isFinalChunk {
|
||||
channelChunk = channelRange[numChansSent:]
|
||||
|
||||
log.Infof("GossipSyncer(%x): sending final chan "+
|
||||
"range chunk, size=%v", g.cfg.peerPub[:],
|
||||
len(channelChunk))
|
||||
} else {
|
||||
// Otherwise, we'll only send off a fragment exactly
|
||||
// sized to the proper chunk size.
|
||||
channelChunk = channelRange[numChansSent : numChansSent+g.cfg.chunkSize]
|
||||
|
||||
log.Infof("GossipSyncer(%x): sending range chunk of "+
|
||||
"size=%v", g.cfg.peerPub[:], len(channelChunk))
|
||||
}
|
||||
|
||||
// If we have any channels at all to return, then we need to
|
||||
// update our pointers to the first and last blocks for each
|
||||
// response.
|
||||
if len(channelChunk) > 0 {
|
||||
// If this is the first response we'll send, we'll point
|
||||
// the first block to the first block in the query.
|
||||
// Otherwise, we'll continue from the block we left off
|
||||
// at.
|
||||
if numChansSent == 0 {
|
||||
firstBlockHeight = startBlock
|
||||
} else {
|
||||
firstBlockHeight = lastBlockHeight
|
||||
}
|
||||
|
||||
// If this is the last response we'll send, we'll point
|
||||
// the last block to the last block of the query.
|
||||
// Otherwise, we'll set it to the height of the last
|
||||
// channel in the chunk.
|
||||
if isFinalChunk {
|
||||
lastBlockHeight = endBlock
|
||||
} else {
|
||||
lastBlockHeight = channelChunk[len(channelChunk)-1].BlockHeight
|
||||
}
|
||||
}
|
||||
|
||||
// The number of blocks contained in this response (the total
|
||||
// span) is the difference between the last channel ID and the
|
||||
// first in the range. We add one as even if all channels
|
||||
// The number of blocks contained in the current chunk (the
|
||||
// total span) is the difference between the last channel ID and
|
||||
// the first in the range. We add one as even if all channels
|
||||
// returned are in the same block, we need to count that.
|
||||
numBlocksInResp := lastBlockHeight - firstBlockHeight + 1
|
||||
numBlocks := lastHeight - firstHeight + 1
|
||||
complete := uint8(0)
|
||||
if finalChunk {
|
||||
complete = 1
|
||||
}
|
||||
|
||||
// With our chunk assembled, we'll now send to the remote peer
|
||||
// the current chunk.
|
||||
replyChunk := lnwire.ReplyChannelRange{
|
||||
return g.cfg.sendToPeerSync(&lnwire.ReplyChannelRange{
|
||||
QueryChannelRange: lnwire.QueryChannelRange{
|
||||
ChainHash: query.ChainHash,
|
||||
NumBlocks: numBlocksInResp,
|
||||
FirstBlockHeight: firstBlockHeight,
|
||||
NumBlocks: numBlocks,
|
||||
FirstBlockHeight: firstHeight,
|
||||
},
|
||||
Complete: 0,
|
||||
Complete: complete,
|
||||
EncodingType: g.cfg.encodingType,
|
||||
ShortChanIDs: channelChunk,
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
firstHeight = query.FirstBlockHeight
|
||||
lastHeight uint32
|
||||
channelChunk []lnwire.ShortChannelID
|
||||
)
|
||||
for _, channelRange := range channelRanges {
|
||||
channels := channelRange.Channels
|
||||
numChannels := int32(len(channels))
|
||||
numLeftToAdd := g.cfg.chunkSize - int32(len(channelChunk))
|
||||
|
||||
// Include the current block in the ongoing chunk if it can fit
|
||||
// and move on to the next block.
|
||||
if numChannels <= numLeftToAdd {
|
||||
channelChunk = append(channelChunk, channels...)
|
||||
continue
|
||||
}
|
||||
if isFinalChunk {
|
||||
replyChunk.Complete = 1
|
||||
}
|
||||
if err := g.cfg.sendToPeerSync(&replyChunk); err != nil {
|
||||
|
||||
// Otherwise, we need to send our existing channel chunk as is
|
||||
// as its own reply and start a new one for the current block.
|
||||
// We'll mark the end of our current chunk as the height before
|
||||
// the current block to ensure the whole query range is replied
|
||||
// to.
|
||||
log.Infof("GossipSyncer(%x): sending range chunk of size=%v",
|
||||
g.cfg.peerPub[:], len(channelChunk))
|
||||
lastHeight = channelRange.Height - 1
|
||||
err := sendReplyForChunk(
|
||||
channelChunk, firstHeight, lastHeight, false,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this was the final chunk, then we'll exit now as our
|
||||
// response is now complete.
|
||||
if isFinalChunk {
|
||||
return nil
|
||||
// With the reply constructed, we'll start tallying channels for
|
||||
// our next one keeping in mind our chunk size. This may result
|
||||
// in channels for this block being left out from the reply, but
|
||||
// this isn't an issue since we'll randomly shuffle them and we
|
||||
// assume a historical gossip sync is performed at a later time.
|
||||
firstHeight = channelRange.Height
|
||||
chunkSize := numChannels
|
||||
exceedsChunkSize := numChannels > g.cfg.chunkSize
|
||||
if exceedsChunkSize {
|
||||
rand.Shuffle(len(channels), func(i, j int) {
|
||||
channels[i], channels[j] = channels[j], channels[i]
|
||||
})
|
||||
chunkSize = g.cfg.chunkSize
|
||||
}
|
||||
channelChunk = channels[:chunkSize]
|
||||
|
||||
numChansSent += int32(len(channelChunk))
|
||||
// Sort the chunk once again if we had to shuffle it.
|
||||
if exceedsChunkSize {
|
||||
sort.Slice(channelChunk, func(i, j int) bool {
|
||||
return channelChunk[i].ToUint64() <
|
||||
channelChunk[j].ToUint64()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Send the remaining chunk as the final reply.
|
||||
log.Infof("GossipSyncer(%x): sending final chan range chunk, size=%v",
|
||||
g.cfg.peerPub[:], len(channelChunk))
|
||||
return sendReplyForChunk(
|
||||
channelChunk, firstHeight, query.LastBlockHeight(), true,
|
||||
)
|
||||
}
|
||||
|
||||
// replyShortChanIDs will be dispatched in response to a query by the remote
|
||||
@@ -1285,11 +1312,23 @@ func (g *GossipSyncer) FilterGossipMsgs(msgs ...msgWithSenders) {
|
||||
|
||||
// ProcessQueryMsg is used by outside callers to pass new channel time series
|
||||
// queries to the internal processing goroutine.
|
||||
func (g *GossipSyncer) ProcessQueryMsg(msg lnwire.Message, peerQuit <-chan struct{}) {
|
||||
func (g *GossipSyncer) ProcessQueryMsg(msg lnwire.Message, peerQuit <-chan struct{}) error {
|
||||
var msgChan chan lnwire.Message
|
||||
switch msg.(type) {
|
||||
case *lnwire.QueryChannelRange, *lnwire.QueryShortChanIDs:
|
||||
msgChan = g.queryMsgs
|
||||
|
||||
// Reply messages should only be expected in states where we're waiting
|
||||
// for a reply.
|
||||
case *lnwire.ReplyChannelRange, *lnwire.ReplyShortChanIDsEnd:
|
||||
syncState := g.syncState()
|
||||
if syncState != waitingQueryRangeReply &&
|
||||
syncState != waitingQueryChanReply {
|
||||
return fmt.Errorf("received unexpected query reply "+
|
||||
"message %T", msg)
|
||||
}
|
||||
msgChan = g.gossipMsgs
|
||||
|
||||
default:
|
||||
msgChan = g.gossipMsgs
|
||||
}
|
||||
@@ -1299,6 +1338,8 @@ func (g *GossipSyncer) ProcessQueryMsg(msg lnwire.Message, peerQuit <-chan struc
|
||||
case <-peerQuit:
|
||||
case <-g.quit:
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setSyncState sets the gossip syncer's state to the given state.
|
||||
|
||||
Reference in new issue
Block a user