Skip to content

Commit f8d8708

Browse files
committed
Add async payments
1 parent e4a87d4 commit f8d8708

File tree

9 files changed

+292
-8
lines changed

9 files changed

+292
-8
lines changed

src/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1458,7 +1458,7 @@ fn build_with_store_internal(
14581458
Arc::clone(&channel_manager),
14591459
message_router,
14601460
Arc::clone(&channel_manager),
1461-
IgnoringMessageHandler {},
1461+
Arc::clone(&channel_manager),
14621462
IgnoringMessageHandler {},
14631463
IgnoringMessageHandler {},
14641464
));

src/event.rs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
77

8+
use crate::static_invoice_store::StaticInvoiceStore;
89
use crate::types::{CustomTlvRecord, DynStore, PaymentStore, Sweeper, Wallet};
910

1011
use crate::{
@@ -458,6 +459,7 @@ where
458459
runtime: Arc<Runtime>,
459460
logger: L,
460461
config: Arc<Config>,
462+
static_invoice_store: StaticInvoiceStore,
461463
}
462464

463465
impl<L: Deref + Clone + Sync + Send + 'static> EventHandler<L>
@@ -470,8 +472,9 @@ where
470472
channel_manager: Arc<ChannelManager>, connection_manager: Arc<ConnectionManager<L>>,
471473
output_sweeper: Arc<Sweeper>, network_graph: Arc<Graph>,
472474
liquidity_source: Option<Arc<LiquiditySource<Arc<Logger>>>>,
473-
payment_store: Arc<PaymentStore>, peer_store: Arc<PeerStore<L>>, runtime: Arc<Runtime>,
474-
logger: L, config: Arc<Config>,
475+
payment_store: Arc<PaymentStore>, peer_store: Arc<PeerStore<L>>,
476+
static_invoice_store: StaticInvoiceStore, runtime: Arc<Runtime>, logger: L,
477+
config: Arc<Config>,
475478
) -> Self {
476479
Self {
477480
event_queue,
@@ -487,6 +490,7 @@ where
487490
logger,
488491
runtime,
489492
config,
493+
static_invoice_store,
490494
}
491495
}
492496

