-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add invoice request BOLT 12 functionality
- Loading branch information
1 parent
bb5da5c
commit d4e02d1
Showing
5 changed files
with
285 additions
and
7 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
#[allow(dead_code)] | ||
pub mod lndk_offers; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
) | ||
} | ||
} |