From b1ecfad187a202e9b1b688737f0c76b182da307a Mon Sep 17 00:00:00 2001 From: Orbital Date: Sun, 29 Oct 2023 17:25:14 -0500 Subject: [PATCH 1/3] Upgrade to LDK 0.0.18 --- Cargo.lock | 49 +++++++++++++++++++++++++----------------- Cargo.toml | 2 +- src/lnd.rs | 16 ++++++++++++++ src/onion_messenger.rs | 6 +----- 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 603754a3..d8107418 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,9 +313,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" dependencies = [ "libc", ] @@ -702,9 +702,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -862,7 +862,7 @@ dependencies = [ "chrono", "hex 0.3.2", "libc", - "lightning", + "lightning 0.0.116", "lightning-background-processor", "lightning-block-sync", "lightning-invoice", @@ -890,6 +890,15 @@ dependencies = [ "bitcoin 0.29.2", ] +[[package]] +name = "lightning" +version = "0.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cec5fa9382154fe9671e8df93095b800c7d77abc66e2a5ef839d672521c5e" +dependencies = [ + "bitcoin 0.29.2", +] + [[package]] name = "lightning-background-processor" version = "0.0.116" @@ -897,7 +906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "398b68a96cceb3c1227504bd5faeb74f26c3233447bc10cc1cb2c67e01b51556" dependencies = [ "bitcoin 0.29.2", - "lightning", + "lightning 0.0.116", "lightning-rapid-gossip-sync", ] @@ -909,7 +918,7 @@ checksum = "d94c276dbe2a777d58ed6ececca96006247a4717c00ac4cdfff62d76852be783" dependencies = [ "bitcoin 0.29.2", "chunked_transfer", - "lightning", + "lightning 0.0.116", "serde_json", ] @@ -922,7 +931,7 @@ dependencies = [ "bech32 0.9.1", "bitcoin 0.29.2", "bitcoin_hashes 0.11.0", - "lightning", + "lightning 0.0.116", "num-traits", "secp256k1 0.24.3", ] @@ -934,7 +943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366c0ae225736cbc03555bd5fb4b44b2e8fe2ca3c868ec53a4b325c38b2ab2bd" dependencies = [ "bitcoin 0.29.2", - "lightning", + "lightning 0.0.116", "tokio", ] @@ -946,7 +955,7 @@ checksum = "93caaafeb42115b70119619c2420e362cce776670427fc4ced3e6df77b41c0b6" dependencies = [ "bitcoin 0.29.2", "libc", - "lightning", + "lightning 0.0.116", "winapi", ] @@ -957,7 +966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a07af5814234924e623bca499e003fca1864024d5bd984e752230f73a131584" dependencies = [ "bitcoin 0.29.2", - "lightning", + "lightning 0.0.116", ] [[package]] @@ -988,7 +997,7 @@ dependencies = [ "hex 0.4.3", "home", "ldk-sample", - "lightning", + "lightning 0.0.118", "log", "log4rs", "mockall", @@ -1622,9 +1631,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64 0.21.5", ] @@ -1782,9 +1791,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "socket2" @@ -1919,9 +1928,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.33.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ "backtrace", "bytes", @@ -1946,9 +1955,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a56d7b9e..2625d4e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ async-trait = "0.1.66" bitcoin = { version = "0.29.2", features = ["rand"] } futures = "0.3.26" home = "0.5.5" -lightning = "0.0.116" +lightning = "0.0.118" rand_chacha = "0.3.1" rand_core = "0.6.4" log = "0.4.17" diff --git a/src/lnd.rs b/src/lnd.rs index 0ec99c40..1c801319 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -5,6 +5,8 @@ use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{self, PublicKey, Scalar, Secp256k1}; use futures::executor::block_on; use lightning::ln::msgs::UnsignedGossipMessage; +use lightning::offers::invoice::UnsignedBolt12Invoice; +use lightning::offers::invoice_request::UnsignedInvoiceRequest; use lightning::sign::{KeyMaterial, NodeSigner, Recipient}; use std::cell::RefCell; use std::collections::HashMap; @@ -131,6 +133,20 @@ impl<'a> NodeSigner for LndNodeSigner<'a> { unimplemented!("not required for onion messaging"); } + fn sign_bolt12_invoice_request( + &self, + _: &UnsignedInvoiceRequest, + ) -> Result { + unimplemented!("not required for onion messaging") + } + + fn sign_bolt12_invoice( + &self, + _: &UnsignedBolt12Invoice, + ) -> Result { + unimplemented!("not required for onion messaging") + } + fn sign_gossip_message(&self, _msg: UnsignedGossipMessage) -> Result { unimplemented!("not required for onion messaging"); } diff --git a/src/onion_messenger.rs b/src/onion_messenger.rs index 82188351..6a8b9df0 100644 --- a/src/onion_messenger.rs +++ b/src/onion_messenger.rs @@ -703,7 +703,6 @@ mod tests { use bitcoin::network::constants::Network; use bitcoin::secp256k1::PublicKey; use bytes::BufMut; - use lightning::events::OnionMessageProvider; use lightning::ln::features::{InitFeatures, NodeFeatures}; use lightning::ln::msgs::{OnionMessage, OnionMessageHandler}; use lightning::util::ser::Readable; @@ -742,12 +741,9 @@ mod tests { mock! { OnionHandler{} - impl OnionMessageProvider for OnionHandler { - fn next_onion_message_for_peer(&self, peer_node_id: PublicKey) -> Option; - } - impl OnionMessageHandler for OnionHandler { fn handle_onion_message(&self, peer_node_id: &PublicKey, msg: &OnionMessage); + fn next_onion_message_for_peer(&self, peer_node_id: PublicKey) -> Option; fn peer_connected(&self, their_node_id: &PublicKey, init: &Init, inbound: bool) -> Result<(), ()>; fn peer_disconnected(&self, their_node_id: &PublicKey); fn provided_node_features(&self) -> NodeFeatures; From 97de626cf5265aedd1323fe1b31986dcd658d618 Mon Sep 17 00:00:00 2001 From: Orbital Date: Thu, 12 Oct 2023 22:24:34 -0500 Subject: [PATCH 2/3] Add a basic cli binary --- Cargo.lock | 115 ++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 ++ README.md | 6 +-- src/cli.rs | 39 +++++++++++++++ src/lib.rs | 2 + src/lndk_offers.rs | 7 +++ 6 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/lndk_offers.rs diff --git a/Cargo.lock b/Cargo.lock index d8107418..dac1fe77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,54 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -346,6 +394,52 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" +[[package]] +name = "clap" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "configure_me" version = "0.4.0" @@ -667,6 +761,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.3" @@ -990,6 +1090,7 @@ dependencies = [ "bitcoind", "bytes", "chrono", + "clap", "configure_me", "configure_me_codegen", "core-rpc", @@ -1367,7 +1468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603" dependencies = [ "bytes", - "heck", + "heck 0.3.3", "itertools", "log", "multimap", @@ -1827,6 +1928,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.109" @@ -2236,6 +2343,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "void" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index 2625d4e9..d4d78631 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,10 @@ version = "0.0.1" edition = "2021" repository = "https://github.com/lndk-org/lndk" +[[bin]] +name = "lndk-cli" +path = "src/cli.rs" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [package.metadata.configure_me] spec = "config_spec.toml" @@ -11,6 +15,7 @@ spec = "config_spec.toml" [dependencies] async-trait = "0.1.66" bitcoin = { version = "0.29.2", features = ["rand"] } +clap = { version = "4.4.6", features = ["derive"] } futures = "0.3.26" home = "0.5.5" lightning = "0.0.118" diff --git a/README.md b/README.md index 67db32bb..8a69b926 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,11 @@ In order for `LNDK` successfully connect to `LND`, we need to pass in the grpc a 1) These values can be passed in via the command line when running the `LNDK` program, like this: -`cargo run -- --address=
--cert= --macaroon=` +`cargo run --bin=lndk -- --address=
--cert= --macaroon=` Or in a more concrete example: -`cargo run -- --address=https://localhost:10009 --cert=/home//.lnd/tls.cert --macaroon=/home//.lnd/data/chain/bitcoin/regtest/admin.macaroon` +`cargo run --bin=lndk -- --address=https://localhost:10009 --cert=/home//.lnd/tls.cert --macaroon=/home//.lnd/data/chain/bitcoin/regtest/admin.macaroon` **Remember** that the grpc address must start with https:// for the program to work. @@ -81,7 +81,7 @@ Or in a more concrete example: * `address=" { + println!("Decoding offer: {offer_string}."); + match decode(offer_string) { + Ok(offer) => println!("Decoded offer: {:?}.", offer), + Err(e) => { + println!( + "ERROR please provide offer starting with lno. Provided offer is \ + invalid, failed to decode with error: {:?}.", + e + ) + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 97441d2c..0bcf8c17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ mod clock; pub mod lnd; +#[allow(dead_code)] +pub mod lndk_offers; mod onion_messenger; mod rate_limit; diff --git a/src/lndk_offers.rs b/src/lndk_offers.rs new file mode 100644 index 00000000..117e116a --- /dev/null +++ b/src/lndk_offers.rs @@ -0,0 +1,7 @@ +use lightning::offers::offer::Offer; +use lightning::offers::parse::Bolt12ParseError; + +// Decodes a bech32 string into an LDK offer. +pub fn decode(offer_str: String) -> Result { + offer_str.parse::() +} From 41a226c06734d8371082809bd3ebf9bfe0cae5c4 Mon Sep 17 00:00:00 2001 From: Orbital Date: Thu, 12 Oct 2023 22:36:53 -0500 Subject: [PATCH 3/3] Add invoice request BOLT 12 functionality --- Cargo.lock | 14 ++- Cargo.toml | 4 +- src/lib.rs | 2 +- src/lnd.rs | 16 ++++ src/lndk_offers.rs | 229 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 258 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dac1fe77..f1fc2c90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -993,10 +993,10 @@ dependencies = [ [[package]] name = "lightning" version = "0.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52cec5fa9382154fe9671e8df93095b800c7d77abc66e2a5ef839d672521c5e" +source = "git+https://github.com/lightningdevkit/rust-lightning?rev=caafcedf3fc40fc6253261218c25b254dd955a82#caafcedf3fc40fc6253261218c25b254dd955a82" dependencies = [ "bitcoin 0.29.2", + "musig2", ] [[package]] @@ -1254,6 +1254,14 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "musig2" +version = "0.1.0" +source = "git+https://github.com/arik-so/rust-musig2?rev=27797d7#27797d78cf64e8974e38d7f31ebb11e455015a9e" +dependencies = [ + "bitcoin 0.29.2", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -2209,7 +2217,7 @@ dependencies = [ [[package]] name = "tonic_lnd" version = "0.5.1" -source = "git+https://github.com/Kixunil/tonic_lnd?rev=fac4a67a8d4951d62fc020d61d38628c0064e6df#fac4a67a8d4951d62fc020d61d38628c0064e6df" +source = "git+https://github.com/orbitalturtle/tonic_lnd?branch=update-signer-client#de989089fdb23f87d3e6bc4796c504bda9b9be9b" dependencies = [ "hex 0.4.3", "prost 0.9.0", diff --git a/Cargo.toml b/Cargo.toml index d4d78631..a584456f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,14 +18,14 @@ bitcoin = { version = "0.29.2", features = ["rand"] } clap = { version = "4.4.6", features = ["derive"] } futures = "0.3.26" home = "0.5.5" -lightning = "0.0.118" +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "caafcedf3fc40fc6253261218c25b254dd955a82" } rand_chacha = "0.3.1" rand_core = "0.6.4" log = "0.4.17" log4rs = { version = "1.2.0", features = ["file_appender"] } tokio = { version = "1.25.0", features = ["rt", "rt-multi-thread"] } tonic = "0.8.3" -tonic_lnd = { git = "https://github.com/Kixunil/tonic_lnd", rev = "fac4a67a8d4951d62fc020d61d38628c0064e6df" } +tonic_lnd = { git = "https://github.com/orbitalturtle/tonic_lnd", branch = "update-signer-client" } hex = "0.4.3" configure_me = "0.4.0" bytes = "1.4.0" diff --git a/src/lib.rs b/src/lib.rs index 0bcf8c17..c3ef0a53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ mod clock; -pub mod lnd; #[allow(dead_code)] +pub mod lnd; pub mod lndk_offers; mod onion_messenger; mod rate_limit; diff --git a/src/lnd.rs b/src/lnd.rs index 1c801319..277b2161 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -1,4 +1,6 @@ +use async_trait::async_trait; use bitcoin::bech32::u5; +use bitcoin::hashes::sha256::Hash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; @@ -13,6 +15,8 @@ use std::collections::HashMap; use std::error::Error; use std::fmt; use std::path::PathBuf; +use tonic_lnd::signrpc::KeyLocator; +use tonic_lnd::tonic::Status; use tonic_lnd::{Client, ConnectError}; const ONION_MESSAGES_REQUIRED: u32 = 38; @@ -178,3 +182,15 @@ pub(crate) fn string_to_network(network_str: &str) -> Result Err(NetworkParseError::Invalid(network_str.to_string())), } } + +/// MessageSigner provides a layer of abstraction over the LND API for message signing. +#[async_trait] +pub(crate) trait MessageSigner { + async fn derive_key(&mut self, key_loc: KeyLocator) -> Result, Status>; + async fn sign_message( + &mut self, + key_loc: KeyLocator, + merkle_hash: Hash, + tag: String, + ) -> Result, Status>; +} diff --git a/src/lndk_offers.rs b/src/lndk_offers.rs index 117e116a..26aae8fd 100644 --- a/src/lndk_offers.rs +++ b/src/lndk_offers.rs @@ -1,7 +1,234 @@ +use crate::lnd::MessageSigner; +use async_trait::async_trait; +use bitcoin::hashes::sha256::Hash; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::schnorr::Signature; +use bitcoin::secp256k1::{Error as Secp256k1Error, PublicKey}; +use futures::executor::block_on; +use lightning::offers::invoice_request::{InvoiceRequest, UnsignedInvoiceRequest}; +use lightning::offers::merkle::SignError; use lightning::offers::offer::Offer; -use lightning::offers::parse::Bolt12ParseError; +use lightning::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; +use std::error::Error; +use std::fmt::Display; +use tokio::task; +use tonic_lnd::signrpc::{KeyLocator, SignMessageReq}; +use tonic_lnd::tonic::Status; +use tonic_lnd::Client; + +#[derive(Debug)] +/// OfferError is an error that occurs during the process of paying an offer. +pub(crate) enum OfferError { + /// BuildUIRFailure indicates a failure to build the unsigned invoice request. + BuildUIRFailure(Bolt12SemanticError), + /// SignError indicates a failure to sign the invoice request. + SignError(SignError), + /// DeriveKeyFailure indicates a failure to derive key for signing the invoice request. + DeriveKeyFailure(Status), +} + +impl Display for OfferError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OfferError::BuildUIRFailure(e) => write!(f, "Error building invoice request: {e:?}"), + OfferError::SignError(e) => write!(f, "Error signing invoice request: {e:?}"), + OfferError::DeriveKeyFailure(e) => write!(f, "Error signing invoice request: {e:?}"), + } + } +} + +impl Error for OfferError {} // Decodes a bech32 string into an LDK offer. pub fn decode(offer_str: String) -> Result { offer_str.parse::() } + +#[allow(dead_code)] +// create_request_invoice builds and signs an invoice request, the first step in the BOLT 12 process of paying an offer. +pub(crate) async fn create_request_invoice( + mut signer: impl MessageSigner + std::marker::Send + 'static, + offer: Offer, + metadata: Vec, + network: Network, + msats: u64, +) -> Result> { + // We use KeyFamily KeyFamilyNodeKey (6) to derive a key to represent our node id. See: + // https://github.com/lightningnetwork/lnd/blob/a3f8011ed695f6204ec6a13ad5c2a67ac542b109/keychain/derivation.go#L103 + let key_loc = KeyLocator { + key_family: 6, + key_index: 1, + }; + + let pubkey_bytes = signer + .derive_key(key_loc.clone()) + .await + .map_err(OfferError::DeriveKeyFailure)?; + let pubkey = PublicKey::from_slice(&pubkey_bytes).expect("failed to deserialize public key"); + + let unsigned_invoice_req = offer + .request_invoice(metadata, pubkey) + .unwrap() + .chain(network) + .unwrap() + .amount_msats(msats) + .unwrap() + .build() + .map_err(OfferError::BuildUIRFailure)?; + + // To create a valid invoice request, we also need to sign it. This is spawned in a blocking + // task because we need to call block_on on sign_message so that sign_closure can be a + // synchronous closure. + task::spawn_blocking(move || { + let sign_closure = |msg: &UnsignedInvoiceRequest| { + let tagged_hash = msg.as_ref(); + let tag = tagged_hash.tag().to_string(); + + let signature = block_on(signer.sign_message(key_loc, tagged_hash.merkle_root(), tag)) + .map_err(|_| Secp256k1Error::InvalidSignature)?; + + Signature::from_slice(&signature) + }; + + unsigned_invoice_req + .sign(sign_closure) + .map_err(OfferError::SignError) + }) + .await + .unwrap() +} + +#[async_trait] +impl MessageSigner for Client { + async fn derive_key(&mut self, key_loc: KeyLocator) -> Result, Status> { + match self.wallet().derive_key(key_loc).await { + Ok(resp) => Ok(resp.into_inner().raw_key_bytes), + Err(e) => Err(e), + } + } + + async fn sign_message( + &mut self, + key_loc: KeyLocator, + merkle_root: Hash, + tag: String, + ) -> Result, Status> { + let tag_vec = tag.as_bytes().to_vec(); + let req = SignMessageReq { + msg: merkle_root.as_ref().to_vec(), + tag: tag_vec, + key_loc: Some(key_loc), + schnorr_sig: true, + ..Default::default() + }; + + let resp = self.signer().sign_message(req).await?; + + let resp_inner = resp.into_inner(); + Ok(resp_inner.signature) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use mockall::mock; + use std::str::FromStr; + + fn get_offer() -> String { + "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqgn3qzsyvfkx26qkyypvr5hfx60h9w9k934lt8s2n6zc0wwtgqlulw7dythr83dqx8tzumg".to_string() + } + + fn get_pubkey() -> String { + "0313ba7ccbd754c117962b9afab6c2870eb3ef43f364a9f6c43d0fabb4553776ba".to_string() + } + + fn get_signature() -> String { + "28b937976a29c15827433086440b36c2bec6ca5bd977557972dca8641cd59ffba50daafb8ee99a19c950976b46f47d9e7aa716652e5657dfc555b82eff467f18".to_string() + } + + mock! { + TestBolt12Signer{} + + #[async_trait] + impl MessageSigner for TestBolt12Signer { + async fn derive_key(&mut self, key_loc: KeyLocator) -> Result, Status>; + async fn sign_message(&mut self, key_loc: KeyLocator, merkle_hash: Hash, tag: String) -> Result, Status>; + } + } + + #[tokio::test] + async fn test_request_invoice() { + let mut signer_mock = MockTestBolt12Signer::new(); + + signer_mock.expect_derive_key().returning(|_| { + Ok(PublicKey::from_str(&get_pubkey()) + .unwrap() + .serialize() + .to_vec()) + }); + + signer_mock.expect_sign_message().returning(|_, _, _| { + Ok(Signature::from_str(&get_signature()) + .unwrap() + .as_ref() + .to_vec()) + }); + + let offer = decode(get_offer()).unwrap(); + + assert!( + create_request_invoice(signer_mock, offer, vec![], Network::Regtest, 10000) + .await + .is_ok() + ) + } + + #[tokio::test] + async fn test_request_invoice_derive_key_error() { + let mut signer_mock = MockTestBolt12Signer::new(); + + signer_mock + .expect_derive_key() + .returning(|_| Err(Status::unknown("error testing"))); + + signer_mock.expect_sign_message().returning(|_, _, _| { + Ok(Signature::from_str(&get_signature()) + .unwrap() + .as_ref() + .to_vec()) + }); + + let offer = decode(get_offer()).unwrap(); + + assert!( + create_request_invoice(signer_mock, offer, vec![], Network::Regtest, 10000) + .await + .is_err() + ) + } + + #[tokio::test] + async fn test_request_invoice_signer_error() { + let mut signer_mock = MockTestBolt12Signer::new(); + + signer_mock.expect_derive_key().returning(|_| { + Ok(PublicKey::from_str(&get_pubkey()) + .unwrap() + .serialize() + .to_vec()) + }); + + signer_mock + .expect_sign_message() + .returning(|_, _, _| Err(Status::unknown("error testing"))); + + let offer = decode(get_offer()).unwrap(); + + assert!( + create_request_invoice(signer_mock, offer, vec![], Network::Regtest, 10000) + .await + .is_err() + ) + } +}