Skip to content

Commit ce0d9cf

Browse files
committed
Drop rate-limited static invoice requests
Drop static invoice requests when the rate limit is reached instead of retrying them and delaying later events. Keep retrying storage errors and only acknowledge invoices after saving them. Co-Authored-By: HAL 9000
1 parent 685d038 commit ce0d9cf

2 files changed

Lines changed: 226 additions & 17 deletions

File tree

src/event.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ use crate::io::{
4848
use crate::liquidity::LiquiditySource;
4949
use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
5050
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
51-
use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore;
51+
use crate::payment::asynchronous::static_invoice_store::{
52+
StaticInvoiceStore, StaticInvoiceStoreError,
53+
};
5254
use crate::payment::forwarding_store::{ForwardRecord, ForwardingStore};
5355
use crate::payment::store::{
5456
PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus,
@@ -2115,7 +2117,11 @@ where
21152117
Ok(_) => {
21162118
self.channel_manager.static_invoice_persisted(invoice_persisted_path);
21172119
},
2118-
Err(e) => {
2120+
Err(StaticInvoiceStoreError::RateLimited) => {
2121+
// Drop silently: logging each rejected request, even at trace level,
2122+
// can cause excessive log output and I/O under sustained load.
2123+
},
2124+
Err(StaticInvoiceStoreError::Io(e)) => {
21192125
log_error!(self.logger, "Failed to persist static invoice: {}", e);
21202126
return Err(ReplayEvent());
21212127
},
@@ -2151,7 +2157,11 @@ where
21512157
invoice_slot
21522158
);
21532159
},
2154-
Err(e) => {
2160+
Err(StaticInvoiceStoreError::RateLimited) => {
2161+
// Drop silently: logging each rejected request, even at trace level,
2162+
// can cause excessive log output and I/O under sustained load.
2163+
},
2164+
Err(StaticInvoiceStoreError::Io(e)) => {
21552165
log_error!(self.logger, "Failed to retrieve static invoice: {}", e);
21562166
return Err(ReplayEvent());
21572167
},

src/payment/asynchronous/static_invoice_store.rs

Lines changed: 213 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ impl_writeable_tlv_based!(PersistedStaticInvoice, {
3333
(2, request_path, required)
3434
});
3535

36+
#[derive(Debug)]
37+
pub(crate) enum StaticInvoiceStoreError {
38+
RateLimited,
39+
Io(lightning::io::Error),
40+
}
41+
3642
pub(crate) struct StaticInvoiceStore {
3743
kv_store: Arc<DynStore>,
3844
request_rate_limiter: Mutex<RateLimiter>,
@@ -62,18 +68,18 @@ impl StaticInvoiceStore {
6268

6369
fn check_rate_limit(
6470
limiter: &Mutex<RateLimiter>, recipient_id: &[u8],
65-
) -> Result<(), lightning::io::Error> {
71+
) -> Result<(), StaticInvoiceStoreError> {
6672
let mut limiter = limiter.lock().expect("lock");
6773
if !limiter.allow(recipient_id) {
68-
Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, "Rate limit exceeded"))
74+
Err(StaticInvoiceStoreError::RateLimited)
6975
} else {
7076
Ok(())
7177
}
7278
}
7379

7480
pub(crate) async fn handle_static_invoice_requested(
7581
&self, recipient_id: &[u8], invoice_slot: u16,
76-
) -> Result<Option<(StaticInvoice, BlindedMessagePath)>, lightning::io::Error> {
82+
) -> Result<Option<(StaticInvoice, BlindedMessagePath)>, StaticInvoiceStoreError> {
7783
Self::check_rate_limit(&self.request_rate_limiter, &recipient_id)?;
7884

7985
let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, recipient_id);
@@ -97,21 +103,14 @@ impl StaticInvoiceStore {
97103
)
98104
})
99105
})
100-
.or_else(
101-
|e| {
102-
if e.kind() == lightning::io::ErrorKind::NotFound {
103-
Ok(None)
104-
} else {
105-
Err(e)
106-
}
107-
},
108-
)
106+
.or_else(|e| if e.kind() == lightning::io::ErrorKind::NotFound { Ok(None) } else { Err(e) })
107+
.map_err(StaticInvoiceStoreError::Io)
109108
}
110109

