diff --git a/discovery/sync_manager.go b/discovery/sync_manager.go index de071e8fae..8db0301846 100644 --- a/discovery/sync_manager.go +++ b/discovery/sync_manager.go @@ -660,13 +660,11 @@ func (m *SyncManager) createGossipSyncer(peer lnpeer.Peer) *GossipSyncer { nodeID := route.Vertex(peer.PubKey()) log.Infof("Creating new GossipSyncer for peer=%x", nodeID[:]) - encoding := lnwire.EncodingSortedPlain s := newGossipSyncer(gossipSyncerCfg{ chainHash: m.cfg.ChainHash, peerPub: nodeID, channelSeries: m.cfg.ChanSeries, - encodingType: encoding, - chunkSize: encodingTypeToChunkSize[encoding], + chunkSize: defaultChunkSize, batchSize: requestBatchSize, sendMsg: func(ctx context.Context, sync bool, msgs ...lnwire.Message) error { diff --git a/discovery/syncer.go b/discovery/syncer.go index d1e5178ea8..bb8ad226b5 100644 --- a/discovery/syncer.go +++ b/discovery/syncer.go @@ -168,14 +168,9 @@ const ( // 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 - // maxChanRangeReplySCIDs is the maximum number of short channel IDs // we'll process for a single QueryChannelRange request. maxChanRangeReplySCIDs = 100_000 - // 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. @@ -189,18 +184,15 @@ const ( // remote peer for in a QueryShortChanIDs message. requestBatchSize = 500 + // defaultChunkSize is the max number of short chan IDs using plain + // encoding that we can fit into a single message safely. + defaultChunkSize = 8000 + // syncerBufferSize is the size of the syncer's buffers. syncerBufferSize = 50 ) var ( - // encodingTypeToChunkSize maps an encoding type, to the max number of - // short chan ID's using the encoding type that we can fit into a - // single message safely. - encodingTypeToChunkSize = map[lnwire.QueryEncoding]int32{ - lnwire.EncodingSortedPlain: 8000, - } - // ErrGossipSyncerExiting signals that the syncer has been killed. ErrGossipSyncerExiting = errors.New("gossip syncer exiting") @@ -242,12 +234,8 @@ type gossipSyncerCfg struct { // our queries and respond to the queries of the remote peer. channelSeries ChannelGraphTimeSeries - // encodingType is the current encoding type we're aware of. Requests - // with different encoding types will be rejected. - encodingType lnwire.QueryEncoding - - // chunkSize is the max number of short chan IDs using the syncer's - // encoding type that we can fit into a single message safely. + // chunkSize is the max number of short chan IDs that we can fit into a + // single message safely. chunkSize int32 // batchSize is the max number of channels the syncer will query from @@ -1024,9 +1012,6 @@ func (g *GossipSyncer) bufferChanRangeReply(_ context.Context, case lnwire.EncodingSortedPlain: replyCount = 1 - case lnwire.EncodingSortedZlib: - replyCount = maxQueryChanRangeRepliesZlibFactor - default: return fmt.Errorf( "unhandled encoding type %v", msg.EncodingType, @@ -1099,6 +1084,7 @@ func (g *GossipSyncer) bufferChanRangeReply(_ context.Context, ) } + g.numChanRangeRepliesRcvd++ log.Infof("GossipSyncer(%x): buffering chan range reply of size=%v", g.cfg.peerPub[:], len(msg.ShortChanIDs)) @@ -1288,7 +1274,7 @@ func (g *GossipSyncer) replyChanRangeQuery(ctx context.Context, FirstBlockHeight: query.FirstBlockHeight, NumBlocks: query.NumBlocks, Complete: 0, - EncodingType: g.cfg.encodingType, + EncodingType: lnwire.EncodingSortedPlain, ShortChanIDs: nil, }) } @@ -1361,7 +1347,7 @@ func (g *GossipSyncer) replyChanRangeQuery(ctx context.Context, NumBlocks: numBlocks, FirstBlockHeight: firstHeight, Complete: complete, - EncodingType: g.cfg.encodingType, + EncodingType: lnwire.EncodingSortedPlain, ShortChanIDs: scids, Timestamps: timestamps, }) diff --git a/discovery/syncer_atomic_test.go b/discovery/syncer_atomic_test.go index ea1a608863..c0bface4a4 100644 --- a/discovery/syncer_atomic_test.go +++ b/discovery/syncer_atomic_test.go @@ -56,7 +56,7 @@ func TestGossipSyncerSingleBacklogSend(t *testing.T) { // Now we'll kick off the test by making a syncer that uses our blocking // send function. msgChan, syncer, chanSeries := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, true, true, true, ) diff --git a/discovery/syncer_queue_test.go b/discovery/syncer_queue_test.go index 5dee661c49..65668690cd 100644 --- a/discovery/syncer_queue_test.go +++ b/discovery/syncer_queue_test.go @@ -29,7 +29,7 @@ func TestGossipSyncerQueueTimestampRange(t *testing.T) { // Enable timestamp queries (third flag set to true). msgChan, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, true, true, true, ) @@ -68,7 +68,7 @@ func TestGossipSyncerQueueTimestampRangeFull(t *testing.T) { // processed. Enable timestamp queries. _, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, true, true, true, ) @@ -104,7 +104,7 @@ func TestGossipSyncerQueueTimestampRangeConcurrent(t *testing.T) { // Create and start a test syncer. Enable timestamp queries. msgChan, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, true, true, true, ) syncer.Start() @@ -183,7 +183,7 @@ func TestGossipSyncerQueueShutdown(t *testing.T) { // Create and start a test syncer. Enable timestamp queries. _, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, true, true, true, ) syncer.Start() @@ -256,7 +256,7 @@ func TestGossipSyncerQueueInvariants(t *testing.T) { // Create a test syncer. Enable timestamp queries. msgChan, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, true, true, true, ) @@ -366,7 +366,7 @@ func TestGossipSyncerQueueOrder(t *testing.T) { // Enable timestamp queries. msgChan, syncer, chanSeries := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, true, true, true, ) diff --git a/discovery/syncer_test.go b/discovery/syncer_test.go index da48e61706..fd41238426 100644 --- a/discovery/syncer_test.go +++ b/discovery/syncer_test.go @@ -22,14 +22,9 @@ import ( ) const ( - defaultEncoding = lnwire.EncodingSortedPlain latestKnownHeight = 1337 ) -var ( - defaultChunkSize = encodingTypeToChunkSize[defaultEncoding] -) - type horizonQuery struct { start time.Time end time.Time @@ -182,8 +177,7 @@ var _ ChannelGraphTimeSeries = (*mockChannelGraphTimeSeries)(nil) // ignored. If no flags are provided, both a channelGraphSyncer and replyHandler // will be spawned by default. func newTestSyncer(hID lnwire.ShortChannelID, - encodingType lnwire.QueryEncoding, chunkSize int32, - flags ...bool) (chan []lnwire.Message, + chunkSize int32, flags ...bool) (chan []lnwire.Message, *GossipSyncer, *mockChannelGraphTimeSeries) { var ( @@ -204,7 +198,6 @@ func newTestSyncer(hID lnwire.ShortChannelID, msgChan := make(chan []lnwire.Message, 20) cfg := gossipSyncerCfg{ channelSeries: newMockChannelGraphTimeSeries(hID), - encodingType: encodingType, chunkSize: chunkSize, batchSize: chunkSize, noSyncChannels: !syncChannels, @@ -276,7 +269,6 @@ func newErrorInjectingSyncer(hID lnwire.ShortChannelID, chunkSize int32) ( cfg := gossipSyncerCfg{ channelSeries: newMockChannelGraphTimeSeries(hID), - encodingType: defaultEncoding, chunkSize: chunkSize, batchSize: chunkSize, noSyncChannels: false, @@ -343,7 +335,7 @@ func TestGossipSyncerFilterGossipMsgsNoHorizon(t *testing.T) { // First, we'll create a GossipSyncer instance with a canned sendToPeer // message to allow us to intercept their potential sends. msgChan, syncer, _ := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) @@ -393,7 +385,7 @@ func TestGossipSyncerFilterGossipMsgsAllInMemory(t *testing.T) { // First, we'll create a GossipSyncer instance with a canned sendToPeer // message to allow us to intercept their potential sends. msgChan, syncer, chanSeries := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) @@ -547,7 +539,7 @@ func TestGossipSyncerApplyNoHistoricalGossipFilter(t *testing.T) { // First, we'll create a GossipSyncer instance with a canned sendToPeer // message to allow us to intercept their potential sends. _, syncer, chanSeries := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) syncer.cfg.ignoreHistoricalFilters = true @@ -608,7 +600,7 @@ func TestGossipSyncerApplyGossipFilter(t *testing.T) { // First, we'll create a GossipSyncer instance with a canned sendToPeer // message to allow us to intercept their potential sends. msgChan, syncer, chanSeries := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) @@ -728,7 +720,7 @@ func TestGossipSyncerQueryChannelRangeWrongChainHash(t *testing.T) { // First, we'll create a GossipSyncer instance with a canned sendToPeer // message to allow us to intercept their potential sends. msgChan, syncer, _ := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) @@ -781,7 +773,7 @@ func TestGossipSyncerReplyShortChanIDsWrongChainHash(t *testing.T) { // First, we'll create a GossipSyncer instance with a canned sendToPeer // message to allow us to intercept their potential sends. msgChan, syncer, _ := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) @@ -831,7 +823,7 @@ func TestGossipSyncerReplyShortChanIDs(t *testing.T) { // First, we'll create a GossipSyncer instance with a canned sendToPeer // message to allow us to intercept their potential sends. msgChan, syncer, chanSeries := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) @@ -941,7 +933,7 @@ func TestGossipSyncerReplyChanRangeQuery(t *testing.T) { // We'll now create our test gossip syncer that will shortly respond to // our canned query. msgChan, syncer, chanSeries := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, chunkSize, + lnwire.NewShortChanIDFromInt(10), chunkSize, ) // Next, we'll craft a query to ask for all the new chan ID's after @@ -1109,7 +1101,7 @@ func TestGossipSyncerReplyChanRangeQueryBlockRange(t *testing.T) { // First create our test gossip syncer that will handle and // respond to the test queries _, syncer, chanSeries := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, math.MaxInt32, + lnwire.NewShortChanIDFromInt(10), math.MaxInt32, ) // Next construct test queries with various startBlock and endBlock @@ -1223,7 +1215,7 @@ func TestGossipSyncerReplyChanRangeQueryNoNewChans(t *testing.T) { // We'll now create our test gossip syncer that will shortly respond to // our canned query. msgChan, syncer, chanSeries := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) @@ -1305,7 +1297,7 @@ func TestGossipSyncerGenChanRangeQuery(t *testing.T) { const startingHeight = 200 _, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: startingHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, ) // If we now ask the syncer to generate an initial range query, it @@ -1367,7 +1359,7 @@ func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) { BlockHeight: latestKnownHeight, } _, syncer, chanSeries := newTestSyncer( - highestID, defaultEncoding, defaultChunkSize, + highestID, defaultChunkSize, ) startingState := syncer.state @@ -1604,7 +1596,7 @@ func TestGossipSyncerSynchronizeChanIDs(t *testing.T) { // First, we'll create a GossipSyncer instance with a canned sendToPeer // message to allow us to intercept their potential sends. msgChan, syncer, _ := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, chunkSize, + lnwire.NewShortChanIDFromInt(10), chunkSize, ) // Next, we'll construct a set of chan ID's that we should query for, @@ -1842,13 +1834,13 @@ func TestGossipSyncerRoutineSync(t *testing.T) { BlockHeight: 1144, } msgChan1, syncer1, chanSeries1 := newTestSyncer( - highestID, defaultEncoding, chunkSize, true, false, + highestID, chunkSize, true, false, ) syncer1.Start() defer syncer1.Stop() msgChan2, syncer2, chanSeries2 := newTestSyncer( - highestID, defaultEncoding, chunkSize, false, true, + highestID, chunkSize, false, true, ) syncer2.Start() defer syncer2.Stop() @@ -1990,13 +1982,13 @@ func TestGossipSyncerAlreadySynced(t *testing.T) { BlockHeight: 1144, } msgChan1, syncer1, chanSeries1 := newTestSyncer( - highestID, defaultEncoding, chunkSize, + highestID, chunkSize, ) syncer1.Start() defer syncer1.Stop() msgChan2, syncer2, chanSeries2 := newTestSyncer( - highestID, defaultEncoding, chunkSize, + highestID, chunkSize, ) syncer2.Start() defer syncer2.Stop() @@ -2307,7 +2299,7 @@ func TestGossipSyncerSyncTransitions(t *testing.T) { lnwire.ShortChannelID{ BlockHeight: latestKnownHeight, }, - defaultEncoding, defaultChunkSize, + defaultChunkSize, ) syncer.setSyncState(chansSynced) @@ -2356,7 +2348,7 @@ func TestProcessSyncTransitionShutdown(t *testing.T) { // the syncer's quit channel. _, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, ) syncer.setSyncState(chansSynced) syncer.setSyncType(PassiveSync) @@ -2405,7 +2397,7 @@ func TestGossipSyncerHistoricalSync(t *testing.T) { // historical sync requests in this state. msgChan, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, true, true, true, + defaultChunkSize, true, true, true, ) syncer.setSyncType(PassiveSync) syncer.setSyncState(chansSynced) @@ -2447,7 +2439,7 @@ func TestGossipSyncerSyncedSignal(t *testing.T) { // We'll create a new gossip syncer and manually override its state to // chansSynced. _, syncer, _ := newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) syncer.setSyncState(chansSynced) @@ -2470,7 +2462,7 @@ func TestGossipSyncerSyncedSignal(t *testing.T) { // We'll try this again, but this time we'll request the signal after // the syncer is active and has already reached its chansSynced state. _, syncer, _ = newTestSyncer( - lnwire.NewShortChanIDFromInt(10), defaultEncoding, + lnwire.NewShortChanIDFromInt(10), defaultChunkSize, ) @@ -2498,7 +2490,7 @@ func TestGossipSyncerMaxChannelRangeReplies(t *testing.T) { msgChan, syncer, chanSeries := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, ) // We'll tune the maxQueryChanRangeReplies to a more sensible value for @@ -2579,7 +2571,7 @@ func TestGossipSyncerMaxChannelRangeSCIDs(t *testing.T) { _, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, ) query, err := syncer.genChanRangeQuery(ctx, true) @@ -2638,7 +2630,7 @@ func TestGossipSyncerChanRangeReplyNoQuery(t *testing.T) { _, syncer, _ := newTestSyncer( lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, + defaultChunkSize, ) // Note that we deliberately skip genChanRangeQuery here, so @@ -2656,34 +2648,6 @@ func TestGossipSyncerChanRangeReplyNoQuery(t *testing.T) { require.ErrorContains(t, err, "without an active query") } -// TestGossipSyncerCountsReceivedEncoding ensures that compressed range -// replies consume the larger reply budget even when the local syncer uses -// plain encoding. -func TestGossipSyncerCountsReceivedEncoding(t *testing.T) { - t.Parallel() - ctx := t.Context() - - _, syncer, _ := newTestSyncer( - lnwire.ShortChannelID{BlockHeight: latestKnownHeight}, - defaultEncoding, defaultChunkSize, - ) - - query, err := syncer.genChanRangeQuery(ctx, true) - require.NoError(t, err) - - reply := &lnwire.ReplyChannelRange{ - ChainHash: query.ChainHash, - FirstBlockHeight: query.FirstBlockHeight, - NumBlocks: query.NumBlocks, - EncodingType: lnwire.EncodingSortedZlib, - } - require.NoError(t, syncer.processChanRangeReply(ctx, reply)) - require.Equal( - t, uint32(maxQueryChanRangeRepliesZlibFactor), - syncer.numChanRangeRepliesRcvd, - ) -} - // deliverOverBudgetRangeReply waits for the syncer to send its initial range // query, then answers it with a single reply that overruns the aggregate SCID // budget. Sending the query is what populates curQueryRangeMsg and moves the diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md index 750a20069c..e15f1ab45b 100644 --- a/docs/release-notes/release-notes-0.22.0.md +++ b/docs/release-notes/release-notes-0.22.0.md @@ -98,6 +98,13 @@ ## Deprecations +* [Support for decoding `EncodingSortedZlib` (type 1) gossip queries has been + removed](https://github.com/lightningnetwork/lnd/pull/10980) as it + was dropped from the BOLT 7 specification. `lnd` never produced this encoding, + so in practice only the receive path changes. If a legacy peer attempts to + send a zlib-encoded `QueryShortChanIDs` or `ReplyChannelRange` message, it + will now trigger a fatal wire-decode error and instantly disconnect the peer. + # Technical and Architectural Updates ## BOLT Spec Updates @@ -159,3 +166,4 @@ * Boris Nagaev * Erick Cestari * Jared Tobin +* TechLateef diff --git a/lnwire/encoding.go b/lnwire/encoding.go index 72000f81d1..105b4b6778 100644 --- a/lnwire/encoding.go +++ b/lnwire/encoding.go @@ -11,10 +11,12 @@ const ( // regular encoding, in a sorted order. EncodingSortedPlain QueryEncoding = 0 - // EncodingSortedZlib signals that the set of data is encoded by first - // sorting the set of channel ID's, as then compressing them using zlib. + // EncodingSortedZlib signals that the set of data was encoded using + // zlib compression. This encoding was dropped from the BOLT 7 spec + // and is no longer supported. The constant is retained to reject the + // encoding in both directions (encoding and decoding). // - // NOTE: this should no longer be used or accepted. + // Deprecated: zlib encoding is not accepted or produced by lnd. EncodingSortedZlib QueryEncoding = 1 ) diff --git a/lnwire/fuzz_test.go b/lnwire/fuzz_test.go index 6bbc86710c..6e87fce6a8 100644 --- a/lnwire/fuzz_test.go +++ b/lnwire/fuzz_test.go @@ -2,7 +2,6 @@ package lnwire import ( "bytes" - "compress/zlib" "encoding/binary" "testing" @@ -312,76 +311,12 @@ func FuzzQueryChannelRange(f *testing.F) { }) } -func FuzzZlibQueryShortChanIDs(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - var buf bytes.Buffer - zlibWriter := zlib.NewWriter(&buf) - _, err := zlibWriter.Write(data) - require.NoError(t, err) // Zlib bug? - - err = zlibWriter.Close() - require.NoError(t, err) // Zlib bug? - - compressedPayload := buf.Bytes() - - chainhash := []byte("00000000000000000000000000000000") - numBytesInBody := len(compressedPayload) + 1 - zlibByte := []byte("\x01") - - bodyBytes := make([]byte, 2) - binary.BigEndian.PutUint16(bodyBytes, uint16(numBytesInBody)) - - payload := chainhash - payload = append(payload, bodyBytes...) - payload = append(payload, zlibByte...) - payload = append(payload, compressedPayload...) - - wireMsgHarness(t, payload, MsgQueryShortChanIDs) - }) -} - func FuzzQueryShortChanIDs(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { wireMsgHarness(t, data, MsgQueryShortChanIDs) }) } -func FuzzZlibReplyChannelRange(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - var buf bytes.Buffer - zlibWriter := zlib.NewWriter(&buf) - _, err := zlibWriter.Write(data) - require.NoError(t, err) // Zlib bug? - - err = zlibWriter.Close() - require.NoError(t, err) // Zlib bug? - - compressedPayload := buf.Bytes() - - // Initialize some []byte vars which will prefix our payload - chainhash := []byte("00000000000000000000000000000000") - firstBlockHeight := []byte("\x00\x00\x00\x00") - numBlocks := []byte("\x00\x00\x00\x00") - completeByte := []byte("\x00") - - numBytesInBody := len(compressedPayload) + 1 - zlibByte := []byte("\x01") - - bodyBytes := make([]byte, 2) - binary.BigEndian.PutUint16(bodyBytes, uint16(numBytesInBody)) - - payload := chainhash - payload = append(payload, firstBlockHeight...) - payload = append(payload, numBlocks...) - payload = append(payload, completeByte...) - payload = append(payload, bodyBytes...) - payload = append(payload, zlibByte...) - payload = append(payload, compressedPayload...) - - wireMsgHarness(t, payload, MsgReplyChannelRange) - }) -} - func FuzzReplyChannelRange(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { // We can't use require.Equal for Timestamps, since we consider diff --git a/lnwire/message_test.go b/lnwire/message_test.go index 11b3b1fc4a..7e854f98e4 100644 --- a/lnwire/message_test.go +++ b/lnwire/message_test.go @@ -289,8 +289,6 @@ func makeAllMessages(t testing.TB, r *rand.Rand) []lnwire.Message { msgAll = append(msgAll, newMsgQueryChannelRange(t, r)) msgAll = append(msgAll, newMsgReplyChannelRange(t, r)) msgAll = append(msgAll, newMsgGossipTimestampRange(t, r)) - msgAll = append(msgAll, newMsgQueryShortChanIDsZlib(t, r)) - msgAll = append(msgAll, newMsgReplyChannelRangeZlib(t, r)) msgAll = append(msgAll, newMsgOnionMessage(t, r)) return msgAll @@ -769,27 +767,6 @@ func newMsgQueryShortChanIDs(t testing.TB, return msg } -func newMsgQueryShortChanIDsZlib(t testing.TB, - r *rand.Rand) *lnwire.QueryShortChanIDs { - - t.Helper() - - msg := &lnwire.QueryShortChanIDs{ - EncodingType: lnwire.EncodingSortedZlib, - ExtraData: createExtraData(t, r), - } - - _, err := rand.Read(msg.ChainHash[:]) - require.NoError(t, err, "unable to read chain hash") - - for i := 0; i < testNumChanIDs; i++ { - msg.ShortChanIDs = append(msg.ShortChanIDs, - lnwire.NewShortChanIDFromInt(uint64(r.Int63()))) - } - - return msg -} - func newMsgReplyShortChanIDsEnd(t testing.TB, r *rand.Rand) *lnwire.ReplyShortChanIDsEnd { @@ -846,29 +823,6 @@ func newMsgReplyChannelRange(t testing.TB, return msg } -func newMsgReplyChannelRangeZlib(t testing.TB, - r *rand.Rand) *lnwire.ReplyChannelRange { - - t.Helper() - - msg := &lnwire.ReplyChannelRange{ - EncodingType: lnwire.EncodingSortedZlib, - ExtraData: createExtraData(t, r), - } - - _, err := rand.Read(msg.ChainHash[:]) - require.NoError(t, err, "unable to read chain hash") - - msg.Complete = uint8(r.Int31n(2)) - - for i := 0; i < testNumChanIDs; i++ { - msg.ShortChanIDs = append(msg.ShortChanIDs, - lnwire.NewShortChanIDFromInt(uint64(r.Int63()))) - } - - return msg -} - func newMsgGossipTimestampRange(t testing.TB, r *rand.Rand) *lnwire.GossipTimestampRange { diff --git a/lnwire/query_short_chan_ids.go b/lnwire/query_short_chan_ids.go index 3e98a2b15a..a34fd3ccc2 100644 --- a/lnwire/query_short_chan_ids.go +++ b/lnwire/query_short_chan_ids.go @@ -2,12 +2,9 @@ package lnwire import ( "bytes" - "compress/zlib" - "errors" "fmt" "io" "sort" - "sync" "github.com/btcsuite/btcd/chainhash/v2" ) @@ -32,11 +29,6 @@ func (e ErrUnsortedSIDs) Error() string { e.curSID, e.prevSID) } -// zlibDecodeMtx is a package level mutex that we'll use in order to ensure -// that we'll only attempt a single zlib decoding instance at a time. This -// allows us to also further bound our memory usage. -var zlibDecodeMtx sync.Mutex - // ErrUnknownShortChanIDEncoding is a parametrized error that indicates that we // came across an unknown short channel ID encoding, and therefore were unable // to continue parsing. @@ -44,6 +36,12 @@ func ErrUnknownShortChanIDEncoding(encoding QueryEncoding) error { return fmt.Errorf("unknown short chan id encoding: %v", encoding) } +// ErrZlibNotSupported indicates that the deprecated zlib encoding was +// encountered during the encoding or decoding of a short channel ID query +// or response. +var ErrZlibNotSupported = fmt.Errorf("zlib encoding (type %d) is no "+ + "longer supported", EncodingSortedZlib) + // QueryShortChanIDs is a message that allows the sender to query a set of // channel announcement and channel update messages that correspond to the set // of encoded short channel ID's. The encoding of the short channel ID's is @@ -200,46 +198,8 @@ func decodeShortChanIDs(r io.Reader) (QueryEncoding, []ShortChannelID, error) { return encodingType, shortChanIDs, nil - // In this encoding, we'll use zlib to decode the compressed payload. - // However, we'll pay attention to ensure that we don't open our selves - // up to a memory exhaustion attack. case EncodingSortedZlib: - // We'll obtain an ultimately release the zlib decode mutex. - // This guards us against allocating too much memory to decode - // each instance from concurrent peers. - zlibDecodeMtx.Lock() - defer zlibDecodeMtx.Unlock() - - // At this point, if there's no body remaining, then only the encoding - // type was specified, meaning that there're no further bytes to be - // parsed. - if len(queryBody) == 0 { - return encodingType, nil, nil - } - - decompressor, err := zlib.NewReader(bytes.NewReader(queryBody)) - if err != nil { - return 0, nil, fmt.Errorf("unable to create zlib "+ - "reader: %w", err) - } - - shortChanIDs, decodeErr := decodeCompressedShortChanIDs( - decompressor, - ) - closeErr := decompressor.Close() - - switch { - case decodeErr != nil: - return 0, nil, decodeErr - - case closeErr != nil: - return 0, nil, fmt.Errorf( - "unable to close zlib reader: %w", closeErr, - ) - - default: - return encodingType, shortChanIDs, nil - } + return 0, nil, ErrZlibNotSupported default: // If we've been sent an encoding type that we don't know of, @@ -249,48 +209,8 @@ func decodeShortChanIDs(r io.Reader) (QueryEncoding, []ShortChannelID, error) { } } -// decodeCompressedShortChanIDs decodes and validates the decompressed short -// channel ID stream. -func decodeCompressedShortChanIDs(r io.Reader) ([]ShortChannelID, error) { - var ( - shortChanIDs []ShortChannelID - lastChanID ShortChannelID - ) - - for { - var cid ShortChannelID - err := ReadElements(r, &cid) - - switch { - // Only a clean EOF terminates the stream. A partial final ID - // returns io.ErrUnexpectedEOF and remains an error. - case errors.Is(err, io.EOF): - return shortChanIDs, nil - - case err != nil: - return nil, fmt.Errorf("unable to deflate next short "+ - "chan ID: %w", err) - } - - if len(shortChanIDs) == maxDecodedShortChanIDs { - return nil, fmt.Errorf("too many short channel IDs: "+ - "max=%v", maxDecodedShortChanIDs) - } - - if len(shortChanIDs) > 0 && - cid.ToUint64() <= lastChanID.ToUint64() { - - return nil, ErrUnsortedSIDs{lastChanID, cid} - } - - shortChanIDs = append(shortChanIDs, cid) - lastChanID = cid - } -} - // Encode serializes the target QueryShortChanIDs into the passed io.Writer // observing the protocol version specified. -// // This is part of the lnwire.Message interface. func (q *QueryShortChanIDs) Encode(w *bytes.Buffer, pver uint32) error { // First, we'll write out the chain hash. @@ -298,9 +218,9 @@ func (q *QueryShortChanIDs) Encode(w *bytes.Buffer, pver uint32) error { return err } - // For both of the current encoding types, the channel ID's are to be - // sorted in place, so we'll do that now. The sorting is applied unless - // we were specifically requested not to for testing purposes. + // The channel ID's are to be sorted in place, so we'll do that now. + // The sorting is applied unless we were specifically requested not + // to for testing purposes. if !q.noSort { sort.Slice(q.ShortChanIDs, func(i, j int) bool { return q.ShortChanIDs[i].ToUint64() < @@ -353,81 +273,11 @@ func encodeShortChanIDs(w *bytes.Buffer, encodingType QueryEncoding, } return nil - - // For this encoding we'll first write out a serialized version of all - // the channel ID's into a buffer, then zlib encode that. The final - // payload is what we'll write out to the passed io.Writer. - // - // TODO(roasbeef): assumes the caller knows the proper chunk size to - // pass to avoid bin-packing here case EncodingSortedZlib: - // If we don't have anything at all to write, then we'll write - // an empty payload so we don't include things like the zlib - // header when the remote party is expecting no actual short - // channel IDs. - var compressedPayload []byte - if len(shortChanIDs) > 0 { - // We'll make a new write buffer to hold the bytes of - // shortChanIDs. - var wb bytes.Buffer - - // Next, we'll write out all the channel ID's directly - // into the zlib writer, which will do compressing on - // the fly. - for _, chanID := range shortChanIDs { - err := WriteShortChannelID(&wb, chanID) - if err != nil { - return fmt.Errorf( - "unable to write short chan "+ - "ID: %v", err, - ) - } - } - - // With shortChanIDs written into wb, we'll create a - // zlib writer and write all the compressed bytes. - var zlibBuffer bytes.Buffer - zlibWriter := zlib.NewWriter(&zlibBuffer) - - if _, err := zlibWriter.Write(wb.Bytes()); err != nil { - return fmt.Errorf( - "unable to write compressed short chan"+ - "ID: %w", err) - } - - // Now that we've written all the elements, we'll - // ensure the compressed stream is written to the - // underlying buffer. - if err := zlibWriter.Close(); err != nil { - return fmt.Errorf("unable to finalize "+ - "compression: %v", err) - } - - compressedPayload = zlibBuffer.Bytes() - } - - // Now that we have all the items compressed, we can compute - // what the total payload size will be. We add one to account - // for the byte to encode the type. - // - // If we don't have any actual bytes to write, then we'll end - // up emitting one byte for the length, followed by the - // encoding type, and nothing more. The spec isn't 100% clear - // in this area, but we do this as this is what most of the - // other implementations do. - numBytesBody := len(compressedPayload) + 1 - - // Finally, we can write out the number of bytes, the - // compression type, and finally the buffer itself. - if err := WriteUint16(w, uint16(numBytesBody)); err != nil { - return err - } - err := WriteQueryEncoding(w, encodingType) - if err != nil { - return err - } - - return WriteBytes(w, compressedPayload) + // Zlib encoding was removed from the BOLT 7 spec. Nothing in + // lnd ever sets this encoding type, so reaching this case + // would mean a programming error on our side. + return ErrZlibNotSupported default: // If we're trying to encode with an encoding type that we diff --git a/lnwire/query_short_chan_ids_test.go b/lnwire/query_short_chan_ids_test.go index 30ecbbe069..50a0fe1aeb 100644 --- a/lnwire/query_short_chan_ids_test.go +++ b/lnwire/query_short_chan_ids_test.go @@ -2,6 +2,7 @@ package lnwire import ( "bytes" + "encoding/hex" "testing" "github.com/stretchr/testify/require" @@ -36,16 +37,6 @@ var ( encType: EncodingSortedPlain, sids: duplicateSids, }, - { - name: "zlib unsorted", - encType: EncodingSortedZlib, - sids: unsortedSids, - }, - { - name: "zlib duplicate", - encType: EncodingSortedZlib, - sids: duplicateSids, - }, } ) @@ -86,9 +77,6 @@ func TestQueryShortChanIDsZero(t *testing.T) { { name: "plain", encoding: EncodingSortedPlain, - }, { - name: "zlib", - encoding: EncodingSortedZlib, }, } @@ -120,17 +108,12 @@ func TestQueryShortChanIDsZero(t *testing.T) { } } -// TestQueryShortChanIDsRoundTrip uses property-based testing to ensure both -// supported encodings preserve sorted short channel ID sets. +// TestQueryShortChanIDsRoundTrip uses property-based testing to ensure plain +// encoding preserves sorted short channel ID sets. func TestQueryShortChanIDsRoundTrip(t *testing.T) { t.Parallel() rapid.Check(t, func(t *rapid.T) { - encoding := rapid.SampledFrom([]QueryEncoding{ - EncodingSortedPlain, - EncodingSortedZlib, - }).Draw(t, "encoding") - numSCIDs := rapid.IntRange(0, 512).Draw(t, "num-scids") var scids []ShortChannelID if numSCIDs > 0 { @@ -146,181 +129,45 @@ func TestQueryShortChanIDsRoundTrip(t *testing.T) { var b bytes.Buffer require.NoError(t, encodeShortChanIDs( - &b, encoding, scids, + &b, EncodingSortedPlain, scids, )) decodedEncoding, decoded, err := decodeShortChanIDs( bytes.NewReader(b.Bytes()), ) require.NoError(t, err) - require.Equal(t, encoding, decodedEncoding) + require.Equal(t, EncodingSortedPlain, decodedEncoding) require.Equal(t, scids, decoded) }) } -// TestQueryShortChanIDsDecodeLimit ensures that a decompressed short channel -// ID stream cannot exceed its resource limit. -func TestQueryShortChanIDsDecodeLimit(t *testing.T) { +// TestEncodeShortChanIDsZlibRejection tests that attempting to encode using +// the deprecated zlib encoding format returns an ErrZlibNotSupported failure. +func TestEncodeShortChanIDsZlibRejection(t *testing.T) { t.Parallel() + var b bytes.Buffer - var stream bytes.Buffer - for i := 0; i <= maxDecodedShortChanIDs; i++ { - require.NoError(t, WriteElements( - &stream, NewShortChanIDFromInt(uint64(i)), - )) - } + err := encodeShortChanIDs(&b, EncodingSortedZlib, nil) - decoded, err := decodeCompressedShortChanIDs(bytes.NewReader( - stream.Bytes()[:maxDecodedShortChanIDs*8], - )) - require.NoError(t, err) - require.Len(t, decoded, maxDecodedShortChanIDs) - - _, err = decodeCompressedShortChanIDs( - bytes.NewReader(stream.Bytes()), - ) - require.ErrorContains(t, err, "too many short channel IDs") + require.ErrorIs(t, err, ErrZlibNotSupported) } -// TestQueryShortChanIDsZlibCompatibility ensures that a protocol-valid -// compressed reply can contain far more short channel IDs than a plain reply. -// The plain encoding is bounded by the wire size at maxPlainReplySCIDs, so it -// is the compressed encoding that determines how much headroom a single reply -// actually has. -func TestQueryShortChanIDsZlibCompatibility(t *testing.T) { +// TestDecodeShortChanIDsZlibRejection tests that decoding a query that uses +// the deprecated zlib encoding returns ErrZlibNotSupported. +func TestDecodeShortChanIDsZlibRejection(t *testing.T) { t.Parallel() - const ( - // maxWireMsgSize is the largest a message may be on the wire, - // including its type prefix. - maxWireMsgSize = MaxMsgBody + MessageTypeSize - - // maxPlainReplySCIDs is the number of SCIDs that saturate a - // ReplyChannelRange under the plain encoding. The message - // carries 41 bytes of fixed fields, and the SCID blob adds a - // 2-byte length prefix plus a 1-byte encoding type, leaving - // (65533 - 44) / 8 SCIDs. - maxPlainReplySCIDs = 8186 - - // maxZlibReplySCIDs is the number of consecutive SCIDs that - // saturate the same message under the zlib encoding. Runs of - // consecutive SCIDs are the best case for the compressor, so - // this is an upper bound rather than a figure real peers hit. - maxZlibReplySCIDs = 30_794 + var buf bytes.Buffer + buf.Write(make([]byte, 32)) + buf.Write([]byte{0x00, 0x16}) + buf.WriteByte(byte(EncodingSortedZlib)) + payload, err := hex.DecodeString( + "789c636000833e08659309a65c971d0100126e02e3", ) - - // A reply full of consecutive SCIDs is what we'll size both encodings - // against. - newReply := func(enc QueryEncoding, n int) *ReplyChannelRange { - scids := make([]ShortChannelID, n) - for i := range scids { - scids[i] = NewShortChanIDFromInt(uint64(i)) - } - - return &ReplyChannelRange{ - Complete: 1, - EncodingType: enc, - ShortChanIDs: scids, - ExtraData: make([]byte, 0), - } - } - - // The plain encoding tops out at maxPlainReplySCIDs: that many SCIDs - // fit, and one more overflows the message. - plain := newReply(EncodingSortedPlain, maxPlainReplySCIDs) - size, err := plain.SerializedSize() - require.NoError(t, err) - require.LessOrEqual(t, size, uint32(maxWireMsgSize)) - - plain = newReply(EncodingSortedPlain, maxPlainReplySCIDs+1) - size, err = plain.SerializedSize() - require.NoError(t, err) - require.Greater(t, size, uint32(maxWireMsgSize)) - - // The zlib encoding fits far more SCIDs into the very same message, - // which is the compatibility property we care about: a compressed - // reply can carry a much larger slice of the graph than a plain one. - zlib := newReply(EncodingSortedZlib, maxZlibReplySCIDs) - size, err = zlib.SerializedSize() - require.NoError(t, err) - require.LessOrEqual(t, size, uint32(maxWireMsgSize)) - require.Greater(t, maxZlibReplySCIDs, maxPlainReplySCIDs) - - // One more SCID pushes the compressed reply over the wire limit, so - // maxZlibReplySCIDs really is the ceiling. - over := newReply(EncodingSortedZlib, maxZlibReplySCIDs+1) - size, err = over.SerializedSize() require.NoError(t, err) - require.Greater(t, size, uint32(maxWireMsgSize)) + buf.Write(payload) - // Finally, the saturated compressed reply must still round trip - // cleanly through the decoder. - var b bytes.Buffer - require.NoError(t, encodeShortChanIDs( - &b, EncodingSortedZlib, zlib.ShortChanIDs, - )) - - encoding, decoded, err := decodeShortChanIDs( - bytes.NewReader(b.Bytes()), - ) - require.NoError(t, err) - require.Equal(t, EncodingSortedZlib, encoding) - require.Equal(t, zlib.ShortChanIDs, decoded) -} - -// TestQueryShortChanIDsRejectsCorruptZlib ensures that truncated or corrupt -// compressed streams are not accepted as valid partial replies. -func TestQueryShortChanIDsRejectsCorruptZlib(t *testing.T) { - t.Parallel() - - scids := []ShortChannelID{ - NewShortChanIDFromInt(1), - NewShortChanIDFromInt(2), - NewShortChanIDFromInt(3), - } - - var encoded bytes.Buffer - require.NoError(t, encodeShortChanIDs( - &encoded, EncodingSortedZlib, scids, - )) - - body := encoded.Bytes()[2:] - corruptChecksum := append([]byte(nil), body...) - corruptChecksum[len(corruptChecksum)-1] ^= 1 - - tests := []struct { - name string - body []byte - }{ - { - name: "truncated header", - body: body[:2], - }, - { - name: "truncated checksum", - body: body[:len(body)-1], - }, - { - name: "corrupt checksum", - body: corruptChecksum, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - var message bytes.Buffer - require.NoError(t, WriteElements( - &message, uint16(len(test.body)), - )) - _, err := message.Write(test.body) - require.NoError(t, err) - - _, _, err = decodeShortChanIDs( - bytes.NewReader(message.Bytes()), - ) - require.Error(t, err) - }) - } + var q QueryShortChanIDs + err = q.Decode(bytes.NewReader(buf.Bytes()), 0) + require.ErrorIs(t, err, ErrZlibNotSupported) } diff --git a/lnwire/reply_channel_range.go b/lnwire/reply_channel_range.go index 63ffd21f41..17f9f515e5 100644 --- a/lnwire/reply_channel_range.go +++ b/lnwire/reply_channel_range.go @@ -144,9 +144,9 @@ func (c *ReplyChannelRange) Encode(w *bytes.Buffer, pver uint32) error { return err } - // For both of the current encoding types, the channel ID's are to be - // sorted in place, so we'll do that now. The sorting is applied unless - // we were specifically requested not to for testing purposes. + // The channel ID's are to be sorted in place, so we'll do that now. + // The sorting is applied unless we were specifically requested not + // to for testing purposes. if !c.noSort { var scidPreSortIndex map[uint64]int if len(c.Timestamps) != 0 { diff --git a/lnwire/reply_channel_range_test.go b/lnwire/reply_channel_range_test.go index ac95066a5a..5ca97f7b29 100644 --- a/lnwire/reply_channel_range_test.go +++ b/lnwire/reply_channel_range_test.go @@ -52,13 +52,6 @@ func TestReplyChannelRangeEmpty(t *testing.T) { "00000000000000000000000000000000100000002" + "01000100", }, - { - name: "empty zlib encoding", - encType: EncodingSortedZlib, - encodedHex: "00000000000000000000000000000000000000" + - "0000000000000000000000000000000001000000" + - "0201000101", - }, } for _, test := range emptyChannelsTests { @@ -270,18 +263,6 @@ func TestReplyChannelRangeDecode(t *testing.T) { "0:69:42692", }, }, - { - name: "zlib encoding", - hex: "01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb4360" + - "12afca590b1a11466e2206000006400000006e010016" + - "01789c636000833e08659309a65878be010010a9023a", - expEncoding: EncodingSortedZlib, - expSCIDs: []string{ - "0:0:142", - "0:0:15465", - "0:4:3318", - }, - }, { name: "plain encoding including timestamps", hex: "01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb43601" + @@ -312,14 +293,23 @@ func TestReplyChannelRangeDecode(t *testing.T) { }, }, { - name: "unsupported encoding", + name: "zlib encoding rejected", + hex: "01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb4360" + + "12afca590b1a11466e2206000006400000006e010016" + + "01789c636000833e08659309a65878be010010a9023a", + expError: "zlib encoding (type 1) is no longer" + + " supported", + }, + { + name: "zlib encoding with timestamps rejected", hex: "01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb" + "436012afca590b1a11466e22060001ddde000005dc01" + "001801789c63600001036730c55e710d4cbb3d3c0800" + "17c303b1012201789c63606a3ac8c0577e9481bd622d" + "8327d7060686ad150c53a3ff0300554707db03180000" + "0457000008ae00000d050000115c000015b300001a0a", - expError: "unsupported encoding", + expError: "zlib encoding (type 1) is no longer" + + " supported", }, } diff --git a/lnwire/test_message.go b/lnwire/test_message.go index 469b6208a9..ff45b17204 100644 --- a/lnwire/test_message.go +++ b/lnwire/test_message.go @@ -1714,14 +1714,9 @@ func (q *QueryShortChanIDs) RandTestMessage(t *rapid.T) Message { hashBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "chainHash") copy(chainHash[:], hashBytes) - encodingType := EncodingSortedPlain - if rapid.Bool().Draw(t, "useZlibEncoding") { - encodingType = EncodingSortedZlib - } - msg := &QueryShortChanIDs{ ChainHash: chainHash, - EncodingType: encodingType, + EncodingType: EncodingSortedPlain, ExtraData: RandExtraOpaqueData(t, nil), noSort: false, } @@ -1765,11 +1760,9 @@ func (c *ReplyChannelRange) RandTestMessage(t *rapid.T) Message { NumBlocks: uint32(rapid.IntRange(1, 10000).Draw( t, "numBlocks"), ), - Complete: uint8(rapid.IntRange(0, 1).Draw(t, "complete")), - EncodingType: QueryEncoding( - rapid.IntRange(0, 1).Draw(t, "encodingType"), - ), - ExtraData: RandExtraOpaqueData(t, nil), + Complete: uint8(rapid.IntRange(0, 1).Draw(t, "complete")), + EncodingType: EncodingSortedPlain, + ExtraData: RandExtraOpaqueData(t, nil), } msg.ChainHash = RandChainHash(t)