discovery: prevent endBlock overflow in replyChanRangeQuery

Modifies syncer.replyChanRangeQuery method to use the LastBlockHeight
method on the query. LastBlockHeight safely calculates the ending
block height and prevents an overflow of start_block + num_blocks.

Prior to this change, query messages that had a start_block +
num_blocks that overflows uint32_max would return zero results in the
reply message.

Tests are added to fix the bug and ensure proper start and end values
are supplied to the channel graph filter.
This commit is contained in:
Brian Mancini 2020-06-18 15:23:20 -04:00
parent 9b8d51231c
commit 28931390ff
2 changed files with 113 additions and 1 deletions

@ -918,7 +918,7 @@ func (g *GossipSyncer) replyChanRangeQuery(query *lnwire.QueryChannelRange) erro
// Next, we'll consult the time series to obtain the set of known
// channel ID's that match their query.
startBlock := query.FirstBlockHeight
endBlock := startBlock + query.NumBlocks - 1
endBlock := query.LastBlockHeight()
channelRange, err := g.cfg.channelSeries.FilterChannelRange(
query.ChainHash, startBlock, endBlock,
)

@ -915,6 +915,118 @@ func TestGossipSyncerReplyChanRangeQuery(t *testing.T) {
}
}
// TestGossipSyncerReplyChanRangeQuery tests a variety of
// QueryChannelRange messages to ensure the underlying queries are
// executed with the correct block range
func TestGossipSyncerReplyChanRangeQueryBlockRange(t *testing.T) {
t.Parallel()
// First create our test gossip syncer that will handle and
// respond to the test queries
_, syncer, chanSeries := newTestSyncer(
lnwire.NewShortChanIDFromInt(10), defaultEncoding, math.MaxInt32,
)
// Next construct test queries with various startBlock and endBlock
// ranges
queryReqs := []*lnwire.QueryChannelRange{
// full range example
{
FirstBlockHeight: uint32(0),
NumBlocks: uint32(math.MaxUint32),
},
// small query example that does not overflow
{
FirstBlockHeight: uint32(1000),
NumBlocks: uint32(100),
},
// overflow example
{
FirstBlockHeight: uint32(1000),
NumBlocks: uint32(math.MaxUint32),
},
}
// Next construct the expected filterRangeReq startHeight and endHeight
// values that we will compare to the captured values
expFilterReqs := []filterRangeReq{
{
startHeight: uint32(0),
endHeight: uint32(math.MaxUint32 - 1),
},
{
startHeight: uint32(1000),
endHeight: uint32(1099),
},
{
startHeight: uint32(1000),
endHeight: uint32(math.MaxUint32),
},
}
// We'll then launch a goroutine to capture the filterRangeReqs for
// each request and return those results once all queries have been
// received
resultsCh := make(chan []filterRangeReq, 1)
errCh := make(chan error, 1)
go func() {
// We will capture the values supplied to the chanSeries here
// and return the results once all the requests have been
// collected
capFilterReqs := []filterRangeReq{}
for filterReq := range chanSeries.filterRangeReqs {
// capture the filter request so we can compare to the
// expected values later
capFilterReqs = append(capFilterReqs, filterReq)
// Reply with an empty result for each query to allow
// unblock the caller
queryResp := []lnwire.ShortChannelID{}
chanSeries.filterRangeResp <- queryResp
// Once we have collected all results send the results
// back to the main thread and terminate the goroutine
if len(capFilterReqs) == len(expFilterReqs) {
resultsCh <- capFilterReqs
return
}
}
}()
// We'll launch a goroutine to send the query sequentially. This
// goroutine ensures that the timeout logic below on the mainthread
// will be reached
go func() {
for _, query := range queryReqs {
if err := syncer.replyChanRangeQuery(query); err != nil {
errCh <- fmt.Errorf("unable to issue query: %v", err)
return
}
}
}()
// Wait for the results to be collected and validate that the
// collected results match the expected results, the timeout to
// expire, or an error to occur
select {
case capFilterReq := <-resultsCh:
if !reflect.DeepEqual(expFilterReqs, capFilterReq) {
t.Fatalf("mismatched filter reqs: expected %v, got %v",
spew.Sdump(expFilterReqs), spew.Sdump(capFilterReq))
}
case <-time.After(time.Second * 10):
t.Fatalf("goroutine did not return within 10 seconds")
case err := <-errCh:
if err != nil {
t.Fatal(err)
}
}
}
// TestGossipSyncerReplyChanRangeQueryNoNewChans tests that if we issue a reply
// for a channel range query, and we don't have any new channels, then we send
// back a single response that signals completion.