Skip to content

Commit 411d827

Browse files
committed
Create normal/phantom invoice with description hash
1 parent 699ac83 commit 411d827

File tree

1 file changed

+299
-36
lines changed

1 file changed

+299
-36
lines changed

lightning-invoice/src/utils.rs

Lines changed: 299 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
use {CreationError, Currency, DEFAULT_EXPIRY_TIME, Invoice, InvoiceBuilder, SignOrCreationError};
44
use payment::{Payer, Router};
55

6+
use crate::{prelude::*, Description, InvoiceDescription};
67
use bech32::ToBase32;
78
use bitcoin_hashes::{Hash, sha256};
8-
use crate::prelude::*;
99
use lightning::chain;
1010
use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator};
1111
use lightning::chain::keysinterface::{Recipient, KeysInterface, Sign};
@@ -50,18 +50,100 @@ use sync::Mutex;
5050
/// [`ChannelManager::get_phantom_route_hints`]: lightning::ln::channelmanager::ChannelManager::get_phantom_route_hints
5151
/// [`PhantomRouteHints::channels`]: lightning::ln::channelmanager::PhantomRouteHints::channels
5252
pub fn create_phantom_invoice<Signer: Sign, K: Deref>(
53-
amt_msat: Option<u64>, description: String, payment_hash: PaymentHash, payment_secret:
54-
PaymentSecret, phantom_route_hints: Vec<PhantomRouteHints>, keys_manager: K, network: Currency
55-
) -> Result<Invoice, SignOrCreationError<()>> where K::Target: KeysInterface {
56-
if phantom_route_hints.len() == 0 {
57-
return Err(SignOrCreationError::CreationError(CreationError::MissingRouteHints))
58-
}
59-
let mut invoice = InvoiceBuilder::new(network)
60-
.description(description)
61-
.current_timestamp()
62-
.payment_hash(Hash::from_slice(&payment_hash.0).unwrap())
63-
.payment_secret(payment_secret)
64-
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
53+
amt_msat: Option<u64>,
54+
description: String,
55+
payment_hash: PaymentHash,
56+
payment_secret: PaymentSecret,
57+
phantom_route_hints: Vec<PhantomRouteHints>,
58+
keys_manager: K,
59+
network: Currency,
60+
) -> Result<Invoice, SignOrCreationError<()>>
61+
where
62+
K::Target: KeysInterface,
63+
{
64+
let description = Description::new(description).map_err(SignOrCreationError::CreationError)?;
65+
let description = InvoiceDescription::Direct(&description,);
66+
_create_phantom_invoice::<Signer, K>(
67+
amt_msat, description, payment_hash, payment_secret, phantom_route_hints, keys_manager, network,
68+
)
69+
}
70+
71+
#[cfg(feature = "std")]
72+
/// Utility to create an invoice that can be paid to one of multiple nodes, or a "phantom invoice."
73+
/// See [`PhantomKeysManager`] for more information on phantom node payments.
74+
///
75+
/// `phantom_route_hints` parameter:
76+
/// * Contains channel info for all nodes participating in the phantom invoice
77+
/// * Entries are retrieved from a call to [`ChannelManager::get_phantom_route_hints`] on each
78+
/// participating node
79+
/// * It is fine to cache `phantom_route_hints` and reuse it across invoices, as long as the data is
80+
/// updated when a channel becomes disabled or closes
81+
/// * Note that if too many channels are included in [`PhantomRouteHints::channels`], the invoice
82+
/// may be too long for QR code scanning. To fix this, `PhantomRouteHints::channels` may be pared
83+
/// down
84+
///
85+
/// `description` will be SHA-256 hashed and transformed in `description_hash` on the invoice
86+
///
87+
/// `payment_hash` and `payment_secret` come from [`ChannelManager::create_inbound_payment`] or
88+
/// [`ChannelManager::create_inbound_payment_for_hash`]. These values can be retrieved from any
89+
/// participating node.
90+
///
91+
/// Note that the provided `keys_manager`'s `KeysInterface` implementation must support phantom
92+
/// invoices in its `sign_invoice` implementation ([`PhantomKeysManager`] satisfies this
93+
/// requirement).
94+
///
95+
/// [`PhantomKeysManager`]: lightning::chain::keysinterface::PhantomKeysManager
96+
/// [`ChannelManager::get_phantom_route_hints`]: lightning::ln::channelmanager::ChannelManager::get_phantom_route_hints
97+
/// [`PhantomRouteHints::channels`]: lightning::ln::channelmanager::PhantomRouteHints::channels
98+
pub fn create_phantom_invoice_with_description_hash<Signer: Sign, K: Deref>(
99+
amt_msat: Option<u64>,
100+
description: String,
101+
payment_hash: PaymentHash,
102+
payment_secret: PaymentSecret,
103+
phantom_route_hints: Vec<PhantomRouteHints>,
104+
keys_manager: K,
105+
network: Currency,
106+
) -> Result<Invoice, SignOrCreationError<()>>
107+
where
108+
K::Target: KeysInterface,
109+
{
110+
_create_phantom_invoice::<Signer, K>(
111+
amt_msat,
112+
InvoiceDescription::Hash(&crate::Sha256(Hash::hash(description.as_bytes()))),
113+
payment_hash, payment_secret, phantom_route_hints, keys_manager, network,
114+
)
115+
}
116+
117+
#[cfg(feature = "std")]
118+
fn _create_phantom_invoice<Signer: Sign, K: Deref>(
119+
amt_msat: Option<u64>,
120+
description: InvoiceDescription,
121+
payment_hash: PaymentHash,
122+
payment_secret: PaymentSecret,
123+
phantom_route_hints: Vec<PhantomRouteHints>,
124+
keys_manager: K,
125+
network: Currency,
126+
) -> Result<Invoice, SignOrCreationError<()>>
127+
where
128+
K::Target: KeysInterface,
129+
{
130+
if phantom_route_hints.len() == 0 {
131+
return Err(SignOrCreationError::CreationError(
132+
CreationError::MissingRouteHints,
133+
));
134+
}
135+
let invoice = match description {
136+
InvoiceDescription::Direct(description) => {
137+
InvoiceBuilder::new(network).description(description.0.clone())
138+
}
139+
InvoiceDescription::Hash(hash) => InvoiceBuilder::new(network).description_hash(hash.0),
140+
};
141+
142+
let mut invoice = invoice
143+
.current_timestamp()
144+
.payment_hash(Hash::from_slice(&payment_hash.0).unwrap())
145+
.payment_secret(payment_secret)
146+
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
65147
if let Some(amt) = amt_msat {
66148
invoice = invoice.amount_milli_satoshis(amt);
67149
}
@@ -126,15 +208,81 @@ where
126208
let duration = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
127209
.expect("for the foreseeable future this shouldn't happen");
128210
create_invoice_from_channelmanager_and_duration_since_epoch(
129-
channelmanager,
130-
keys_manager,
131-
network,
132-
amt_msat,
133-
description,
134-
duration
211+
channelmanager, keys_manager, network, amt_msat, description, duration
135212
)
136213
}
137214

