diff --git a/Cargo.lock b/Cargo.lock index 7c9e0a7a..ee7cf887 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -995,10 +995,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/orbitalturtle/rust-lightning?branch=signature-data-enum#2e78328f8d89f8c61422925b40436655d4053c92" dependencies = [ "bitcoin 0.29.2", + "musig2", ] [[package]] @@ -1204,6 +1204,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" @@ -2141,7 +2149,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 1e4ad9aa..be9262d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,14 +17,14 @@ async-trait = "0.1.66" bitcoin = { version = "0.29.2", features = ["rand"] } clap = { version = "4.4.6", features = ["derive"] } futures = "0.3.26" -lightning = "0.0.118" +lightning = { git = "https://github.com/orbitalturtle/rust-lightning", branch = "signature-data-enum" } rand_chacha = "0.3.1" rand_core = "0.6.4" log = "0.4.17" simple_logger = "4.0.0" 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/blocking_client.rs b/src/blocking_client.rs new file mode 100644 index 00000000..3bcf11c7 --- /dev/null +++ b/src/blocking_client.rs @@ -0,0 +1,41 @@ +use std::path::PathBuf; +use tokio::runtime::{Builder, Runtime}; +use tonic_lnd::signrpc::{KeyDescriptor, KeyLocator, SignMessageReq, SignMessageResp}; +use tonic_lnd::{connect, Client}; + +// For some LND grpc calls, we need a blocking version of the client. Namely, in order to use LDK's "sign" +// API for signing an invoice request, the provided closure containing the LND signing API calls can't be +// asynchronous. +pub(crate) struct BlockingClient { + client: Client, + rt: Runtime, +} + +impl BlockingClient { + pub(crate) fn connect( + address: String, + cert_path: PathBuf, + macaroon_path: PathBuf, + ) -> Result { + let rt = Builder::new_multi_thread().enable_all().build().unwrap(); + let client = rt + .block_on(connect(address, cert_path, macaroon_path)) + .unwrap(); + + Ok(Self { client, rt }) + } + + pub(crate) fn derive_key( + &mut self, + request: KeyLocator, + ) -> Result, tonic_lnd::tonic::Status> { + self.rt.block_on(self.client.wallet().derive_key(request)) + } + + pub(crate) fn sign_message( + &mut self, + request: SignMessageReq, + ) -> Result, tonic_lnd::tonic::Status> { + self.rt.block_on(self.client.signer().sign_message(request)) + } +} diff --git a/src/lib.rs b/src/lib.rs index ed74377a..d535c6c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1 @@ -#[allow(dead_code)] pub mod lndk_offers; diff --git a/src/lndk_offers.rs b/src/lndk_offers.rs index 117e116a..b8e77223 100644 --- a/src/lndk_offers.rs +++ b/src/lndk_offers.rs @@ -1,7 +1,237 @@ +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(Bolt12SemanticError), + SignFailure(SignError), +} + +impl Display for OfferError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OfferError::BuildUIRFailure(e) => write!(f, "{:?}", e), + OfferError::SignFailure(e) => write!(f, "{:?}", 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> { + let key_loc = KeyLocator { + key_family: 6, + key_index: 1, + }; + + let pubkey_bytes = signer + .derive_key(key_loc.clone()) + .await + .expect("failed to get key"); + 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::SignFailure) + }) + .await + .unwrap() +} + +/// 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>; +} + +/// Bolt12Signer is responsible for signing the InvoiceRequest. +#[derive(Clone)] +pub(crate) struct Bolt12Signer { + client: Client, +} + +#[async_trait] +impl MessageSigner for Bolt12Signer { + async fn derive_key(&mut self, key_loc: KeyLocator) -> Result, Status> { + match self.client.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.client.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(); + + let _ = create_request_invoice(signer_mock, offer, vec![], Network::Regtest, 10000).await; + } + + #[tokio::test] + #[should_panic] + 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(); + + let _ = create_request_invoice(signer_mock, offer, vec![], Network::Regtest, 10000).await; + } + + #[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() + ) + } +}