Skip to content

Commit eff64aa

Browse files
Support phantom payment receive in ChannelManager, with invoice util
See get_phantom_scid and invoice util's create_phantom_invoice for more info
1 parent d7d13b7 commit eff64aa

File tree

5 files changed

+356
-23
lines changed

5 files changed

+356
-23
lines changed

lightning-invoice/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,20 @@ pub enum CreationError {
14941494

14951495
/// The supplied millisatoshi amount was greater than the total bitcoin supply.
14961496
InvalidAmount,
1497+
1498+
/// Route hints were required for this invoice and were missing. Applies to
1499+
/// [phantom invoices].
1500+
///
1501+
/// [phantom invoices]: crate::utils::create_phantom_invoice
1502+
MissingRouteHints,
1503+
1504+
/// For [phantom invoices], short channel ids for phantom route hints are supplied and used to
1505+
/// retrieve the phantom's private key. This can fail if an scid was not previously retrieved from
1506+
/// [`ChannelManager::get_phantom_scid`].
1507+
///
1508+
/// [phantom invoices]: crate::utils::create_phantom_invoice
1509+
/// [`ChannelManager::get_phantom_scid`]: lightning::ln::channelmanager::ChannelManager::get_phantom_scid
1510+
InvalidPhantomScid,
14971511
}
14981512

14991513
impl Display for CreationError {
@@ -1504,6 +1518,8 @@ impl Display for CreationError {
15041518
CreationError::TimestampOutOfBounds => f.write_str("The unix timestamp of the supplied date is <0 or can't be represented as `SystemTime`"),
15051519
CreationError::ExpiryTimeOutOfBounds => f.write_str("The supplied expiry time could cause an overflow if added to a `PositiveTimestamp`"),
15061520
CreationError::InvalidAmount => f.write_str("The supplied millisatoshi amount was greater than the total bitcoin supply"),
1521+
CreationError::MissingRouteHints => f.write_str("The invoice required route hints and they weren't provided"),
1522+
CreationError::InvalidPhantomScid => f.write_str("Failed to retrieve the phantom secret with the supplied phantom scid"),
15071523
}
15081524
}
15091525
}

lightning-invoice/src/utils.rs

Lines changed: 245 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,127 @@ use payment::{Payer, Router};
55

66
use bech32::ToBase32;
77
use bitcoin_hashes::Hash;
8+
use bitcoin_hashes::sha256::Hash as Sha256;
89
use crate::prelude::*;
910
use lightning::chain;
1011
use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator};
1112
use lightning::chain::keysinterface::{Sign, KeysInterface};
1213
use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret};
13-
use lightning::ln::channelmanager::{ChannelDetails, ChannelManager, PaymentId, PaymentSendFailure, MIN_FINAL_CLTV_EXPIRY};
14+
use lightning::ln::channelmanager::{ChannelDetails, ChannelManager, PaymentId, PaymentSendFailure, MIN_FINAL_CLTV_EXPIRY, MIN_CLTV_EXPIRY_DELTA};
1415
use lightning::ln::msgs::LightningError;
1516
use lightning::routing::scoring::Score;
1617
use lightning::routing::network_graph::{NetworkGraph, RoutingFees};
1718
use lightning::routing::router::{Route, RouteHint, RouteHintHop, RouteParameters, find_route};
1819
use lightning::util::logger::Logger;
20+
use secp256k1::Secp256k1;
1921
use secp256k1::key::PublicKey;
2022
use core::convert::TryInto;
2123
use core::ops::Deref;
2224
use core::time::Duration;
2325