111110
pub(crate) async fn handle_persist_static_invoice(
112111
&self, invoice: StaticInvoice, invoice_request_path: BlindedMessagePath, invoice_slot: u16,
113112
recipient_id: Vec<u8>,
114-
) -> Result<(), lightning::io::Error> {
113+
) -> Result<(), StaticInvoiceStoreError> {
115114
Self::check_rate_limit(&self.persist_rate_limiter, &recipient_id)?;
116115

117116
let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, &recipient_id);
@@ -120,7 +119,7 @@ impl StaticInvoiceStore {
120119
PersistedStaticInvoice { invoice, request_path: invoice_request_path };
121120

122121
let mut buf = Vec::new();
123-
persisted_invoice.write(&mut buf)?;
122+
persisted_invoice.write(&mut buf).map_err(StaticInvoiceStoreError::Io)?;
124123

125124
// Static invoices will be persisted at "static_invoices/<sha256(recipient_id)>/<invoice_slot>".
126125
//
@@ -133,6 +132,7 @@ impl StaticInvoiceStore {
133132
buf,
134133
)
135134
.await
135+
.map_err(StaticInvoiceStoreError::Io)
136136
}
137137

138138
fn get_storage_location(invoice_slot: u16, recipient_id: &[u8]) -> (String, String) {
@@ -146,6 +146,7 @@ impl StaticInvoiceStore {
146146

147147
#[cfg(test)]
148148
mod tests {
149+
use std::sync::atomic::{AtomicBool, Ordering};
149150
use std::sync::Arc;
150151
use std::time::Duration;
151152

@@ -154,16 +155,214 @@ mod tests {
154155
use lightning::blinded_path::message::BlindedMessagePath;
155156
use lightning::blinded_path::payment::{BlindedPayInfo, BlindedPaymentPath};
156157
use lightning::blinded_path::BlindedHop;
158+
use lightning::events::Event;
159+
use lightning::ln::channelmanager::PaymentId;
157160
use lightning::ln::inbound_payment::ExpandedKey;
158161
use lightning::offers::nonce::Nonce;
159162
use lightning::offers::offer::OfferBuilder;
160163
use lightning::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder};
164+
use lightning::onion_message::async_payments::{
165+
AsyncPaymentsMessage, AsyncPaymentsMessageHandler,
166+
};
167+
use lightning::onion_message::messenger::Responder;
168+
use lightning::onion_message::offers::OffersMessageHandler;
161169
use lightning::sign::EntropySource;
170+
use lightning::util::persist::KVStore;
171+
use lightning::util::ser::{Readable, Writeable};
172+
use lightning::util::wallet_utils::Wallet as LdkWallet;
162173
use lightning_types::features::BlindedHopFeatures;
163174

175+
use crate::builder::NodeBuilder;
176+
use crate::entropy::NodeEntropy;
177+
use crate::event::EventHandler;
164178
use crate::io::test_utils::InMemoryStore;
179+
use crate::io::STATIC_INVOICE_STORE_PRIMARY_NAMESPACE;
180+
use crate::logger::{LogRecord, LogWriter, Logger};
181+
use crate::payment::asynchronous::rate_limiter::RateLimiter;
165182
use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore;
166183
use crate::types::{DynStore, DynStoreWrapper};
184+
use crate::{BumpTransactionEventHandler, Node};
185+
186+
#[derive(Default)]
187+
struct TestLogWriter {
188+
logged: AtomicBool,
189+
}
190+
191+
impl LogWriter for TestLogWriter {
192+
fn log(&self, _record: LogRecord) {
193+
self.logged.store(true, Ordering::Relaxed);
194+
}
195+
}
196+
197+
fn event_handler(
198+
store: StaticInvoiceStore,
199+
) -> (Node, EventHandler<Arc<Logger>>, Arc<TestLogWriter>) {
200+
let mut builder = NodeBuilder::new();
201+
builder.set_log_facade_logger();
202+
#[cfg(not(feature = "uniffi"))]
203+
let entropy = NodeEntropy::from_seed_bytes([42; 64]);
204+
#[cfg(feature = "uniffi")]
205+
let entropy = NodeEntropy::from_seed_bytes(vec![42; 64]).unwrap();
206+
let node = builder.build_with_store(entropy, InMemoryStore::new()).unwrap();
207+
let bump_handler = Arc::new(BumpTransactionEventHandler::new(
208+
Arc::clone(&node.tx_broadcaster),
209+
Arc::new(LdkWallet::new(Arc::clone(&node.wallet), Arc::clone(&node.logger))),
210+
Arc::clone(&node.keys_manager),
211+
Arc::clone(&node.logger),
212+
));
213+
let log_writer = Arc::new(TestLogWriter::default());
214+
let handler = EventHandler::new(
215+
Arc::clone(&node.event_queue),
216+
Arc::clone(&node.wallet),
217+
bump_handler,
218+
Arc::clone(&node.channel_manager),
219+
Arc::clone(&node.connection_manager),
220+
Arc::clone(&node.output_sweeper),
221+
Arc::clone(&node.network_graph),
222+
Arc::clone(&node.liquidity_source),
223+
Arc::clone(&node.payment_store),
224+
Arc::clone(&node.forwarding_store),
225+
Arc::clone(&node.peer_store),
226+
Arc::clone(&node.keys_manager),
227+
Some(store),
228+
Arc::clone(&node.onion_messenger),
229+
None,
230+
None,
231+
Arc::clone(&node.runtime),
232+
Arc::new(Logger::new_custom_writer(log_writer.clone())),
233+
Arc::clone(&node.config),
234+
);
235+
(node, handler, log_writer)
236+
}
237+
238+
fn responder() -> Responder {
239+
// Responder has no public constructor, so use its serialized representation.
240+
struct ReplyPath {
241+
path: BlindedMessagePath,
242+
}
243+
lightning::impl_writeable_tlv_based!(ReplyPath, { (0, path, required) });
244+
let bytes = ReplyPath { path: blinded_path() }.encode();
245+
Responder::read(&mut &bytes[..]).unwrap()
246+
}
247+
248+
fn static_invoice_event(persist: bool) -> Event {
249+
if persist {
250+
Event::PersistStaticInvoice {
251+
invoice: invoice(),
252+
invoice_request_path: blinded_path(),
253+
invoice_slot: 1,
254+
recipient_id: vec![1, 1, 1],
255+
invoice_persisted_path: responder(),
256+
}
257+
} else {
258+
let invoice_request = OfferBuilder::new(recipient_pubkey())
259+
.amount_msats(1_000)
260+
.build()
261+
.unwrap()
262+
.request_invoice(
263+
&ExpandedKey::new([42; 32]),
264+
Nonce::from_entropy_source(&FixedEntropy {}),
265+
&Secp256k1::new(),
266+
PaymentId([42; 32]),
267+
)
268+
.unwrap()
269+
.build_and_sign()
270+
.unwrap();
271+
Event::StaticInvoiceRequested {
272+
recipient_id: vec![1, 1, 1],
273+
invoice_slot: 0,
274+
reply_path: responder(),
275+
invoice_request,
276+
}
277+
}
278+
}
279+
280+
async fn check_rate_limited_static_invoice_event(persist: bool) {
281+
let kv_store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
282+
let mut store = StaticInvoiceStore::new(Arc::clone(&kv_store));
283+
store
284+
.handle_persist_static_invoice(invoice(), blinded_path(), 0, vec![1, 1, 1])
285+
.await
286+
.unwrap();
287+
// Reject requests deterministically, without depending on refill timing.
288+
*store.request_rate_limiter.get_mut().unwrap() =
289+
RateLimiter::new(0, Duration::from_secs(1), Duration::from_secs(600));
290+
*store.persist_rate_limiter.get_mut().unwrap() =
291+
RateLimiter::new(0, Duration::from_secs(1), Duration::from_secs(600));
292+
let (node, handler, log_writer) = event_handler(store);
293+
node.channel_manager.push_pending_event(static_invoice_event(persist));
294+
node.channel_manager.push_pending_event(Event::PaymentFailed {
295+
payment_id: PaymentId([42; 32]),
296+
payment_hash: None,
297+
reason: None,
298+
});
299+
let handled_next_event = AtomicBool::new(false);
300+
node.channel_manager
301+
.process_pending_events_async(|event| async {
302+
if matches!(event, Event::PaymentFailed { .. }) {
303+
handled_next_event.store(true, Ordering::Relaxed);
304+
Ok(())
305+
} else {
306+
handler.handle_event(event).await
307+
}
308+
})
309+
.await;
310+
assert!(
311+
handled_next_event.load(Ordering::Relaxed),
312+
"a rate-limited static invoice event must not delay the next event"
313+
);
314+
assert!(
315+
!log_writer.logged.load(Ordering::Relaxed),
316+
"a rate-limited static invoice event must not produce a log message"
317+
);
318+
assert!(AsyncPaymentsMessageHandler::release_pending_messages(&*node.channel_manager)
319+
.is_empty());
320+
assert!(OffersMessageHandler::release_pending_messages(&*node.channel_manager).is_empty());
321+
let store = StaticInvoiceStore::new(kv_store);
322+
assert!(store.handle_static_invoice_requested(&[1, 1, 1], 0).await.unwrap().is_some());
323+
assert!(store.handle_static_invoice_requested(&[1, 1, 1], 1).await.unwrap().is_none());
324+
}
325+
326+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
327+
async fn rate_limited_static_invoice_persistence_does_not_replay() {
328+
check_rate_limited_static_invoice_event(true).await;
329+
}
330+
331+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
332+
async fn rate_limited_static_invoice_request_does_not_replay() {
333+
check_rate_limited_static_invoice_event(false).await;
334+
}
335+
336+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
337+
async fn static_invoice_persistence_is_acknowledged() {
338+
let kv_store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
339+
let (node, handler, _) = event_handler(StaticInvoiceStore::new(Arc::clone(&kv_store)));
340+
handler.handle_event(static_invoice_event(true)).await.unwrap();
341+
let messages =
342+
AsyncPaymentsMessageHandler::release_pending_messages(&*node.channel_manager);
343+
assert_eq!(messages.len(), 1);
344+
assert!(matches!(messages[0].0, AsyncPaymentsMessage::StaticInvoicePersisted(_)));
345+
let store = StaticInvoiceStore::new(kv_store);
346+
assert!(store.handle_static_invoice_requested(&[1, 1, 1], 1).await.unwrap().is_some());
347+
}
348+
349+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
350+
async fn static_invoice_read_error_is_replayed() {
351+
let kv_store: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
352+
let (namespace, key) = StaticInvoiceStore::get_storage_location(0, &[1, 1, 1]);
353+
KVStore::write(
354+
&*kv_store,
355+
STATIC_INVOICE_STORE_PRIMARY_NAMESPACE,
356+
&namespace,
357+
&key,
358+
vec![0xff],
359+
)
360+
.await
361+
.unwrap();
362+
let (_node, handler, log_writer) = event_handler(StaticInvoiceStore::new(kv_store));
363+
assert!(handler.handle_event(static_invoice_event(false)).await.is_err());
364+
assert!(log_writer.logged.load(Ordering::Relaxed));
365+
}
167366

168367
#[tokio::test]
169368
async fn static_invoice_store_test() {

0 commit comments

Comments
 (0)