Skip to content

Commit

Permalink
Add invoice request BOLT 12 functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
orbitalturtle committed Nov 14, 2023
1 parent 97de626 commit dc3b5a5
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 7 deletions.
14 changes: 11 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/lnd.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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;
Expand Down Expand Up @@ -178,3 +182,15 @@ pub(crate) fn string_to_network(network_str: &str) -> Result<Network, NetworkPar
_ => 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<Vec<u8>, Status>;
async fn sign_message(
&mut self,
key_loc: KeyLocator,
merkle_hash: Hash,
tag: String,
) -> Result<Vec<u8>, Status>;
}
229 changes: 228 additions & 1 deletion src/lndk_offers.rs
Original file line number Diff line number Diff line change
@@ -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<Secp256k1Error> {
/// BuildUIRFailure indicates a failure to build the unsigned invoice request.
BuildUIRFailure(Bolt12SemanticError),
/// SignError indicates a failure to sign the invoice request.
SignError(SignError<Secp256k1Error>),
/// DeriveKeyFailure indicates a failure to derive key for signing the invoice request.
DeriveKeyFailure(Status),
}

impl Display for OfferError<Secp256k1Error> {
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<Secp256k1Error> {}

// Decodes a bech32 string into an LDK offer.
pub fn decode(offer_str: String) -> Result<Offer, Bolt12ParseError> {
offer_str.parse::<Offer>()
}

#[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<u8>,
network: Network,
msats: u64,
) -> Result<InvoiceRequest, OfferError<bitcoin::secp256k1::Error>> {
// 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, Status>;
async fn sign_message(&mut self, key_loc: KeyLocator, merkle_hash: Hash, tag: String) -> Result<Vec<u8>, 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()
)
}
}

0 comments on commit dc3b5a5

Please sign in to comment.