Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions discovery/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ func (g *GossipSyncer) channelGraphSyncer(ctx context.Context) {
if err != nil {
log.Errorf("Unable to "+
"process chan range "+
"query: %v", err)
"reply: %v", err)
return
}
continue
Expand Down Expand Up @@ -647,9 +647,18 @@ func (g *GossipSyncer) channelGraphSyncer(ctx context.Context) {
// If this is the final reply to one of our
// queries, then we'll loop back into our query
// state to send of the remaining query chunks.
_, ok := msg.(*lnwire.ReplyShortChanIDsEnd)
reply, ok := msg.(*lnwire.ReplyShortChanIDsEnd)
if ok {
g.setSyncState(queryNewChannels)
err := g.processScidEndReply(
ctx, reply,
)
if err != nil {
log.Errorf("Unable to "+
"process short chan "+
"id end reply: %v", err)

return
}
continue
}

Expand Down Expand Up @@ -919,6 +928,14 @@ func isLegacyReplyChannelRange(query *lnwire.QueryChannelRange,
func (g *GossipSyncer) processChanRangeReply(_ context.Context,
msg *lnwire.ReplyChannelRange) error {

// If the peer sent us a reply with a different chain hash, then we are
// not going to process the message any further.
if g.cfg.chainHash != msg.ChainHash {
return fmt.Errorf("reply includes channels for "+
"chain=%v, we're on chain=%v", msg.ChainHash,
g.cfg.chainHash)
}

// isStale returns whether the timestamp is too far into the past.
isStale := func(timestamp time.Time) bool {
return time.Since(timestamp) > graph.DefaultChannelPruneExpiry
Expand Down Expand Up @@ -1103,6 +1120,25 @@ func (g *GossipSyncer) processChanRangeReply(_ context.Context,
return nil
}

// processScidEndReply is called when the GossipSyncer receives the final reply
// to one of our query SCIDs requests, after which we transition back into the
// query state.
func (g *GossipSyncer) processScidEndReply(_ context.Context,
msg *lnwire.ReplyShortChanIDsEnd) error {

// If the peer sent us a reply with a different chain hash, then we are
// not going to process the message any further.
if g.cfg.chainHash != msg.ChainHash {
return fmt.Errorf("reply includes channels for "+
"chain=%v, we're on chain=%v", msg.ChainHash,
g.cfg.chainHash)
}

g.setSyncState(queryNewChannels)

return nil
}

// genChanRangeQuery generates the initial message we'll send to the remote
// party when we're kicking off the channel graph synchronization upon
// connection. The historicalQuery boolean can be used to generate a query from
Expand Down Expand Up @@ -1433,6 +1469,13 @@ func (g *GossipSyncer) replyShortChanIDs(ctx context.Context,
func (g *GossipSyncer) ApplyGossipFilter(ctx context.Context,
filter *lnwire.GossipTimestampRange) error {

// If the peer sent us a gossip with a different chain hash, then we are
// not going to process the message any further.
if g.cfg.chainHash != filter.ChainHash {
return fmt.Errorf("gossip filter specifies chain=%v, we're on "+
"chain=%v", filter.ChainHash, g.cfg.chainHash)
}

g.Lock()

g.remoteUpdateHorizon = filter
Expand Down
1 change: 1 addition & 0 deletions discovery/syncer_atomic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func TestGossipSyncerSingleBacklogSend(t *testing.T) {

// Now we'll create a filter, then apply it in a goroutine.
filter := &lnwire.GossipTimestampRange{
ChainHash: syncer.cfg.chainHash,
FirstTimestamp: uint32(time.Now().Unix() - 3600),
TimestampRange: 7200,
}
Expand Down
3 changes: 2 additions & 1 deletion discovery/syncer_queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ func TestGossipSyncerQueueOrder(t *testing.T) {
orderMu.Lock()
processedRanges = append(
processedRanges, &lnwire.GossipTimestampRange{
ChainHash: syncer.cfg.chainHash,
FirstTimestamp: uint32(
req.start.Unix(),
),
Expand Down Expand Up @@ -408,7 +409,7 @@ func TestGossipSyncerQueueOrder(t *testing.T) {
var queuedMessages []*lnwire.GossipTimestampRange
for i := 0; i < numMessages; i++ {
msg := &lnwire.GossipTimestampRange{
ChainHash: chainhash.Hash{},
ChainHash: syncer.cfg.chainHash,
FirstTimestamp: uint32(1000 + i*100),
TimestampRange: 3600,
}
Expand Down
103 changes: 103 additions & 0 deletions discovery/syncer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ func newTestSyncer(hID lnwire.ShortChannelID,

msgChan := make(chan []lnwire.Message, 20)
cfg := gossipSyncerCfg{
chainHash: *chaincfg.MainNetParams.GenesisHash,
channelSeries: newMockChannelGraphTimeSeries(hID),
encodingType: encodingType,
chunkSize: chunkSize,
Expand Down Expand Up @@ -555,6 +556,7 @@ func TestGossipSyncerApplyNoHistoricalGossipFilter(t *testing.T) {

// We'll apply this gossip horizon for the remote peer.
remoteHorizon := &lnwire.GossipTimestampRange{
ChainHash: syncer.cfg.chainHash,
FirstTimestamp: unixStamp(25000),
TimestampRange: uint32(1000),
}
Expand Down Expand Up @@ -615,6 +617,7 @@ func TestGossipSyncerApplyGossipFilter(t *testing.T) {

// We'll apply this gossip horizon for the remote peer.
remoteHorizon := &lnwire.GossipTimestampRange{
ChainHash: syncer.cfg.chainHash,
FirstTimestamp: unixStamp(25000),
TimestampRange: uint32(1000),
}
Expand Down Expand Up @@ -844,9 +847,11 @@ func TestGossipSyncerReplyShortChanIDs(t *testing.T) {

queryReply := []lnwire.Message{
&lnwire.ChannelAnnouncement1{
ChainHash: syncer.cfg.chainHash,
ShortChannelID: lnwire.NewShortChanIDFromInt(20),
},
&lnwire.ChannelUpdate1{
ChainHash: syncer.cfg.chainHash,
ShortChannelID: lnwire.NewShortChanIDFromInt(20),
Timestamp: unixStamp(999999),
},
Expand Down Expand Up @@ -879,6 +884,7 @@ func TestGossipSyncerReplyShortChanIDs(t *testing.T) {
// With our set up above complete, we'll now attempt to obtain a reply
// from the channel syncer for our target chan ID query.
err := syncer.replyShortChanIDs(ctx, &lnwire.QueryShortChanIDs{
ChainHash: syncer.cfg.chainHash,
ShortChanIDs: queryChanIDs,
})
require.NoError(t, err, "unable to query for chan IDs")
Expand Down Expand Up @@ -951,6 +957,7 @@ func TestGossipSyncerReplyChanRangeQuery(t *testing.T) {
const numBlocks = 50
const endingBlockHeight = startingBlockHeight + numBlocks - 1
query := &lnwire.QueryChannelRange{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: uint32(startingBlockHeight),
NumBlocks: uint32(numBlocks),
}
Expand Down Expand Up @@ -1118,18 +1125,21 @@ func TestGossipSyncerReplyChanRangeQueryBlockRange(t *testing.T) {
queryReqs := []*lnwire.QueryChannelRange{
// full range example
{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: uint32(0),
NumBlocks: uint32(math.MaxUint32),
},

// small query example that does not overflow
{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: uint32(1000),
NumBlocks: uint32(100),
},

// overflow example
{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: uint32(1000),
NumBlocks: uint32(math.MaxUint32),
},
Expand Down Expand Up @@ -1231,6 +1241,7 @@ func TestGossipSyncerReplyChanRangeQueryNoNewChans(t *testing.T) {
// Next, we'll craft a query to ask for all the new chan ID's after
// block 100.
query := &lnwire.QueryChannelRange{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: 100,
NumBlocks: 50,
}
Expand Down Expand Up @@ -1387,6 +1398,7 @@ func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) {
// last block.
replies := []*lnwire.ReplyChannelRange{
{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: 0,
NumBlocks: 11,
ShortChanIDs: []lnwire.ShortChannelID{
Expand All @@ -1396,6 +1408,7 @@ func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) {
},
},
{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: 11,
NumBlocks: 1,
ShortChanIDs: []lnwire.ShortChannelID{
Expand All @@ -1405,6 +1418,7 @@ func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) {
},
},
{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: 12,
NumBlocks: 1,
ShortChanIDs: []lnwire.ShortChannelID{
Expand All @@ -1414,6 +1428,7 @@ func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) {
},
},
{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: 13,
NumBlocks: query.NumBlocks - 13,
Complete: 1,
Expand Down Expand Up @@ -1590,6 +1605,85 @@ func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) {
}
}

// TestGossipSyncerProcessMalformedChanRangeReply verifies that the
// GossipSyncer rejects a ReplyChannelRange message if its ChainHash does not
// match the configured chain.
func TestGossipSyncerProcessMalformedChanRangeReply(t *testing.T) {
t.Parallel()
ctx := t.Context()

_, syncer, _ := newTestSyncer(
lnwire.ShortChannelID{BlockHeight: latestKnownHeight},
defaultEncoding, defaultChunkSize,
)

reply := &lnwire.ReplyChannelRange{
ChainHash: *chaincfg.RegressionNetParams.
GenesisHash,
FirstBlockHeight: 0,
NumBlocks: 11,
Complete: 1,
ShortChanIDs: []lnwire.ShortChannelID{
{
BlockHeight: 10,
TxIndex: 1,
},
},
}

require.ErrorContains(
t, syncer.processChanRangeReply(ctx, reply),
"reply includes channels for chain",
)
}

// TestGossipSyncerProcessMalformedShortChanIDsEndReply verifies that the
// GossipSyncer rejects a ReplyShortChanIDsEnd message if its ChainHash does not
// match the configured chain.
func TestGossipSyncerProcessMalformedShortChanIDsEndReply(t *testing.T) {
t.Parallel()
ctx := t.Context()

_, syncer, _ := newTestSyncer(
lnwire.ShortChannelID{BlockHeight: latestKnownHeight},
defaultEncoding, defaultChunkSize,
)

reply := &lnwire.ReplyShortChanIDsEnd{
ChainHash: *chaincfg.RegressionNetParams.GenesisHash,
Complete: 1,
}

require.ErrorContains(
t, syncer.processScidEndReply(ctx, reply),
"reply includes channels for chain",
)
}

// TestGossipSyncerProcessMalformedGossipTimestampRange verifies that the
// GossipSyncer rejects a GossipTimestampRange message if its ChainHash does not
// match the configured chain.
func TestGossipSyncerProcessMalformedGossipTimestampRange(t *testing.T) {
t.Parallel()
ctx := t.Context()

_, syncer, _ := newTestSyncer(
lnwire.ShortChannelID{BlockHeight: latestKnownHeight},
defaultEncoding, defaultChunkSize,
)

gossip := &lnwire.GossipTimestampRange{
ChainHash: *chaincfg.RegressionNetParams.GenesisHash,
FirstTimestamp: uint32(time.Now().Unix()),
TimestampRange: math.MaxUint32,
}

require.ErrorContains(
t, syncer.ApplyGossipFilter(ctx, gossip),
"gossip filter specifies chain",
)
}

// TestGossipSyncerSynchronizeChanIDs tests that we properly request chunks of
// the short chan ID's which were unknown to us. We'll ensure that we request
// chunk by chunk, and after the last chunk, we return true indicating that we
Expand Down Expand Up @@ -2243,6 +2337,9 @@ func TestGossipSyncerSyncTransitions(t *testing.T) {
firstTimestamp := uint32(time.Now().Unix())
assertMsgSent(
t, mChan, &lnwire.GossipTimestampRange{
ChainHash: *chaincfg.
MainNetParams.
GenesisHash,
FirstTimestamp: firstTimestamp,
TimestampRange: math.MaxUint32,
},
Expand All @@ -2255,6 +2352,9 @@ func TestGossipSyncerSyncTransitions(t *testing.T) {
// updates.
assertMsgSent(
t, mChan, &lnwire.GossipTimestampRange{
ChainHash: *chaincfg.
MainNetParams.
GenesisHash,
FirstTimestamp: uint32(
zeroTimestamp.Unix(),
),
Expand Down Expand Up @@ -2283,6 +2383,8 @@ func TestGossipSyncerSyncTransitions(t *testing.T) {
// updates.
firstTimestamp := uint32(time.Now().Unix())
assertMsgSent(t, msgChan, &lnwire.GossipTimestampRange{
ChainHash: *chaincfg.MainNetParams.
GenesisHash,
FirstTimestamp: firstTimestamp,
TimestampRange: math.MaxUint32,
})
Expand Down Expand Up @@ -2363,6 +2465,7 @@ func TestGossipSyncerHistoricalSync(t *testing.T) {
// We should expect to see a single lnwire.QueryChannelRange message be
// sent to the remote peer with a FirstBlockHeight of 0.
expectedMsg := &lnwire.QueryChannelRange{
ChainHash: syncer.cfg.chainHash,
FirstBlockHeight: 0,
NumBlocks: latestKnownHeight,
QueryOptions: lnwire.NewTimestampQueryOption(),
Expand Down
5 changes: 5 additions & 0 deletions docs/release-notes/release-notes-0.21.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
transitions during startup, avoiding lost unlocks during slow database
initialization.

* [Fixed the gossiper accepting malformed gossip
messages](https://github.com/lightningnetwork/lnd/pull/10581) by enforcing chain
hash validation on `ReplyChannelRange`, `ReplyShortChanIDsEnd` and
`GossipTimestampRange`.

# New Features

- Basic Support for [onion messaging forwarding](https://github.com/lightningnetwork/lnd/pull/9868)
Expand Down
Loading