Merge pull request #10103 from yyforyongyu/rate-limit-peer

Rate limit outgoing gossip bandwidth by peer
This commit is contained in:
Yong authored and GitHub committed 2025-09-02 23:31:29 +08:00
commit f51684dd0f
11 files changed
+181 -67

No files matched your search

+1
View File
@@ -721,6 +721,7 @@ func DefaultConfig() Config {
MsgBurstBytes: discovery.DefaultMsgBytesBurst, MsgBurstBytes: discovery.DefaultMsgBytesBurst,
FilterConcurrency: discovery.DefaultFilterConcurrency, FilterConcurrency: discovery.DefaultFilterConcurrency,
BanThreshold: discovery.DefaultBanThreshold, BanThreshold: discovery.DefaultBanThreshold,
PeerMsgRateBytes: discovery.DefaultPeerMsgBytesPerSecond,
}, },
Invoices: &lncfg.Invoices{ Invoices: &lncfg.Invoices{
HoldExpiryDelta: lncfg.DefaultHoldInvoiceExpiryDelta, HoldExpiryDelta: lncfg.DefaultHoldInvoiceExpiryDelta,
+5
View File
@@ -407,6 +407,10 @@ type Config struct {
// BanThreshold is the score used to decide whether a given peer is // BanThreshold is the score used to decide whether a given peer is
// banned or not. // banned or not.
BanThreshold uint64 BanThreshold uint64
// PeerMsgRateBytes is the rate limit for the number of bytes per second
// that we'll allocate to outbound gossip messages for a single peer.
PeerMsgRateBytes uint64
} }
// processedNetworkMsg is a wrapper around networkMsg and a boolean. It is // processedNetworkMsg is a wrapper around networkMsg and a boolean. It is
@@ -609,6 +613,7 @@ func New(cfg Config, selfKeyDesc *keychain.KeyDescriptor) *AuthenticatedGossiper
AllotedMsgBytesPerSecond: cfg.MsgRateBytes, AllotedMsgBytesPerSecond: cfg.MsgRateBytes,
AllotedMsgBytesBurst: cfg.MsgBurstBytes, AllotedMsgBytesBurst: cfg.MsgBurstBytes,
FilterConcurrency: cfg.FilterConcurrency, FilterConcurrency: cfg.FilterConcurrency,
PeerMsgBytesPerSecond: cfg.PeerMsgRateBytes,
}) })
gossiper.reliableSender = newReliableSender(&reliableSenderCfg{ gossiper.reliableSender = newReliableSender(&reliableSenderCfg{
+29 -19
View File
@@ -40,6 +40,11 @@ const (
// they'll be refilled at this rate. // they'll be refilled at this rate.
DefaultMsgBytesPerSecond = 1000 * 1_024 DefaultMsgBytesPerSecond = 1000 * 1_024
// DefaultPeerMsgBytesPerSecond is the max bytes/s we'll permit for
// outgoing messages for a single peer. Once tokens (bytes) have been
// taken from the bucket, they'll be refilled at this rate.
DefaultPeerMsgBytesPerSecond = 50 * 1_024
// assumedMsgSize is the assumed size of a message if we can't compute // assumedMsgSize is the assumed size of a message if we can't compute
// its serialized size. This comes out to 1 KB. // its serialized size. This comes out to 1 KB.
assumedMsgSize = 1_024 assumedMsgSize = 1_024
@@ -141,6 +146,11 @@ type SyncManagerCfg struct {
// FilterConcurrency is the maximum number of concurrent gossip filter // FilterConcurrency is the maximum number of concurrent gossip filter
// applications that can be processed. If not set, defaults to 5. // applications that can be processed. If not set, defaults to 5.
FilterConcurrency int FilterConcurrency int
// PeerMsgBytesPerSecond is the allotted bandwidth rate, expressed in
// bytes/second that a single gossip syncer can consume. Once we exceed
// this rate, message sending will block until we're below the rate.
PeerMsgBytesPerSecond uint64
} }
// SyncManager is a subsystem of the gossiper that manages the gossip syncers // SyncManager is a subsystem of the gossiper that manages the gossip syncers
@@ -200,7 +210,7 @@ type SyncManager struct {
gossipFilterSema chan struct{} gossipFilterSema chan struct{}
// rateLimiter dictates the frequency with which we will reply to gossip // rateLimiter dictates the frequency with which we will reply to gossip
// queries from a peer. This is used to delay responses to peers to // queries from all peers. This is used to delay responses to peers to
// prevent DOS vulnerabilities if they are spamming with an unreasonable // prevent DOS vulnerabilities if they are spamming with an unreasonable
// number of queries. // number of queries.
rateLimiter *rate.Limiter rateLimiter *rate.Limiter
@@ -554,8 +564,8 @@ func (m *SyncManager) isPinnedSyncer(s *GossipSyncer) bool {
// deriveRateLimitReservation will take the current message and derive a // deriveRateLimitReservation will take the current message and derive a
// reservation that can be used to wait on the rate limiter. // reservation that can be used to wait on the rate limiter.
func (m *SyncManager) deriveRateLimitReservation(msg lnwire.Message, func deriveRateLimitReservation(rl *rate.Limiter,
) (*rate.Reservation, error) { msg lnwire.Message) (*rate.Reservation, error) {
var ( var (
msgSize uint32 msgSize uint32
@@ -575,12 +585,12 @@ func (m *SyncManager) deriveRateLimitReservation(msg lnwire.Message,
msgSize = assumedMsgSize msgSize = assumedMsgSize
} }
return m.rateLimiter.ReserveN(time.Now(), int(msgSize)), nil return rl.ReserveN(time.Now(), int(msgSize)), nil
} }
// waitMsgDelay takes a delay, and waits until it has finished. // waitMsgDelay takes a delay, and waits until it has finished.
func (m *SyncManager) waitMsgDelay(ctx context.Context, peerPub [33]byte, func waitMsgDelay(ctx context.Context, peerPub [33]byte,
limitReservation *rate.Reservation) error { limitReservation *rate.Reservation, quit <-chan struct{}) error {
// If we've already replied a handful of times, we will start to delay // If we've already replied a handful of times, we will start to delay
// responses back to the remote peer. This can help prevent DOS attacks // responses back to the remote peer. This can help prevent DOS attacks
@@ -602,7 +612,7 @@ func (m *SyncManager) waitMsgDelay(ctx context.Context, peerPub [33]byte,
return ErrGossipSyncerExiting return ErrGossipSyncerExiting
case <-m.quit: case <-quit:
limitReservation.Cancel() limitReservation.Cancel()
return ErrGossipSyncerExiting return ErrGossipSyncerExiting
@@ -614,15 +624,15 @@ func (m *SyncManager) waitMsgDelay(ctx context.Context, peerPub [33]byte,
// maybeRateLimitMsg takes a message, and may wait a period of time to rate // maybeRateLimitMsg takes a message, and may wait a period of time to rate
// limit the msg. // limit the msg.
func (m *SyncManager) maybeRateLimitMsg(ctx context.Context, peerPub [33]byte, func maybeRateLimitMsg(ctx context.Context, rl *rate.Limiter, peerPub [33]byte,
msg lnwire.Message) error { msg lnwire.Message, quit <-chan struct{}) error {
delay, err := m.deriveRateLimitReservation(msg) delay, err := deriveRateLimitReservation(rl, msg)
if err != nil { if err != nil {
return nil return nil
} }
return m.waitMsgDelay(ctx, peerPub, delay) return waitMsgDelay(ctx, peerPub, delay, quit)
} }
// sendMessages sends a set of messages to the remote peer. // sendMessages sends a set of messages to the remote peer.
@@ -630,9 +640,13 @@ func (m *SyncManager) sendMessages(ctx context.Context, sync bool,
peer lnpeer.Peer, nodeID route.Vertex, msgs ...lnwire.Message) error { peer lnpeer.Peer, nodeID route.Vertex, msgs ...lnwire.Message) error {
for _, msg := range msgs { for _, msg := range msgs {
if err := m.maybeRateLimitMsg(ctx, nodeID, msg); err != nil { err := maybeRateLimitMsg(
ctx, m.rateLimiter, nodeID, msg, m.quit,
)
if err != nil {
return err return err
} }
if err := peer.SendMessageLazy(sync, msg); err != nil { if err := peer.SendMessageLazy(sync, msg); err != nil {
return err return err
} }
@@ -654,15 +668,10 @@ func (m *SyncManager) createGossipSyncer(peer lnpeer.Peer) *GossipSyncer {
encodingType: encoding, encodingType: encoding,
chunkSize: encodingTypeToChunkSize[encoding], chunkSize: encodingTypeToChunkSize[encoding],
batchSize: requestBatchSize, batchSize: requestBatchSize,
sendToPeer: func(ctx context.Context, sendMsg: func(ctx context.Context, sync bool,
msgs ...lnwire.Message) error { msgs ...lnwire.Message) error {
return m.sendMessages(ctx, false, peer, nodeID, msgs...) return m.sendMessages(ctx, sync, peer, nodeID, msgs...)
},
sendToPeerSync: func(ctx context.Context,
msgs ...lnwire.Message) error {
return m.sendMessages(ctx, true, peer, nodeID, msgs...)
}, },
ignoreHistoricalFilters: m.cfg.IgnoreHistoricalFilters, ignoreHistoricalFilters: m.cfg.IgnoreHistoricalFilters,
bestHeight: m.cfg.BestHeight, bestHeight: m.cfg.BestHeight,
@@ -670,6 +679,7 @@ func (m *SyncManager) createGossipSyncer(peer lnpeer.Peer) *GossipSyncer {
maxQueryChanRangeReplies: maxQueryChanRangeReplies, maxQueryChanRangeReplies: maxQueryChanRangeReplies,
noTimestampQueryOption: m.cfg.NoTimestampQueries, noTimestampQueryOption: m.cfg.NoTimestampQueries,
isStillZombieChannel: m.cfg.IsStillZombieChannel, isStillZombieChannel: m.cfg.IsStillZombieChannel,
msgBytesPerSecond: m.cfg.PeerMsgBytesPerSecond,
}, m.gossipFilterSema) }, m.gossipFilterSema)
// Gossip syncers are initialized by default in a PassiveSync type // Gossip syncers are initialized by default in a PassiveSync type
+12 -8
View File
@@ -748,7 +748,7 @@ func TestDeriveRateLimitReservation(t *testing.T) {
} }
// First message should have no delay as it fits within burst. // First message should have no delay as it fits within burst.
delay1, err := sm.deriveRateLimitReservation(msg) delay1, err := deriveRateLimitReservation(sm.rateLimiter, msg)
require.NoError(t, err) require.NoError(t, err)
require.Equal( require.Equal(
t, time.Duration(0), delay1.Delay(), "first message "+ t, time.Duration(0), delay1.Delay(), "first message "+
@@ -757,7 +757,7 @@ func TestDeriveRateLimitReservation(t *testing.T) {
// Second message should have a non-zero delay as the token // Second message should have a non-zero delay as the token
// bucket is now depleted. // bucket is now depleted.
delay2, err := sm.deriveRateLimitReservation(msg) delay2, err := deriveRateLimitReservation(sm.rateLimiter, msg)
require.NoError(t, err) require.NoError(t, err)
require.True( require.True(
t, delay2.Delay() > 0, "second message should have "+ t, delay2.Delay() > 0, "second message should have "+
@@ -766,7 +766,7 @@ func TestDeriveRateLimitReservation(t *testing.T) {
// Third message should have an even longer delay since the // Third message should have an even longer delay since the
// token bucket is still refilling at a constant rate. // token bucket is still refilling at a constant rate.
delay3, err := sm.deriveRateLimitReservation(msg) delay3, err := deriveRateLimitReservation(sm.rateLimiter, msg)
require.NoError(t, err) require.NoError(t, err)
require.True(t, delay3.Delay() > delay2.Delay(), "third "+ require.True(t, delay3.Delay() > delay2.Delay(), "third "+
"message should have longer delay than second: %s > %s", "message should have longer delay than second: %s > %s",
@@ -798,7 +798,7 @@ func TestDeriveRateLimitReservation(t *testing.T) {
// The error should propagate through // The error should propagate through
// deriveRateLimitReservation. // deriveRateLimitReservation.
_, err := sm.deriveRateLimitReservation(msg) _, err := deriveRateLimitReservation(sm.rateLimiter, msg)
require.Error(t, err) require.Error(t, err)
require.Equal( require.Equal(
t, errMsg, err, "Error should be propagated unchanged", t, errMsg, err, "Error should be propagated unchanged",
@@ -815,7 +815,7 @@ func TestDeriveRateLimitReservation(t *testing.T) {
initialMsg := &TestSizeableMessage{ initialMsg := &TestSizeableMessage{
size: uint32(bytesBurst), size: uint32(bytesBurst),
} }
_, err := sm.deriveRateLimitReservation(initialMsg) _, err := deriveRateLimitReservation(sm.rateLimiter, initialMsg)
require.NoError(t, err) require.NoError(t, err)
// Now send two messages of different sizes and compare their // Now send two messages of different sizes and compare their
@@ -828,18 +828,22 @@ func TestDeriveRateLimitReservation(t *testing.T) {
} }
// Send the small message first. // Send the small message first.
smallDelay, err := sm.deriveRateLimitReservation(smallMsg) smallDelay, err := deriveRateLimitReservation(
sm.rateLimiter, smallMsg,
)
require.NoError(t, err) require.NoError(t, err)
// Reset the limiter to the same state, then empty the bucket. // Reset the limiter to the same state, then empty the bucket.
sm.rateLimiter = rate.NewLimiter( sm.rateLimiter = rate.NewLimiter(
rate.Limit(bytesPerSec), int(bytesBurst), rate.Limit(bytesPerSec), int(bytesBurst),
) )
_, err = sm.deriveRateLimitReservation(initialMsg) _, err = deriveRateLimitReservation(sm.rateLimiter, initialMsg)
require.NoError(t, err) require.NoError(t, err)
// Now send the large message. // Now send the large message.
largeDelay, err := sm.deriveRateLimitReservation(largeMsg) largeDelay, err := deriveRateLimitReservation(
sm.rateLimiter, largeMsg,
)
require.NoError(t, err) require.NoError(t, err)
// The large message should have a longer delay than the small // The large message should have a longer delay than the small
+79 -22
View File
@@ -17,6 +17,7 @@ import (
graphdb "github.com/lightningnetwork/lnd/graph/db" graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnpeer"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"golang.org/x/time/rate"
) )
// SyncerType encapsulates the different types of syncing mechanisms for a // SyncerType encapsulates the different types of syncing mechanisms for a
@@ -245,15 +246,10 @@ type gossipSyncerCfg struct {
// the remote node in a single QueryShortChanIDs request. // the remote node in a single QueryShortChanIDs request.
batchSize int32 batchSize int32
// sendToPeer sends a variadic number of messages to the remote peer. // sendMsg sends a variadic number of messages to the remote peer.
// This method should not block while waiting for sends to be written // The boolean indicates whether this method should be blocked or not
// to the wire. // while waiting for sends to be written to the wire.
sendToPeer func(context.Context, ...lnwire.Message) error sendMsg func(context.Context, bool, ...lnwire.Message) error
// sendToPeerSync sends a variadic number of messages to the remote
// peer, blocking until all messages have been sent successfully or a
// write error is encountered.
sendToPeerSync func(context.Context, ...lnwire.Message) error
// noSyncChannels will prevent the GossipSyncer from spawning a // noSyncChannels will prevent the GossipSyncer from spawning a
// channelGraphSyncer, meaning we will not try to reconcile unknown // channelGraphSyncer, meaning we will not try to reconcile unknown
@@ -295,6 +291,11 @@ type gossipSyncerCfg struct {
// timestampQueueSize is the size of the timestamp range queue. If not // timestampQueueSize is the size of the timestamp range queue. If not
// set, defaults to the global timestampQueueSize constant. // set, defaults to the global timestampQueueSize constant.
timestampQueueSize int timestampQueueSize int
// msgBytesPerSecond is the allotted bandwidth rate, expressed in
// bytes/second that this gossip syncer can consume. Once we exceed this
// rate, message sending will block until we're below the rate.
msgBytesPerSecond uint64
} }
// GossipSyncer is a struct that handles synchronizing the channel graph state // GossipSyncer is a struct that handles synchronizing the channel graph state
@@ -407,6 +408,10 @@ type GossipSyncer struct {
// allows contexts that either block or cancel on those depending on // allows contexts that either block or cancel on those depending on
// the use case. // the use case.
cg *fn.ContextGuard cg *fn.ContextGuard
// rateLimiter dictates the frequency with which we will reply to gossip
// queries to this peer.
rateLimiter *rate.Limiter
} }
// newGossipSyncer returns a new instance of the GossipSyncer populated using // newGossipSyncer returns a new instance of the GossipSyncer populated using
@@ -418,6 +423,17 @@ func newGossipSyncer(cfg gossipSyncerCfg, sema chan struct{}) *GossipSyncer {
queueSize = defaultTimestampQueueSize queueSize = defaultTimestampQueueSize
} }
bytesPerSecond := cfg.msgBytesPerSecond
if bytesPerSecond == 0 {
bytesPerSecond = DefaultPeerMsgBytesPerSecond
}
bytesBurst := 2 * bytesPerSecond
// We'll use this rate limiter to limit this single peer.
rateLimiter := rate.NewLimiter(
rate.Limit(bytesPerSecond), int(bytesBurst),
)
return &GossipSyncer{ return &GossipSyncer{
cfg: cfg, cfg: cfg,
syncTransitionReqs: make(chan *syncTransitionReq), syncTransitionReqs: make(chan *syncTransitionReq),
@@ -427,8 +443,9 @@ func newGossipSyncer(cfg gossipSyncerCfg, sema chan struct{}) *GossipSyncer {
timestampRangeQueue: make( timestampRangeQueue: make(
chan *lnwire.GossipTimestampRange, queueSize, chan *lnwire.GossipTimestampRange, queueSize,
), ),
syncerSema: sema, syncerSema: sema,
cg: fn.NewContextGuard(), cg: fn.NewContextGuard(),
rateLimiter: rateLimiter,
} }
} }
@@ -496,7 +513,7 @@ func (g *GossipSyncer) handleSyncingChans(ctx context.Context) {
// Send the msg to the remote peer, which is non-blocking as // Send the msg to the remote peer, which is non-blocking as
// `sendToPeer` only queues the msg in Brontide. // `sendToPeer` only queues the msg in Brontide.
err = g.cfg.sendToPeer(ctx, queryRangeMsg) err = g.sendToPeer(ctx, queryRangeMsg)
if err != nil { if err != nil {
log.Errorf("Unable to send chan range query: %v", err) log.Errorf("Unable to send chan range query: %v", err)
return return
@@ -783,11 +800,11 @@ func (g *GossipSyncer) sendGossipTimestampRange(ctx context.Context,
TimestampRange: timestampRange, TimestampRange: timestampRange,
} }
if err := g.cfg.sendToPeer(ctx, localUpdateHorizon); err != nil { if err := g.sendToPeer(ctx, localUpdateHorizon); err != nil {
return err return err
} }
if firstTimestamp == zeroTimestamp && timestampRange == 0 { if firstTimestamp.Equal(zeroTimestamp) && timestampRange == 0 {
g.localUpdateHorizon = nil g.localUpdateHorizon = nil
} else { } else {
g.localUpdateHorizon = localUpdateHorizon g.localUpdateHorizon = localUpdateHorizon
@@ -839,7 +856,7 @@ func (g *GossipSyncer) synchronizeChanIDs(ctx context.Context) bool {
// With our chunk obtained, we'll send over our next query, then return // With our chunk obtained, we'll send over our next query, then return
// false indicating that we're net yet fully synced. // false indicating that we're net yet fully synced.
err := g.cfg.sendToPeer(ctx, &lnwire.QueryShortChanIDs{ err := g.sendToPeer(ctx, &lnwire.QueryShortChanIDs{
ChainHash: g.cfg.chainHash, ChainHash: g.cfg.chainHash,
EncodingType: lnwire.EncodingSortedPlain, EncodingType: lnwire.EncodingSortedPlain,
ShortChanIDs: queryChunk, ShortChanIDs: queryChunk,
@@ -1154,7 +1171,7 @@ func (g *GossipSyncer) replyChanRangeQuery(ctx context.Context,
"chain=%v, we're on chain=%v", query.ChainHash, "chain=%v, we're on chain=%v", query.ChainHash,
g.cfg.chainHash) g.cfg.chainHash)
return g.cfg.sendToPeerSync(ctx, &lnwire.ReplyChannelRange{ return g.sendToPeerSync(ctx, &lnwire.ReplyChannelRange{
ChainHash: query.ChainHash, ChainHash: query.ChainHash,
FirstBlockHeight: query.FirstBlockHeight, FirstBlockHeight: query.FirstBlockHeight,
NumBlocks: query.NumBlocks, NumBlocks: query.NumBlocks,
@@ -1227,7 +1244,7 @@ func (g *GossipSyncer) replyChanRangeQuery(ctx context.Context,
) )
} }
return g.cfg.sendToPeerSync(ctx, &lnwire.ReplyChannelRange{ return g.sendToPeerSync(ctx, &lnwire.ReplyChannelRange{
ChainHash: query.ChainHash, ChainHash: query.ChainHash,
NumBlocks: numBlocks, NumBlocks: numBlocks,
FirstBlockHeight: firstHeight, FirstBlockHeight: firstHeight,
@@ -1333,7 +1350,7 @@ func (g *GossipSyncer) replyShortChanIDs(ctx context.Context,
"chain=%v, we're on chain=%v", query.ChainHash, "chain=%v, we're on chain=%v", query.ChainHash,
g.cfg.chainHash) g.cfg.chainHash)
return g.cfg.sendToPeerSync(ctx, &lnwire.ReplyShortChanIDsEnd{ return g.sendToPeerSync(ctx, &lnwire.ReplyShortChanIDsEnd{
ChainHash: query.ChainHash, ChainHash: query.ChainHash,
Complete: 0, Complete: 0,
}) })
@@ -1364,7 +1381,7 @@ func (g *GossipSyncer) replyShortChanIDs(ctx context.Context,
// each one individually and synchronously to throttle the sends and // each one individually and synchronously to throttle the sends and
// perform buffering of responses in the syncer as opposed to the peer. // perform buffering of responses in the syncer as opposed to the peer.
for _, msg := range replyMsgs { for _, msg := range replyMsgs {
err := g.cfg.sendToPeerSync(ctx, msg) err := g.sendToPeerSync(ctx, msg)
if err != nil { if err != nil {
return err return err
} }
@@ -1372,7 +1389,7 @@ func (g *GossipSyncer) replyShortChanIDs(ctx context.Context,
// Regardless of whether we had any messages to reply with, send over // Regardless of whether we had any messages to reply with, send over
// the sentinel message to signal that the stream has terminated. // the sentinel message to signal that the stream has terminated.
return g.cfg.sendToPeerSync(ctx, &lnwire.ReplyShortChanIDsEnd{ return g.sendToPeerSync(ctx, &lnwire.ReplyShortChanIDsEnd{
ChainHash: query.ChainHash, ChainHash: query.ChainHash,
Complete: 1, Complete: 1,
}) })
@@ -1462,7 +1479,7 @@ func (g *GossipSyncer) ApplyGossipFilter(ctx context.Context,
defer g.isSendingBacklog.Store(false) defer g.isSendingBacklog.Store(false)
for _, msg := range newUpdatestoSend { for _, msg := range newUpdatestoSend {
err := g.cfg.sendToPeerSync(ctx, msg) err := g.sendToPeerSync(ctx, msg)
switch { switch {
case err == ErrGossipSyncerExiting: case err == ErrGossipSyncerExiting:
return return
@@ -1608,7 +1625,7 @@ func (g *GossipSyncer) FilterGossipMsgs(ctx context.Context,
return return
} }
if err = g.cfg.sendToPeer(ctx, msgsToSend...); err != nil { if err = g.sendToPeer(ctx, msgsToSend...); err != nil {
log.Errorf("unable to send gossip msgs: %v", err) log.Errorf("unable to send gossip msgs: %v", err)
} }
@@ -1802,3 +1819,43 @@ func (g *GossipSyncer) handleHistoricalSync(req *historicalSyncReq) {
g.setSyncState(syncingChans) g.setSyncState(syncingChans)
close(req.doneChan) close(req.doneChan)
} }
// sendToPeer sends a variadic number of messages to the remote peer. This
// method should not block while waiting for sends to be written to the wire.
func (g *GossipSyncer) sendToPeer(ctx context.Context,
msgs ...lnwire.Message) error {
return g.sendMsgRateLimited(ctx, false, msgs...)
}
// sendToPeerSync sends a variadic number of messages to the remote peer,
// blocking until all messages have been sent successfully or a write error is
// encountered.
func (g *GossipSyncer) sendToPeerSync(ctx context.Context,
msgs ...lnwire.Message) error {
return g.sendMsgRateLimited(ctx, true, msgs...)
}
// sendMsgRateLimited sends a variadic number of messages to the remote peer,
// applying our per-peer rate limit before each send. The sync boolean
// determines if the send is blocking or not.
func (g *GossipSyncer) sendMsgRateLimited(ctx context.Context, sync bool,
msgs ...lnwire.Message) error {
for _, msg := range msgs {
err := maybeRateLimitMsg(
ctx, g.rateLimiter, g.cfg.peerPub, msg, g.cg.Done(),
)
if err != nil {
return err
}
err = g.cfg.sendMsg(ctx, sync, msg)
if err != nil {
return err
}
}
return nil
}
+8 -3
View File
@@ -26,9 +26,14 @@ func TestGossipSyncerSingleBacklogSend(t *testing.T) {
// Create a blocking sendToPeerSync function. We'll use this to simulate // Create a blocking sendToPeerSync function. We'll use this to simulate
// sending a large backlog. // sending a large backlog.
blockingSendChan := make(chan struct{}) blockingSendChan := make(chan struct{})
sendToPeerSync := func(_ context.Context, mockSendMsg := func(_ context.Context, sync bool,
msgs ...lnwire.Message) error { msgs ...lnwire.Message) error {
// Sync is only true when calling `sendToPeerSync`.
if !sync {
return nil
}
// Track that we're in a send goroutine. // Track that we're in a send goroutine.
count := activeGoroutines.Add(1) count := activeGoroutines.Add(1)
totalGoroutinesLaunched.Add(1) totalGoroutinesLaunched.Add(1)
@@ -55,8 +60,8 @@ func TestGossipSyncerSingleBacklogSend(t *testing.T) {
defaultChunkSize, true, true, true, defaultChunkSize, true, true, true,
) )
// Override the sendToPeerSync to use our blocking version. // Override the sendMsg to use our blocking version.
syncer.cfg.sendToPeerSync = sendToPeerSync syncer.cfg.sendMsg = mockSendMsg
syncer.cfg.ignoreHistoricalFilters = false syncer.cfg.ignoreHistoricalFilters = false
syncer.Start() syncer.Start()
+14 -15
View File
@@ -200,13 +200,7 @@ func newTestSyncer(hID lnwire.ShortChannelID,
noSyncChannels: !syncChannels, noSyncChannels: !syncChannels,
noReplyQueries: !replyQueries, noReplyQueries: !replyQueries,
noTimestampQueryOption: !timestamps, noTimestampQueryOption: !timestamps,
sendToPeer: func(_ context.Context, sendMsg: func(_ context.Context, _ bool,
msgs ...lnwire.Message) error {
msgChan <- msgs
return nil
},
sendToPeerSync: func(_ context.Context,
msgs ...lnwire.Message) error { msgs ...lnwire.Message) error {
msgChan <- msgs msgChan <- msgs
@@ -392,16 +386,21 @@ func TestGossipSyncerFilterGossipMsgsAllInMemory(t *testing.T) {
// We'll then instruct the gossiper to filter this set of messages. // We'll then instruct the gossiper to filter this set of messages.
syncer.FilterGossipMsgs(ctx, msgs...) syncer.FilterGossipMsgs(ctx, msgs...)
// Out of all the messages we sent in, we should only get 2 of them // Out of all the messages we sent in, we should only get 3 of them
// back. // back.
select { msgReceived := make([]lnwire.Message, 0, 3)
case <-time.After(time.Second * 15): for {
t.Fatalf("no msgs received") select {
case <-time.After(time.Second * 1):
t.Fatalf("timeout receiving msg, want 3 msgs, got %v "+
"messages: %v", len(msgReceived), msgReceived)
case msgs := <-msgChan: case msgs := <-msgChan:
if len(msgs) != 3 { msgReceived = append(msgReceived, msgs...)
t.Fatalf("expected 3 messages instead got %v "+ }
"messages: %v", len(msgs), spew.Sdump(msgs))
if len(msgReceived) == 3 {
break
} }
} }
@@ -94,6 +94,10 @@ circuit. The indices are only available for forwarding events saved after v0.20.
finish under this timeout value. Consider using a larger timeout value if you finish under this timeout value. Consider using a larger timeout value if you
have a slow network. have a slow network.
* The default value for `gossip.msg-rate-bytes` has been
[increased](https://github.com/lightningnetwork/lnd/pull/10096) from 100KB to
1MB, and `gossip.msg-burst-bytes` has been increased from 200KB to 2MB.
* Added [`deletecanceledinvoices`]( * Added [`deletecanceledinvoices`](
https://github.com/lightningnetwork/lnd/pull/9625) RPC to allow the removal of https://github.com/lightningnetwork/lnd/pull/9625) RPC to allow the removal of
a canceled invoice. Supports deleting a canceled invoice by providing its a canceled invoice. Supports deleting a canceled invoice by providing its
@@ -105,6 +109,14 @@ circuit. The indices are only available for forwarding events saved after v0.20.
disconnected and banned. Setting the value to 0 effectively disables banning disconnected and banned. Setting the value to 0 effectively disables banning
by setting the threshold to the maximum possible value. by setting the threshold to the maximum possible value.
* A [new config](https://github.com/lightningnetwork/lnd/pull/10103) value
`gossip.peer-msg-rate-bytes=102400` is introduced to allow limiting the
outgoing bandwidth used by each peer when processing gossip-related messages.
Note this is different from `gossip.msg-rate-bytes`, as this new config
controls the bandwidth per peer, while `msg-rate-bytes` controls the gossip as
a whole. This new config prevents a single misbehaving peer from using up all
the bandwidth.
## lncli Additions ## lncli Additions
* [`lncli sendpayment` and `lncli queryroutes` now support the * [`lncli sendpayment` and `lncli queryroutes` now support the
+13
View File
@@ -41,6 +41,8 @@ type Gossip struct {
FilterConcurrency int `long:"filter-concurrency" description:"The maximum number of concurrent gossip filter applications that can be processed. If not set, defaults to 5."` FilterConcurrency int `long:"filter-concurrency" description:"The maximum number of concurrent gossip filter applications that can be processed. If not set, defaults to 5."`
BanThreshold uint64 `long:"ban-threshold" description:"The score at which a peer is banned. A peer's ban score is incremented for each invalid gossip message. Invalid messages include those with bad signatures, stale timestamps, excessive updates, or invalid chain data. Once the score reaches this threshold, the peer is banned. Set to 0 to disable banning."` BanThreshold uint64 `long:"ban-threshold" description:"The score at which a peer is banned. A peer's ban score is incremented for each invalid gossip message. Invalid messages include those with bad signatures, stale timestamps, excessive updates, or invalid chain data. Once the score reaches this threshold, the peer is banned. Set to 0 to disable banning."`
PeerMsgRateBytes uint64 `long:"peer-msg-rate-bytes" description:"The peer-specific rate of outbound gossip messages, expressed in bytes per second. This setting controls the long-term average speed of gossip traffic sent from your node. The rate limit is applied to each peer. If the rate of outgoing messages exceeds this value, lnd will start to queue and delay messages sending to that peer to stay within the limit."`
} }
// Parse the pubkeys for the pinned syncers. // Parse the pubkeys for the pinned syncers.
@@ -72,6 +74,17 @@ func (g *Gossip) Validate() error {
g.MsgBurstBytes, lnwire.MaxSliceLength) g.MsgBurstBytes, lnwire.MaxSliceLength)
} }
if g.MsgBurstBytes <= g.MsgRateBytes {
return fmt.Errorf("msg-burst-bytes=%v must be greater than "+
"msg-rate-bytes=%v", g.MsgBurstBytes, g.MsgRateBytes)
}
if g.MsgRateBytes <= g.PeerMsgRateBytes {
return fmt.Errorf("msg-rate-bytes=%v must be greater than "+
"peer-msg-rate-bytes=%v", g.MsgRateBytes,
g.PeerMsgRateBytes)
}
return nil return nil
} }
+7
View File
@@ -1814,6 +1814,13 @@
; ;
; gossip.ban-threshold=100 ; gossip.ban-threshold=100
; The peer-specific rate of outbound gossip messages, expressed in bytes per
; second. This setting controls the long-term average speed of gossip traffic
; sent from your node. The rate limit is applied to each peer. If the rate of
; outgoing messages exceeds this value, lnd will start to queue and delay
; messages sending to that peer to stay within the limit.
; gossip.peer-msg-rate-bytes=51200
[invoices] [invoices]
; If a hold invoice has accepted htlcs that reach their expiry height and are ; If a hold invoice has accepted htlcs that reach their expiry height and are
+1
View File
@@ -1103,6 +1103,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
MsgBurstBytes: cfg.Gossip.MsgBurstBytes, MsgBurstBytes: cfg.Gossip.MsgBurstBytes,
FilterConcurrency: cfg.Gossip.FilterConcurrency, FilterConcurrency: cfg.Gossip.FilterConcurrency,
BanThreshold: cfg.Gossip.BanThreshold, BanThreshold: cfg.Gossip.BanThreshold,
PeerMsgRateBytes: cfg.Gossip.PeerMsgRateBytes,
}, nodeKeyDesc) }, nodeKeyDesc)
accessCfg := &accessManConfig{ accessCfg := &accessManConfig{