@@ -1494,11 +1498,45 @@ where
14941498
LdkEvent::OnionMessagePeerConnected { .. } => {
14951499
debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted.");
14961500
},
1497-
LdkEvent::PersistStaticInvoice { .. } => {
1498-
debug_assert!(false, "We currently don't support static invoice persistence, so this event should never be emitted.");
1501+
1502+
LdkEvent::PersistStaticInvoice {
1503+
invoice,
1504+
invoice_slot,
1505+
recipient_id,
1506+
invoice_persisted_path,
1507+
} => {
1508+
match self
1509+
.static_invoice_store
1510+
.handle_persist_static_invoice(invoice, invoice_slot, recipient_id)
1511+
.await
1512+
{
1513+
Ok(_) => {},
1514+
Err(e) => {
1515+
log_error!(self.logger, "Failed to persist static invoice: {}", e);
1516+
},
1517+
};
1518+
1519+
self.channel_manager.static_invoice_persisted(invoice_persisted_path);
14991520
},
1500-
LdkEvent::StaticInvoiceRequested { .. } => {
1501-
debug_assert!(false, "We currently don't support static invoice persistence, so this event should never be emitted.");
1521+
LdkEvent::StaticInvoiceRequested { recipient_id, invoice_slot, reply_path } => {
1522+
let invoice = self
1523+
.static_invoice_store
1524+
.handle_static_invoice_requested(recipient_id, invoice_slot)
1525+
.await;
1526+
1527+
match invoice {
1528+
Ok(Some(invoice)) => {
1529+
if let Err(_) =
1530+
self.channel_manager.send_static_invoice(invoice, reply_path)
1531+
{
1532+
log_error!(self.logger, "Failed to send static invoice");
1533+
}
1534+
},
1535+
Ok(None) => {},
1536+
Err(e) => {
1537+
log_error!(self.logger, "Failed to retrieve static invoice: {}", e);
1538+
},
1539+
}
15021540
},
15031541
LdkEvent::FundingTransactionReadyForSigning { .. } => {
15041542
debug_assert!(false, "We currently don't support interactive-tx, so this event should never be emitted.");

src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ pub mod logger;
9494
mod message_handler;
9595
pub mod payment;
9696
mod peer_store;
97+
mod rate_limiter;
9798
mod runtime;
99+
mod static_invoice_store;
98100
mod tx_broadcaster;
99101
mod types;
100102
mod wallet;
@@ -150,6 +152,7 @@ pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId};
150152

151153
use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
152154

155+
use lightning::blinded_path::message::BlindedMessagePath;
153156
use lightning::chain::BestBlock;
154157
use lightning::events::bump_transaction::Wallet as LdkWallet;
155158
use lightning::impl_writeable_tlv_based;
@@ -170,6 +173,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
170173
use std::sync::{Arc, Mutex, RwLock};
171174
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
172175

176+
use crate::static_invoice_store::StaticInvoiceStore;
177+
173178
#[cfg(feature = "uniffi")]
174179
uniffi::include_scaffolding!("ldk_node");
175180

@@ -497,6 +502,8 @@ impl Node {
497502
Arc::clone(&self.logger),
498503
));
499504

505+
let static_invoice_store = StaticInvoiceStore::new(Arc::clone(&self.kv_store));
506+
500507
let event_handler = Arc::new(EventHandler::new(
501508
Arc::clone(&self.event_queue),
502509
Arc::clone(&self.wallet),
@@ -508,6 +515,7 @@ impl Node {
508515
self.liquidity_source.clone(),
509516
Arc::clone(&self.payment_store),
510517
Arc::clone(&self.peer_store),
518+
static_invoice_store,
511519
Arc::clone(&self.runtime),
512520
Arc::clone(&self.logger),
513521
Arc::clone(&self.config),
@@ -1461,6 +1469,22 @@ impl Node {
14611469
Error::PersistenceFailed
14621470
})
14631471
}
1472+
1473+
/// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build [`Offer`]s with a
1474+
/// static invoice server, so the server can serve [`StaticInvoice`]s to payers on our behalf when we're offline.
1475+
pub fn set_paths_to_static_invoice_server(
1476+
&self, paths: Vec<BlindedMessagePath>,
1477+
) -> Result<(), ()> {
1478+
self.channel_manager.set_paths_to_static_invoice_server(paths)
1479+
}
1480+
1481+
/// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively
1482+
/// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments.
1483+
pub fn blinded_paths_for_async_recipient(
1484+
&self, recipient_id: Vec<u8>, relative_expiry: Option<Duration>,
1485+
) -> Result<Vec<BlindedMessagePath>, ()> {
1486+
self.channel_manager.blinded_paths_for_async_recipient(recipient_id, relative_expiry)
1487+
}
14641488
}
14651489

14661490
impl Drop for Node {

src/payment/bolt12.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,4 +450,13 @@ impl Bolt12Payment {
450450

451451
Ok(maybe_wrap(refund))
452452
}
453+
454+
/// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. Will only return an offer if
455+
/// [`Builder::set_paths_to_static_invoice_server`] was called and we succeeded in interactively building a
456+
/// [`StaticInvoice`] with the static invoice server.
457+
///
458+
/// Useful for posting offers to receive payments later, such as posting an offer on a website.
459+
pub fn get_async_receive_offer(&self) -> Result<Offer, ()> {
460+
self.channel_manager.get_async_receive_offer()
461+
}
453462
}

src/rate_limiter.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use std::collections::HashMap;
2+
use std::time::{Duration, Instant};
3+
4+
pub(crate) struct RateLimiter {
5+
users: HashMap<Vec<u8>, Instant>,
6+
min_interval: Duration,
7+
}
8+
9+
impl RateLimiter {
10+
pub(crate) fn new(min_interval: Duration) -> Self {
11+
Self { users: HashMap::new(), min_interval }
12+
}
13+
14+
pub(crate) fn allow(&mut self, user_id: &[u8]) -> bool {
15+
let now = Instant::now();
16+
match self.users.get(user_id) {
17+
Some(&last) if now.duration_since(last) < self.min_interval => false,
18+
_ => {
19+
let new_user = self.users.insert(user_id.to_vec(), now).is_none();
20+
21+
// Each time a new user is added, we take the opportunity to clean up old rate limits.
22+
if new_user {
23+
self.garbage_collect();
24+
}
25+
true
26+
},
27+
}
28+
}
29+
30+
fn garbage_collect(&mut self) {
31+
let now = Instant::now();
32+
self.users.retain(|_, &mut last| now.duration_since(last) < self.min_interval);
33+
}
34+
}

src/static_invoice_store.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use crate::{hex_utils, rate_limiter::RateLimiter, types::DynStore};
2+
use bitcoin::hashes::{sha256, Hash};
3+
use lightning::{offers::static_invoice::StaticInvoice, util::ser::Writeable};
4+
use std::{
5+
sync::{Arc, Mutex},
6+
time::Duration,
7+
};
8+
9+
pub(crate) struct StaticInvoiceStore {
10+
kv_store: Arc<DynStore>,
11+
request_rate_limiter: Mutex<RateLimiter>,
12+
persist_rate_limiter: Mutex<RateLimiter>,
13+
}
14+
15+
impl StaticInvoiceStore {
16+
// Static invoices are stored at "static_invoices/<sha256(recipient_id)>/<invoice_slot>".
17+
//
18+
// Example:
19+
// static_invoices/039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81/001f
20+
const PRIMARY_NAMESPACE: &str = "static_invoices";
21+
22+
pub(crate) fn new(kv_store: Arc<DynStore>) -> Self {
23+
Self {
24+
kv_store,
25+
request_rate_limiter: Mutex::new(RateLimiter::new(Duration::from_millis(100))),
26+
persist_rate_limiter: Mutex::new(RateLimiter::new(Duration::from_millis(100))),
27+
}
28+
}
29+
30+
fn check_rate_limit(
31+
limiter: &Mutex<RateLimiter>, recipient_id: &[u8],
32+
) -> Result<(), lightning::io::Error> {
33+
let mut limiter = limiter.lock().unwrap();
34+
if !limiter.allow(recipient_id) {
35+
Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, "Rate limit exceeded"))
36+
} else {
37+
Ok(())
38+
}
39+
}
40+
41+
pub(crate) async fn handle_static_invoice_requested(
42+
&self, recipient_id: Vec<u8>, invoice_slot: u16,
43+
) -> Result<Option<StaticInvoice>, lightning::io::Error> {
44+
Self::check_rate_limit(&self.request_rate_limiter, &recipient_id)?;
45+
46+
let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, recipient_id);
47+
48+
self.kv_store.read(Self::PRIMARY_NAMESPACE, &secondary_namespace, &key).and_then(|data| {
49+
data.try_into().map(Some).map_err(|e| {
50+
lightning::io::Error::new(
51+
lightning::io::ErrorKind::InvalidData,
52+
format!("Failed to parse static invoice: {:?}", e),
53+
)
54+
})
55+
})
56+
}
57+
58+
pub(crate) async fn handle_persist_static_invoice(
59+
&self, invoice: StaticInvoice, invoice_slot: u16, recipient_id: Vec<u8>,
60+
) -> Result<(), lightning::io::Error> {
61+
Self::check_rate_limit(&self.persist_rate_limiter, &recipient_id)?;
62+
63+
let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, recipient_id);
64+
65+
let mut buf = Vec::new();
66+
invoice.write(&mut buf)?;
67+
68+
self.kv_store.write(Self::PRIMARY_NAMESPACE, &secondary_namespace, &key, buf)
69+
}
70+
71+
fn get_storage_location(invoice_slot: u16, recipient_id: Vec<u8>) -> (String, String) {
72+
let hash = sha256::Hash::hash(&recipient_id).to_byte_array();
73+
let secondary_namespace = hex_utils::to_string(&hash);
74+
75+
let key = hex_utils::to_string(&invoice_slot.to_be_bytes());
76+
(secondary_namespace, key)
77+
}
78+
}

src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse
123123
Arc<ChannelManager>,
124124
Arc<MessageRouter>,
125125
Arc<ChannelManager>,
126-
IgnoringMessageHandler,
126+
Arc<ChannelManager>,
127127
IgnoringMessageHandler,
128128
IgnoringMessageHandler,
129129
>;

tests/common/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,13 @@ pub(crate) fn setup_two_nodes(
288288
) -> (TestNode, TestNode) {
289289
println!("== Node A ==");
290290
let config_a = random_config(anchor_channels);
291+
println!("Node A storage path: {}", config_a.node_config.storage_dir_path.clone());
292+
291293
let node_a = setup_node(chain_source, config_a, None);
292294

293295
println!("\n== Node B ==");
294296
let mut config_b = random_config(anchor_channels);
297+
println!("Node B storage path: {}", config_b.node_config.storage_dir_path.clone());
295298
if allow_0conf {
296299
config_b.node_config.trusted_peers_0conf.push(node_a.node_id());
297300
}

0 commit comments

Comments
 (0)