From: Valentine Wallace Date: Fri, 23 Apr 2021 22:17:52 +0000 (-0400) Subject: Add utility to create an invoice using the ChannelManager X-Git-Tag: v0.0.14~6^2 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=4c7be7e137d5ad552bfae481ba1e5e26f34e74f7;p=rust-lightning Add utility to create an invoice using the ChannelManager This also allows the ChannelManager to track information for inbound payments to check the PaymentSecret on receive. --- diff --git a/lightning-invoice/Cargo.toml b/lightning-invoice/Cargo.toml index 758578ca4..75d9755ad 100644 --- a/lightning-invoice/Cargo.toml +++ b/lightning-invoice/Cargo.toml @@ -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"] diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index ff16ba42f..29d221e94 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -14,6 +14,7 @@ //! * For parsing use `str::parse::(&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 { use bech32::FromBase32; let mut preimage = Vec::::from(hrp_bytes); @@ -748,7 +749,12 @@ impl RawInvoice { preimage.extend_from_slice(&Vec::::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 index 000000000..b90aad82d --- /dev/null +++ b/lightning-invoice/src/utils.rs @@ -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( + channelmanager: &ChannelManager, keys_manager: K, network: Currency, + amt_msat: Option, description: String +) -> Result> +where + M::Target: chain::Watch, + T::Target: BroadcasterInterface, + K::Target: KeysInterface, + 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::>()), + &last_hops.iter().collect::>(), + 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); + } +}