Add utility to create an invoice using the ChannelManager
authorValentine Wallace <vwallace@protonmail.com>
Fri, 23 Apr 2021 22:17:52 +0000 (18:17 -0400)
committerValentine Wallace <vwallace@protonmail.com>
Thu, 29 Apr 2021 23:13:36 +0000 (19:13 -0400)
This also allows the ChannelManager to track information for inbound payments
to check the PaymentSecret on receive.

lightning-invoice/Cargo.toml
lightning-invoice/src/lib.rs
lightning-invoice/src/utils.rs [new file with mode: 0644]

index 758578ca465b5f6cf57e29440e5439002c0ced64..75d9755ad3fd85c8ae152f2ef0e55380b1b2135d 100644 (file)
@@ -15,6 +15,9 @@ secp256k1 = { version = "0.20", features = ["recovery"] }
 num-traits = "0.2.8"
 bitcoin_hashes = "0.9.4"
 
+[dev-dependencies]
+lightning = { version = "0.0.13", path = "../lightning", features = ["_test_utils"] }
+
 [lib]
 crate-type = ["cdylib", "rlib"]
 
index ff16ba42f7dbf28764ebd02d8110a9632108798b..29d221e94ad591e2283f1bfa325bb5b7c60b4db5 100644 (file)
@@ -14,6 +14,7 @@
 //!   * For parsing use `str::parse::<Invoice>(&self)` (see the docs of `impl FromStr for Invoice`)
 //!   * For constructing invoices use the `InvoiceBuilder`
 //!   * For serializing invoices use the `Display`/`ToString` traits
+pub mod utils;
 
 extern crate bech32;
 extern crate bitcoin_hashes;
@@ -33,12 +34,12 @@ use lightning::routing::router::RouteHintHop;
 use secp256k1::key::PublicKey;
 use secp256k1::{Message, Secp256k1};
 use secp256k1::recovery::RecoverableSignature;
-use std::ops::Deref;
 
+use std::fmt::{Display, Formatter, self};
 use std::iter::FilterMap;
+use std::ops::Deref;
 use std::slice::Iter;
 use std::time::{SystemTime, Duration, UNIX_EPOCH};
-use std::fmt::{Display, Formatter, self};
 
 mod de;
 mod ser;