26+
#[cfg(feature = "std")]
27+
/// Utility to create an invoice that can be paid to one of multiple nodes, or a "phantom invoice."
28+
/// This works because the invoice officially pays to a fake node (the "phantom"). Useful for
29+
/// load-balancing payments between multiple nodes.
30+
///
31+
/// `channels` parameter:
32+
/// * Contains a list of tuples of `(phantom_node_scid, real_node_pubkey,
33+
/// real_channel_details)`, where `phantom_node_scid` is unique to each `channels` entry
34+
/// * `phantom_node_scid`s are retrieved from [`ChannelManager::get_phantom_scid`]
35+
/// * `real_channel_details` are retrieved from [`ChannelManager::list_channels`]
36+
/// * Contains channel info for all nodes participating in the phantom invoice
37+
/// * For increased privacy, it is ideal but not required to generate new `phantom_node_scid`s for
38+
/// each new invoice
39+
/// * It is fine to cache this information and reuse it across invoices, as long as you periodically
40+
/// check that no provided real channels have become unavailable
41+
/// * Note that including too many channels here could result in making the invoice too long for QR
42+
/// code scanning
43+
///
44+
/// `payment_hash` and `payment_secret` come from [`ChannelManager::create_inbound_payment`] or
45+
/// [`ChannelManager::create_inbound_payment_for_hash`].
46+
///
47+
/// See [`KeysInterface::get_phantom_secret`] for more requirements on supporting phantom node
48+
/// payments.
49+
///
50+
/// [`KeysInterface::get_phantom_secret`]: lightning::chain::keysinterface::KeysInterface::get_phantom_secret
51+
pub fn create_phantom_invoice<Signer: Sign, K: Deref>(
52+
amt_msat: Option<u64>, description: String, payment_hash: PaymentHash, payment_secret:
53+
PaymentSecret, channels: &[(u64, PublicKey, &ChannelDetails)], keys_manager: K, network: Currency
54+
) -> Result<Invoice, SignOrCreationError<()>> where K::Target: KeysInterface {
55+
if channels.len() == 0 {
56+
return Err(SignOrCreationError::CreationError(CreationError::MissingRouteHints))
57+
}
58+
let phantom_secret = match keys_manager.get_phantom_secret(channels[0].0) {
59+
Ok(s) => s,
60+
Err(()) => return Err(SignOrCreationError::CreationError(CreationError::InvalidPhantomScid))
61+
};
62+
let mut route_hints = vec![];
63+
for (phantom_scid, real_node_pubkey, channel) in channels {
64+
let short_channel_id = match channel.short_channel_id {
65+
Some(id) => id,
66+
None => continue,
67+
};
68+
let forwarding_info = match &channel.counterparty.forwarding_info {
69+
Some(info) => info.clone(),
70+
None => continue,
71+
};
72+
route_hints.push(RouteHint(vec![
73+
RouteHintHop {
74+
src_node_id: channel.counterparty.node_id,
75+
short_channel_id,
76+
fees: RoutingFees {
77+
base_msat: forwarding_info.fee_base_msat,
78+
proportional_millionths: forwarding_info.fee_proportional_millionths,
79+
},
80+
cltv_expiry_delta: forwarding_info.cltv_expiry_delta,
81+
htlc_minimum_msat: None,
82+
htlc_maximum_msat: None,
83+
},
84+
RouteHintHop {
85+
src_node_id: *real_node_pubkey,
86+
short_channel_id: *phantom_scid,
87+
fees: RoutingFees {
88+
base_msat: 0,
89+
proportional_millionths: 0,
90+
},
91+
cltv_expiry_delta: MIN_CLTV_EXPIRY_DELTA,
92+
htlc_minimum_msat: None,
93+
htlc_maximum_msat: None,
94+
}])
95+
);
96+
}
97+
let phantom_pubkey = PublicKey::from_secret_key(&Secp256k1::new(), &phantom_secret);
98+
let mut invoice = InvoiceBuilder::new(network)
99+
.description(description)
100+
.current_timestamp()
101+
.payee_pub_key(phantom_pubkey)
102+
.payment_hash(Hash::from_slice(&payment_hash.0).unwrap())
103+
.payment_secret(payment_secret)
104+
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
105+
if let Some(amt) = amt_msat {
106+
invoice = invoice.amount_milli_satoshis(amt);
107+
}
108+
for hint in route_hints {
109+
invoice = invoice.private_route(hint);
110+
}
111+
112+
let raw_invoice = match invoice.build_raw() {
113+
Ok(inv) => inv,
114+
Err(e) => return Err(SignOrCreationError::CreationError(e))
115+
};
116+
let hrp_str = raw_invoice.hrp.to_string();
117+
let hrp_bytes = hrp_str.as_bytes();
118+
let data_without_signature = raw_invoice.data.to_base32();
119+
let invoice_preimage = RawInvoice::construct_invoice_preimage(hrp_bytes, &data_without_signature);
120+
let secp_ctx = Secp256k1::new();
121+
let invoice_preimage_msg = secp256k1::Message::from_slice(&Sha256::hash(&invoice_preimage)).unwrap();
122+
let signed_raw_invoice = raw_invoice.sign(|_| Ok(secp_ctx.sign_recoverable(&invoice_preimage_msg, &phantom_secret)));
123+
match signed_raw_invoice {
124+
Ok(inv) => Ok(Invoice::from_signed(inv).unwrap()),
125+
Err(e) => Err(SignOrCreationError::SignError(e))
126+
}
127+
}
128+
24129
#[cfg(feature = "std")]
25130
/// Utility to construct an invoice. Generally, unless you want to do something like a custom
26131
/// cltv_expiry, this is what you should be using to create an invoice. The reason being, this
@@ -193,15 +298,20 @@ where
193298
mod test {
194299
use core::time::Duration;
195300
use {Currency, Description, InvoiceDescription};
196-
use lightning::ln::PaymentHash;
197-
use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY;
301+
use bitcoin_hashes::Hash;
302+
use bitcoin_hashes::sha256::Hash as Sha256;
303+
use lightning::chain::keysinterface::KeysManager;
304+
use lightning::ln::{PaymentPreimage, PaymentHash};
305+
use lightning::ln::channelmanager::{ChannelDetails, MIN_FINAL_CLTV_EXPIRY};
198306
use lightning::ln::functional_test_utils::*;
199307
use lightning::ln::features::InitFeatures;
200308
use lightning::ln::msgs::ChannelMessageHandler;
201309
use lightning::routing::router::{Payee, RouteParameters, find_route};
202-
use lightning::util::events::MessageSendEventsProvider;
310+
use lightning::util::enforcing_trait_impls::EnforcingSigner;
311+
use lightning::util::events::{MessageSendEvent, MessageSendEventsProvider, Event, PaymentPurpose};
203312
use lightning::util::test_utils;
204313
use utils::create_invoice_from_channelmanager_and_duration_since_epoch;
314+
use secp256k1::PublicKey;
205315

206316
#[test]
207317
fn test_from_channelmanager() {
@@ -255,4 +365,135 @@ mod test {
255365
let events = nodes[1].node.get_and_clear_pending_msg_events();
256366
assert_eq!(events.len(), 2);
257367
}
368+
369+
#[test]
370+
fn test_multi_node_receive() {
371+
#[cfg(feature = "std")]
372+
do_test_multi_node_receive(true);
373+
#[cfg(feature = "std")]
374+
do_test_multi_node_receive(false);
375+
}
376+
377+
#[cfg(feature = "std")]
378+
fn do_test_multi_node_receive(user_generated_pmt_hash: bool) {
379+
let mut chanmon_cfgs = create_chanmon_cfgs(3);
380+
let seed_1 = [42 as u8; 32];
381+
let seed_2 = [43 as u8; 32];
382+
let cross_node_seed = [44 as u8; 32];
383+
chanmon_cfgs[1].keys_manager.backing = KeysManager::new_multi_receive(&seed_1, 43, 44, &cross_node_seed);
384+
chanmon_cfgs[2].keys_manager.backing = KeysManager::new_multi_receive(&seed_2, 43, 44, &cross_node_seed);
385+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
386+
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
387+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
388+
let chan_0_1 = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, 10001, InitFeatures::known(), InitFeatures::known());
389+
nodes[0].node.handle_channel_update(&nodes[1].node.get_our_node_id(), &chan_0_1.1);
390+
nodes[1].node.handle_channel_update(&nodes[0].node.get_our_node_id(), &chan_0_1.0);
391+
let chan_0_2 = create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 100000, 10001, InitFeatures::known(), InitFeatures::known());
392+
nodes[0].node.handle_channel_update(&nodes[2].node.get_our_node_id(), &chan_0_2.1);
393+
nodes[2].node.handle_channel_update(&nodes[0].node.get_our_node_id(), &chan_0_2.0);
394+
395+
let payment_amt = 10_000;
396+
let (payment_preimage, payment_hash, payment_secret) = {
397+
if user_generated_pmt_hash {
398+
let payment_preimage = PaymentPreimage([1; 32]);
399+
let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).into_inner());
400+
let payment_secret = nodes[1].node.create_inbound_payment_for_hash(payment_hash, Some(payment_amt), 3600).unwrap();
401+
(payment_preimage, payment_hash, payment_secret)
402+
} else {
403+
let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(payment_amt), 3600).unwrap();
404+
let payment_preimage = nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap();
405+
(payment_preimage, payment_hash, payment_secret)
406+
}
407+
};
408+
let channels_1 = nodes[1].node.list_channels();
409+
let channels_2 = nodes[2].node.list_channels();
410+
let mut route_hints = channels_1.iter().map(|e| (nodes[1].node.get_phantom_scid(), nodes[1].node.get_our_node_id(), e)).collect::<Vec<(u64, PublicKey, &ChannelDetails)>>();
411+
for channel in channels_2.iter() {
412+
route_hints.push((nodes[2].node.get_phantom_scid(), nodes[2].node.get_our_node_id(), &channel));
413+
}
414+
let invoice = ::utils::create_phantom_invoice::<EnforcingSigner, &test_utils::TestKeysInterface>(Some(payment_amt), "test".to_string(), payment_hash, payment_secret, &route_hints, &nodes[1].keys_manager, Currency::BitcoinTestnet).unwrap();
415+
416+
assert_eq!(invoice.min_final_cltv_expiry(), MIN_FINAL_CLTV_EXPIRY as u64);
417+
assert_eq!(invoice.description(), InvoiceDescription::Direct(&Description("test".to_string())));
418+
assert_eq!(invoice.route_hints().len(), 2);
419+
assert!(!invoice.features().unwrap().supports_basic_mpp());
420+
421+
let payee = Payee::from_node_id(invoice.recover_payee_pub_key())
422+
.with_features(invoice.features().unwrap().clone())
423+
.with_route_hints(invoice.route_hints());
424+
let params = RouteParameters {
425+
payee,
426+
final_value_msat: invoice.amount_milli_satoshis().unwrap(),
427+
final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32,
428+
};
429+
let first_hops = nodes[0].node.list_usable_channels();
430+
let network_graph = node_cfgs[0].network_graph;
431+
let logger = test_utils::TestLogger::new();
432+
let scorer = test_utils::TestScorer::with_fixed_penalty(0);
433+
let route = find_route(
434+
&nodes[0].node.get_our_node_id(), &params, network_graph,
435+
Some(&first_hops.iter().collect::<Vec<_>>()), &logger, &scorer,
436+
).unwrap();
437+
let (payment_event, fwd_idx) = {
438+
let mut payment_hash = PaymentHash([0; 32]);
439+
payment_hash.0.copy_from_slice(&invoice.payment_hash().as_ref()[0..32]);
440+
nodes[0].node.send_payment(&route, payment_hash, &Some(invoice.payment_secret().clone())).unwrap();
441+
let mut added_monitors = nodes[0].chain_monitor.added_monitors.lock().unwrap();
442+
assert_eq!(added_monitors.len(), 1);
443+
added_monitors.clear();
444+
445+
let mut events = nodes[0].node.get_and_clear_pending_msg_events();
446+
assert_eq!(events.len(), 1);
447+
let fwd_idx = match events[0] {
448+
MessageSendEvent::UpdateHTLCs { node_id, .. } => {
449+
if node_id == nodes[1].node.get_our_node_id() {
450+
1
451+
} else { 2 }
452+
},
453+
_ => panic!("Unexpected event")
454+
};
455+
(SendEvent::from_event(events.remove(0)), fwd_idx)
456+
};
457+
nodes[fwd_idx].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &payment_event.msgs[0]);
458+
commitment_signed_dance!(nodes[fwd_idx], nodes[0], &payment_event.commitment_msg, false, true);
459+
expect_pending_htlcs_forwardable!(nodes[fwd_idx]);
460+
let payment_preimage_opt = if user_generated_pmt_hash { None } else { Some(payment_preimage) };
461+
let events = nodes[fwd_idx].node.get_and_clear_pending_events();
462+
assert_eq!(events.len(), 2);
463+
match events[0] {
464+
Event::PendingHTLCsForwardable { .. } => { },
465+
_ => panic!("Unexpected event"),
466+
}
467+
match events[1] {
468+
Event::PaymentReceived { payment_hash: ref hash, ref purpose, amt } => {
469+
assert_eq!(*hash, payment_hash);
470+
assert_eq!(amt, payment_amt);
471+
match purpose {
472+
PaymentPurpose::InvoicePayment { payment_preimage, payment_secret: secret, .. } => {
473+
assert_eq!(*payment_preimage, payment_preimage_opt);
474+
assert_eq!(*secret, payment_secret);
475+
},
476+
_ => panic!("Unexpected payment purpose"),
477+
}
478+
},
479+
_ => panic!("Unexpected event"),
480+
}
481+
do_claim_payment_along_route(&nodes[0], &vec!(&vec!(&nodes[fwd_idx])[..]), false, payment_preimage);
482+
let events = nodes[0].node.get_and_clear_pending_events();
483+
assert_eq!(events.len(), 2);
484+
match events[0] {
485+
Event::PaymentSent { payment_preimage: ref ev_preimage, payment_hash: ref ev_hash, ref fee_paid_msat, .. } => {
486+
assert_eq!(payment_preimage, *ev_preimage);
487+
assert_eq!(payment_hash, *ev_hash);
488+
assert_eq!(fee_paid_msat, &Some(0));
489+
},
490+
_ => panic!("Unexpected event")
491+
}
492+
match events[1] {
493+
Event::PaymentPathSuccessful { payment_hash: hash, .. } => {
494+
assert_eq!(hash, Some(payment_hash));
495+
},
496+
_ => panic!("Unexpected event")
497+
}
498+
}
258499
}

