diff --git a/Cargo.lock b/Cargo.lock index dac1fe77..0006ce98 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/orbitalturtle/rust-lightning?branch=signature-data-enum#2e78328f8d89f8c61422925b40436655d4053c92" 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..4eb873e0 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/orbitalturtle/rust-lightning", branch = "signature-data-enum" } 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() + ) + } +}