215+
#[cfg(feature = "std")]
216+
/// Utility to construct an invoice. Generally, unless you want to do something like a custom
217+
/// cltv_expiry, this is what you should be using to create an invoice. The reason being, this
218+
/// method stores the invoice's payment secret and preimage in `ChannelManager`, so (a) the user
219+
/// doesn't have to store preimage/payment secret information and (b) `ChannelManager` can verify
220+
/// that the payment secret is valid when the invoice is paid.
221+
/// Use this variant if you want to hash the description and pass a `payment_hash` instead
222+
pub fn create_invoice_from_channelmanager_with_description_hash<
223+
Signer: Sign,
224+
M: Deref,
225+
T: Deref,
226+
K: Deref,
227+
F: Deref,
228+
L: Deref,
229+
>(
230+
channelmanager: &ChannelManager<Signer, M, T, K, F, L>,
231+
keys_manager: K,
232+
network: Currency,
233+
amt_msat: Option<u64>,
234+
description: String,
235+
) -> Result<Invoice, SignOrCreationError<()>>
236+
where
237+
M::Target: chain::Watch<Signer>,
238+
T::Target: BroadcasterInterface,
239+
K::Target: KeysInterface<Signer = Signer>,
240+
F::Target: FeeEstimator,
241+
L::Target: Logger,
242+
{
243+
use std::time::SystemTime;
244+
245+
let duration = SystemTime::now()
246+
.duration_since(SystemTime::UNIX_EPOCH)
247+
.expect("for the foreseeable future this shouldn't happen");
248+
249+
create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
250+
channelmanager, keys_manager, network, amt_msat, description, duration,
251+
)
252+
}
253+
254+
/// See [`create_invoice_from_channelmanager_with_description_hash`]
255+
/// This version can be used in a `no_std` environment, where [`std::time::SystemTime`] is not
256+
/// available and the current time is supplied by the caller.
257+
pub fn create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch<
258+
Signer: Sign,
259+
M: Deref,
260+
T: Deref,
261+
K: Deref,
262+
F: Deref,
263+
L: Deref,
264+
>(
265+
channelmanager: &ChannelManager<Signer, M, T, K, F, L>,
266+
keys_manager: K,
267+
network: Currency,
268+
amt_msat: Option<u64>,
269+
description: String,
270+
duration_since_epoch: Duration,
271+
) -> Result<Invoice, SignOrCreationError<()>>
272+
where
273+
M::Target: chain::Watch<Signer>,
274+
T::Target: BroadcasterInterface,
275+
K::Target: KeysInterface<Signer = Signer>,
276+
F::Target: FeeEstimator,
277+
L::Target: Logger,
278+
{
279+
_create_invoice_from_channelmanager_and_duration_since_epoch(
280+
channelmanager, keys_manager, network, amt_msat,
281+
InvoiceDescription::Hash(&crate::Sha256(Hash::hash(description.as_bytes()))),
282+
duration_since_epoch,
283+
)
284+
}
285+
138286
/// See [`create_invoice_from_channelmanager`]
139287
/// This version can be used in a `no_std` environment, where [`std::time::SystemTime`] is not
140288
/// available and the current time is supplied by the caller.
@@ -148,26 +296,64 @@ where
148296
K::Target: KeysInterface<Signer = Signer>,
149297
F::Target: FeeEstimator,
150298
L::Target: Logger,
299+
{
300+
_create_invoice_from_channelmanager_and_duration_since_epoch(
301+
channelmanager, keys_manager, network, amt_msat,
302+
InvoiceDescription::Direct(
303+
&Description::new(description).map_err(SignOrCreationError::CreationError)?,
304+
),
305+
duration_since_epoch,
306+
)
307+
}
308+
309+
fn _create_invoice_from_channelmanager_and_duration_since_epoch<
310+
Signer: Sign,
311+
M: Deref,
312+
T: Deref,
313+
K: Deref,
314+
F: Deref,
315+
L: Deref,
316+
>(
317+
channelmanager: &ChannelManager<Signer, M, T, K, F, L>,
318+
keys_manager: K,
319+
network: Currency,
320+
amt_msat: Option<u64>,
321+
description: InvoiceDescription,
322+
duration_since_epoch: Duration,
323+
) -> Result<Invoice, SignOrCreationError<()>>
324+
where
325+
M::Target: chain::Watch<Signer>,
326+
T::Target: BroadcasterInterface,
327+
K::Target: KeysInterface<Signer = Signer>,
328+
F::Target: FeeEstimator,
329+
L::Target: Logger,
151330
{
152331
let route_hints = filter_channels(channelmanager.list_usable_channels(), amt_msat);
153332

154-
// `create_inbound_payment` only returns an error if the amount is greater than the total bitcoin
155-
// supply.
156-
let (payment_hash, payment_secret) = channelmanager.create_inbound_payment(
157-
amt_msat, DEFAULT_EXPIRY_TIME.try_into().unwrap())
158-
.map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?;
159-
let our_node_pubkey = channelmanager.get_our_node_id();
160-
let mut invoice = InvoiceBuilder::new(network)
161-
.description(description)
162-
.duration_since_epoch(duration_since_epoch)
163-
.payee_pub_key(our_node_pubkey)
164-
.payment_hash(Hash::from_slice(&payment_hash.0).unwrap())
165-
.payment_secret(payment_secret)
166-
.basic_mpp()
167-
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
168-
if let Some(amt) = amt_msat {
169-
invoice = invoice.amount_milli_satoshis(amt);
170-
}
333+
// `create_inbound_payment` only returns an error if the amount is greater than the total bitcoin
334+
// supply.
335+
let (payment_hash, payment_secret) = channelmanager
336+
.create_inbound_payment(amt_msat, DEFAULT_EXPIRY_TIME.try_into().unwrap())
337+
.map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?;
338+
let our_node_pubkey = channelmanager.get_our_node_id();
339+
340+
let invoice = match description {
341+
InvoiceDescription::Direct(description) => {
342+
InvoiceBuilder::new(network).description(description.0.clone())
343+
}
344+
InvoiceDescription::Hash(hash) => InvoiceBuilder::new(network).description_hash(hash.0),
345+
};
346+
347+
let mut invoice = invoice
348+
.duration_since_epoch(duration_since_epoch)
349+
.payee_pub_key(our_node_pubkey)
350+
.payment_hash(Hash::from_slice(&payment_hash.0).unwrap())
351+
.payment_secret(payment_secret)
352+
.basic_mpp()
353+
.min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
354+
if let Some(amt) = amt_msat {
355+
invoice = invoice.amount_milli_satoshis(amt);
356+
}
171357
for hint in route_hints {
172358
invoice = invoice.private_route(hint);
173359
}
@@ -1015,4 +1201,81 @@ mod test {
10151201
}
10161202
assert!(chan_ids_to_match.is_empty(), "Unmatched short channel ids: {:?}", chan_ids_to_match);
10171203
}
1204+
1205+
#[test]
1206+
fn test_create_invoice_with_description_hash() {
1207+
let chanmon_cfgs = create_chanmon_cfgs(2);
1208+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1209+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1210+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1211+
1212+
let invoice = ::utils::create_invoice_from_channelmanager_with_description_hash_and_duration_since_epoch(
1213+
&nodes[1].node, nodes[1].keys_manager, Currency::BitcoinTestnet, Some(10_000),
1214+
"Testing description_hash".to_string(), Duration::from_secs(1234567),
1215+
)
1216+
.unwrap();
1217+
assert_eq!(invoice.amount_pico_btc(), Some(100_000));
1218+
assert_eq!(
1219+
invoice.min_final_cltv_expiry(),
1220+
MIN_FINAL_CLTV_EXPIRY as u64
1221+
);
1222+
assert_eq!(
1223+
invoice.description(),
1224+
InvoiceDescription::Hash(&crate::Sha256(Sha256::hash(
1225+
"Testing description_hash".as_bytes()
1226+
)))
1227+
);
1228+
}
1229+
1230+
1231+
#[test]
1232+
#[cfg(feature = "std")]
1233+
fn create_phantom_invoice_with_description_hash() {
1234+
let mut chanmon_cfgs = create_chanmon_cfgs(3);
1235+
let seed_1 = [42 as u8; 32];
1236+
let seed_2 = [43 as u8; 32];
1237+
let cross_node_seed = [44 as u8; 32];
1238+
chanmon_cfgs[1].keys_manager.backing = PhantomKeysManager::new(&seed_1, 43, 44, &cross_node_seed);
1239+
chanmon_cfgs[2].keys_manager.backing =PhantomKeysManager::new(&seed_2, 43, 44, &cross_node_seed);
1240+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
1241+
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
1242+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
1243+
let chan_0_1 = create_announced_chan_between_nodes_with_value(
1244+
&nodes, 0, 1, 100000, 10001, InitFeatures::known(), InitFeatures::known(),
1245+
);
1246+
nodes[0].node.handle_channel_update(&nodes[1].node.get_our_node_id(), &chan_0_1.1);
1247+
nodes[1].node.handle_channel_update(&nodes[0].node.get_our_node_id(), &chan_0_1.0);
1248+
let chan_0_2 = create_announced_chan_between_nodes_with_value(
1249+
&nodes, 0, 2, 100000, 10001, InitFeatures::known(), InitFeatures::known(),
1250+
);
1251+
nodes[0].node.handle_channel_update(&nodes[2].node.get_our_node_id(), &chan_0_2.1);
1252+
nodes[2].node.handle_channel_update(&nodes[0].node.get_our_node_id(), &chan_0_2.0);
1253+
1254+
let payment_amt = 20_000;
1255+
let (payment_hash, payment_secret) = nodes[1].node.create_inbound_payment(Some(payment_amt), 3600).unwrap();
1256+
let route_hints = vec![
1257+
nodes[1].node.get_phantom_route_hints(),
1258+
nodes[2].node.get_phantom_route_hints(),
1259+
];
1260+
let invoice = ::utils::create_phantom_invoice_with_description_hash::<
1261+
EnforcingSigner,
1262+
&test_utils::TestKeysInterface,
1263+
>(
1264+
Some(payment_amt), "Description hash phantom invoice".to_string(), payment_hash,
1265+
payment_secret, route_hints, &nodes[1].keys_manager, Currency::BitcoinTestnet,
1266+
)
1267+
.unwrap();
1268+
1269+
assert_eq!(invoice.amount_pico_btc(), Some(200_000));
1270+
assert_eq!(
1271+
invoice.min_final_cltv_expiry(),
1272+
MIN_FINAL_CLTV_EXPIRY as u64
1273+
);
1274+
assert_eq!(
1275+
invoice.description(),
1276+
InvoiceDescription::Hash(&crate::Sha256(Sha256::hash(
1277+
"Description hash phantom invoice".as_bytes()
1278+
)))
1279+
);
1280+
}
10181281
}

0 commit comments

Comments
 (0)