lightning/src/chain/keysinterface.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,10 +407,34 @@ pub trait KeysInterface {
407407

408408
/// Get secret key material as bytes for use in encrypting and decrypting inbound payment data.
409409
///
410+
/// If the implementor of this trait supports [phantom node payments], then every node that is
411+
/// intended to be included in the phantom invoice(s) route hints must return the same value from
412+
/// this method.
413+
///
410414
/// This method must return the same value each time it is called.
415+
///
416+
///[phantom node payments]: crate::ln::channelmanager::ChannelManager::get_phantom_scid
411417
fn get_inbound_payment_key_material(&self) -> KeyMaterial;
412418

413-
/// Get a secret key for use in receiving phantom node payments.
419+
/// Get the phantom node secret key (aka node_id or network_key) for use in receiving a [phantom
420+
/// node payment]. This secret key is used to sign phantom invoices.
421+
///
422+
/// Rules:
423+
/// * when a phantom invoice is generated with phantom scids from multiple nodes, this method must
424+
/// return the same key across each node
425+
/// * if a new invoice is generated with new phantom scids, a new key may be returned (but still
426+
/// must be the same across each node)
427+
/// * if a new invoice is generated that contains old phantom scids, the same key must be returned
428+
/// across nodes as was used for the old invoice
429+
///
430+
/// Note that if you are using [`KeysManager`], every LDK node must use
431+
/// [`KeysManager::new_multi_receive`] to initialize its `KeysManager` or phantom payments will
432+
/// fail to be received.
433+
///
434+
/// See [`KeysInterface::get_inbound_payment_key_material`] for further requirements on nodes
435+
/// supporting phantom node payments.
436+
///
437+
///[phantom node payment]: crate::ln::channelmanager::ChannelManager::get_phantom_scid
414438
fn get_phantom_secret(&self, scid: u64) -> Result<SecretKey, ()>;
415439
}
416440

0 commit comments

Comments
 (0)