@@ -728,8 +729,8 @@ macro_rules! find_extract {
 
 #[allow(missing_docs)]
 impl RawInvoice {
-       /// Hash the HRP as bytes and signatureless data part.
-       fn hash_from_parts(hrp_bytes: &[u8], data_without_signature: &[u5]) -> [u8; 32] {
+       /// Construct the invoice's HRP and signatureless data into a preimage to be hashed.
+       pub(crate) fn construct_invoice_preimage(hrp_bytes: &[u8], data_without_signature: &[u5]) -> Vec<u8> {
                use bech32::FromBase32;
 
                let mut preimage = Vec::<u8>::from(hrp_bytes);
@@ -748,7 +749,12 @@ impl RawInvoice {
 
                preimage.extend_from_slice(&Vec::<u8>::from_base32(&data_part)
                        .expect("No padding error may occur due to appended zero above."));
+               preimage
+       }
 
+       /// Hash the HRP as bytes and signatureless data part.
+       fn hash_from_parts(hrp_bytes: &[u8], data_without_signature: &[u5]) -> [u8; 32] {
+               let preimage = RawInvoice::construct_invoice_preimage(hrp_bytes, data_without_signature);
                let mut hash: [u8; 32] = Default::default();
                hash.copy_from_slice(&sha256::Hash::hash(&preimage)[..]);
                hash
diff --git a/lightning-invoice/src/utils.rs b/lightning-invoice/src/utils.rs
new file mode 100644 (file)
index 0000000..b90aad8
--- /dev/null
@@ -0,0 +1,157 @@
+//! Convenient utilities to create an invoice.
+use {Currency, Invoice, InvoiceBuilder, SignOrCreationError, RawInvoice};
+use bech32::ToBase32;
+use bitcoin_hashes::Hash;
+use lightning::chain;
+use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator};
+use lightning::chain::keysinterface::{Sign, KeysInterface};
+use lightning::ln::channelmanager::{ChannelManager, MIN_FINAL_CLTV_EXPIRY};
+use lightning::ln::features::InvoiceFeatures;
+use lightning::routing::network_graph::RoutingFees;
+use lightning::routing::router::RouteHintHop;
+use lightning::util::logger::Logger;
+use std::ops::Deref;
+
+/// Utility to construct an invoice. Generally, unless you want to do something like a custom
+/// cltv_expiry, this is what you should be using to create an invoice. The reason being, this
+/// method stores the invoice's payment secret and preimage in `ChannelManager`, so (a) the user
+/// doesn't have to store preimage/payment secret information and (b) `ChannelManager` can verify
+/// that the payment secret is valid when the invoice is paid.
+pub fn create_invoice_from_channelmanager<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>(
+       channelmanager: &ChannelManager<Signer, M, T, K, F, L>, keys_manager: K, network: Currency,
+       amt_msat: Option<u64>, description: String
+) -> Result<Invoice, SignOrCreationError<()>>
+where
+       M::Target: chain::Watch<Signer>,
+       T::Target: BroadcasterInterface,
+       K::Target: KeysInterface<Signer = Signer>,
+       F::Target: FeeEstimator,
+       L::Target: Logger,
+{
+       // Marshall route hints.
+       let our_channels = channelmanager.list_usable_channels();
+       let mut route_hints = vec![];
+       for channel in our_channels {
+               let short_channel_id = match channel.short_channel_id {
+                       Some(id) => id,
+                       None => continue,
+               };
+               let forwarding_info = match channel.counterparty_forwarding_info {
+                       Some(info) => info,
+                       None => continue,
+               };
+               route_hints.push(vec![RouteHintHop {
+                       src_node_id: channel.remote_network_id,
+                       short_channel_id,
+                       fees: RoutingFees {
+                               base_msat: forwarding_info.fee_base_msat,
+                               proportional_millionths: forwarding_info.fee_proportional_millionths,
+                       },
+                       cltv_expiry_delta: forwarding_info.cltv_expiry_delta,
+                       htlc_minimum_msat: None,
+                       htlc_maximum_msat: None,
+               }]);
+       }
+
+       let (payment_hash, payment_secret) = channelmanager.create_inbound_payment(
+               amt_msat,
+               7200, // default invoice expiry is 2 hours
+               0,
+       );
+       let our_node_pubkey = channelmanager.get_our_node_id();
+       let mut invoice = InvoiceBuilder::new(network)
+               .description(description)
+               .current_timestamp()
+               .payee_pub_key(our_node_pubkey)
+               .payment_hash(Hash::from_slice(&payment_hash.0).unwrap())
+               .payment_secret(payment_secret)
+               .features(InvoiceFeatures::known())
+               .min_final_cltv_expiry(MIN_FINAL_CLTV_EXPIRY.into());
+       if let Some(amt) = amt_msat {
+               invoice = invoice.amount_pico_btc(amt * 10);
+       }
+       for hint in route_hints.drain(..) {
+               invoice = invoice.route(hint);
+       }
+
+       let raw_invoice = match invoice.build_raw() {
+               Ok(inv) => inv,
+               Err(e) => return Err(SignOrCreationError::CreationError(e))
+       };
+       let hrp_str = raw_invoice.hrp.to_string();
+       let hrp_bytes = hrp_str.as_bytes();
+       let data_without_signature = raw_invoice.data.to_base32();
+       let invoice_preimage = RawInvoice::construct_invoice_preimage(hrp_bytes, &data_without_signature);
+       let signed_raw_invoice = raw_invoice.sign(|_| keys_manager.sign_invoice(invoice_preimage));
+       match signed_raw_invoice {
+               Ok(inv) => Ok(Invoice::from_signed(inv).unwrap()),
+               Err(e) => Err(SignOrCreationError::SignError(e))
+       }
+}
+
+#[cfg(test)]
+mod test {
+       use {Currency, Description, InvoiceDescription};
+       use lightning::ln::PaymentHash;
+       use lightning::ln::functional_test_utils::*;
+       use lightning::ln::features::InitFeatures;
+       use lightning::ln::msgs::ChannelMessageHandler;
+       use lightning::routing::router;
+       use lightning::util::events::MessageSendEventsProvider;
+       use lightning::util::test_utils;
+       #[test]
+       fn test_from_channelmanager() {
+               let chanmon_cfgs = create_chanmon_cfgs(2);
+               let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+               let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+               let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+               let _chan = create_announced_chan_between_nodes(&nodes, 0, 1, InitFeatures::known(), InitFeatures::known());
+               let invoice = ::utils::create_invoice_from_channelmanager(&nodes[1].node, nodes[1].keys_manager, Currency::BitcoinTestnet, Some(10_000), "test".to_string()).unwrap();
+               assert_eq!(invoice.amount_pico_btc(), Some(100_000));
+               assert_eq!(invoice.min_final_cltv_expiry(), Some(9));
+               assert_eq!(invoice.description(), InvoiceDescription::Direct(&Description("test".to_string())));
+
+               let mut route_hints = invoice.routes().clone();
+               let mut last_hops = Vec::new();
+               for hint in route_hints.drain(..) {
+                       last_hops.push(hint[hint.len() - 1].clone());
+               }
+               let amt_msat = invoice.amount_pico_btc().unwrap() / 10;
+
+               let first_hops = nodes[0].node.list_usable_channels();
+               let network_graph = nodes[0].net_graph_msg_handler.network_graph.read().unwrap();
+               let logger = test_utils::TestLogger::new();
+               let route = router::get_route(
+                       &nodes[0].node.get_our_node_id(),
+                       &network_graph,
+                       &invoice.recover_payee_pub_key(),
+                       Some(invoice.features().unwrap().clone()),
+                       Some(&first_hops.iter().collect::<Vec<_>>()),
+                       &last_hops.iter().collect::<Vec<_>>(),
+                       amt_msat,
+                       invoice.min_final_cltv_expiry().unwrap() as u32,
+                       &logger,
+               ).unwrap();
+
+               let payment_event = {
+                       let mut payment_hash = PaymentHash([0; 32]);
+                       payment_hash.0.copy_from_slice(&invoice.payment_hash().as_ref()[0..32]);
+                       nodes[0].node.send_payment(&route, payment_hash, &Some(invoice.payment_secret().unwrap().clone())).unwrap();
+                       let mut added_monitors = nodes[0].chain_monitor.added_monitors.lock().unwrap();
+                       assert_eq!(added_monitors.len(), 1);
+                       added_monitors.clear();
+
+                       let mut events = nodes[0].node.get_and_clear_pending_msg_events();
+                       assert_eq!(events.len(), 1);
+                       SendEvent::from_event(events.remove(0))
+
+               };
+               nodes[1].node.handle_update_add_htlc(&nodes[0].node.get_our_node_id(), &payment_event.msgs[0]);
+               nodes[1].node.handle_commitment_signed(&nodes[0].node.get_our_node_id(), &payment_event.commitment_msg);
+               let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap();
+               assert_eq!(added_monitors.len(), 1);
+               added_monitors.clear();
+               let events = nodes[1].node.get_and_clear_pending_msg_events();
+               assert_eq!(events.len(), 2);
+       }
+}