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 4, 2023
1 parent bb5da5c commit d4e02d1
Show file tree
Hide file tree
Showing 5 changed files with 285 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 @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions src/blocking_client.rs
Original file line number Diff line number Diff line change
@@ -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<Self, tonic::transport::Error> {
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::Response<KeyDescriptor>, 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::Response<SignMessageResp>, tonic_lnd::tonic::Status> {
self.rt.block_on(self.client.signer().sign_message(request))
}
}
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
#[allow(dead_code)]
pub mod lndk_offers;
232 changes: 231 additions & 1 deletion src/lndk_offers.rs
Original file line number Diff line number Diff line change
@@ -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<Secp256k1Error> {
BuildUIRFailure(Bolt12SemanticError),
SignFailure(SignError<Secp256k1Error>),
}

impl Display for OfferError<Secp256k1Error> {
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<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>> {
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<Vec<u8>, Status>;
async fn sign_message(
&mut self,
key_loc: KeyLocator,
merkle_hash: Hash,
tag: String,
) -> Result<Vec<u8>, 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<Vec<u8>, 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<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.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<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();

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()
)
}
}

0 comments on commit d4e02d1

Please sign in to comment.