Skip to content

Commit

Permalink
Validate the invoice network when sending payments
Browse files Browse the repository at this point in the history
  • Loading branch information
dangeross committed Nov 15, 2023
1 parent af67ee9 commit 0a20576
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 43 deletions.
3 changes: 3 additions & 0 deletions libs/sdk-bindings/src/breez_sdk.udl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum LnUrlPayError {
"Generic",
"InvalidAmount",
"InvalidInvoice",
"InvalidNetwork",
"InvalidUri",
"InvoiceExpired",
"PaymentFailed",
Expand Down Expand Up @@ -81,6 +82,7 @@ enum SendPaymentError {
"InvalidAmount",
"InvalidInvoice",
"InvoiceExpired",
"InvalidNetwork",
"PaymentFailed",
"PaymentTimeout",
"RouteNotFound",
Expand Down Expand Up @@ -134,6 +136,7 @@ dictionary RouteHint {

dictionary LNInvoice {
string bolt11;
Network network;
string payee_pubkey;
string payment_hash;
string? description;
Expand Down
16 changes: 14 additions & 2 deletions libs/sdk-core/src/breez_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ use crate::grpc::payment_notifier_client::PaymentNotifierClient;
use crate::grpc::signer_client::SignerClient;
use crate::grpc::swapper_client::SwapperClient;
use crate::grpc::PaymentInformation;
use crate::invoice::{add_lsp_routing_hints, parse_invoice, LNInvoice, RouteHint, RouteHintHop};
use crate::invoice::{
add_lsp_routing_hints, parse_invoice, validate_network, LNInvoice, RouteHint, RouteHintHop,
};
use crate::lnurl::auth::perform_lnurl_auth;
use crate::lnurl::pay::model::SuccessAction::Aes;
use crate::lnurl::pay::model::{
Expand Down Expand Up @@ -149,6 +151,7 @@ pub struct CheckMessageResponse {

/// BreezServices is a facade and the single entry point for the SDK.
pub struct BreezServices {
config: Config,
started: Mutex<bool>,
node_api: Arc<dyn NodeAPI>,
lsp_api: Arc<dyn LspAPI>,
Expand Down Expand Up @@ -239,6 +242,7 @@ impl BreezServices {
req: SendPaymentRequest,
) -> Result<SendPaymentResponse, SendPaymentError> {
self.start_node().await?;
validate_network(req.bolt11.as_str(), self.config.network)?;
let parsed_invoice = parse_invoice(req.bolt11.as_str())?;
let invoice_amount_msat = parsed_invoice.amount_msat.unwrap_or_default();
let provided_amount_msat = req.amount_msat.unwrap_or_default();
Expand Down Expand Up @@ -307,7 +311,14 @@ impl BreezServices {
///
/// This method will return an [anyhow::Error] when any validation check fails.
pub async fn lnurl_pay(&self, req: LnUrlPayRequest) -> Result<LnUrlPayResult, LnUrlPayError> {
match validate_lnurl_pay(req.amount_msat, req.comment, req.data.clone()).await? {
match validate_lnurl_pay(
req.amount_msat,
req.comment,
req.data.clone(),
self.config.network,
)
.await?
{
ValidatedCallbackResponse::EndpointError { data: e } => {
Ok(LnUrlPayResult::EndpointError { data: e })
}
Expand Down Expand Up @@ -1673,6 +1684,7 @@ impl BreezServicesBuilder {

// Create the node services and it them statically
let breez_services = Arc::new(BreezServices {
config: self.config.clone(),
started: Mutex::new(false),
node_api: unwrapped_node_api.clone(),
lsp_api: self.lsp_api.clone().unwrap_or_else(|| breez_server.clone()),
Expand Down
1 change: 1 addition & 0 deletions libs/sdk-core/src/bridge_generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,7 @@ impl support::IntoDart for LNInvoice {
fn into_dart(self) -> support::DartAbi {
vec![
self.bolt11.into_into_dart().into_dart(),
self.network.into_into_dart().into_dart(),
self.payee_pubkey.into_into_dart().into_dart(),
self.payment_hash.into_into_dart().into_dart(),
self.description.into_dart(),
Expand Down
43 changes: 29 additions & 14 deletions libs/sdk-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ pub enum LnUrlPayError {
#[error("Invalid invoice: {err}")]
InvalidInvoice { err: String },

#[error("Invalid network: {err}")]
InvalidNetwork { err: String },

#[error("Invalid uri: {err}")]
InvalidUri { err: String },

Expand Down Expand Up @@ -105,15 +108,26 @@ impl From<bitcoin::hashes::hex::Error> for LnUrlPayError {
}
}

impl From<InvoiceError> for LnUrlPayError {
fn from(value: InvoiceError) -> Self {
match value {
InvoiceError::InvalidNetwork(err) => Self::InvalidNetwork {
err: err.to_string(),
},
_ => Self::InvalidInvoice {
err: value.to_string(),
},
}
}
}

impl From<LnUrlError> for LnUrlPayError {
fn from(value: LnUrlError) -> Self {
match value {
LnUrlError::InvalidUri(err) => Self::InvalidUri {
err: err.to_string(),
},
LnUrlError::InvalidInvoice(err) => Self::InvalidInvoice {
err: err.to_string(),
},
LnUrlError::InvalidInvoice(err) => err.into(),
LnUrlError::ServiceConnectivity(err) => Self::ServiceConnectivity {
err: err.to_string(),
},
Expand Down Expand Up @@ -149,6 +163,7 @@ impl From<SendPaymentError> for LnUrlPayError {
SendPaymentError::AlreadyPaid => Self::AlreadyPaid,
SendPaymentError::InvalidAmount { err } => Self::InvalidAmount { err },
SendPaymentError::InvalidInvoice { err } => Self::InvalidInvoice { err },
SendPaymentError::InvalidNetwork { err } => Self::InvalidNetwork { err },
SendPaymentError::InvoiceExpired { err } => Self::InvoiceExpired { err },
SendPaymentError::PaymentFailed { err } => Self::PaymentFailed { err },
SendPaymentError::PaymentTimeout { err } => Self::PaymentTimeout { err },
Expand Down Expand Up @@ -571,6 +586,9 @@ pub enum SendPaymentError {
#[error("Invalid invoice: {err}")]
InvalidInvoice { err: String },

#[error("Invalid network: {err}")]
InvalidNetwork { err: String },

#[error("Invoice expired: {err}")]
InvoiceExpired { err: String },

Expand Down Expand Up @@ -599,17 +617,14 @@ impl From<anyhow::Error> for SendPaymentError {
}

impl From<InvoiceError> for SendPaymentError {
fn from(err: InvoiceError) -> Self {
Self::InvalidInvoice {
err: err.to_string(),
}
}
}

impl From<InvoiceError> for LnUrlPayError {
fn from(err: InvoiceError) -> Self {
Self::InvalidInvoice {
err: err.to_string(),
fn from(value: InvoiceError) -> Self {
match value {
InvoiceError::InvalidNetwork(err) => Self::InvalidNetwork {
err: err.to_string(),
},
_ => Self::InvalidInvoice {
err: value.to_string(),
},
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion libs/sdk-core/src/greenlight/node_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use tokio::time::sleep;
use tokio_stream::{Stream, StreamExt};
use tonic::Streaming;

use crate::invoice::{parse_invoice, InvoiceError, RouteHintHop};
use crate::invoice::{parse_invoice, validate_network, InvoiceError, RouteHintHop};
use crate::models::*;
use crate::node_api::{NodeAPI, NodeError, NodeResult};
use crate::persist::db::SqliteStorage;
Expand Down Expand Up @@ -702,6 +702,7 @@ impl NodeAPI for Greenlight {
}

async fn send_pay(&self, bolt11: String, max_hops: u32) -> NodeResult<PaymentResponse> {
validate_network(&bolt11, self.sdk_config.network)?;
let invoice = parse_invoice(&bolt11)?;
let last_hop = invoice.routing_hints.first().and_then(|rh| rh.hops.first());
let mut client: node::ClnClient = self.get_node_client().await?;
Expand Down Expand Up @@ -818,6 +819,7 @@ impl NodeAPI for Greenlight {
) -> NodeResult<PaymentResponse> {
let mut description = None;
if !bolt11.is_empty() {
validate_network(&bolt11, self.sdk_config.network)?;
description = parse_invoice(&bolt11)?.description;
}

Expand Down
72 changes: 71 additions & 1 deletion libs/sdk-core/src/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::time::{SystemTimeError, UNIX_EPOCH};

use crate::Network;

pub type InvoiceResult<T, E = InvoiceError> = Result<T, E>;

#[derive(Debug, thiserror::Error)]
pub enum InvoiceError {
#[error("Generic: {0}")]
Generic(#[from] anyhow::Error),

#[error("Invalid network: {0}")]
InvalidNetwork(anyhow::Error),

#[error("Validation: {0}")]
Validation(anyhow::Error),
}
Expand Down Expand Up @@ -60,6 +65,7 @@ impl From<SystemTimeError> for InvoiceError {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct LNInvoice {
pub bolt11: String,
pub network: Network,
pub payee_pubkey: String,
pub payment_hash: String,
pub description: Option<String>,
Expand Down Expand Up @@ -189,6 +195,25 @@ pub fn add_lsp_routing_hints(
Ok(invoice_builder.build_raw()?)
}

// Validate that the BOLT11 payment request matches the network
pub fn validate_network(bolt11: &str, network: Network) -> InvoiceResult<()> {
if bolt11.trim().is_empty() {
return Err(InvoiceError::Validation(anyhow!(
"bolt11 is an empty string"
)));
}
let re = Regex::new(r"(?i)^lightning:")?;
let bolt11 = re.replace_all(bolt11, "");
let signed = bolt11.parse::<SignedRawInvoice>()?;
let invoice = Invoice::from_signed(signed)?;
match invoice.network() == network.into() {
true => Ok(()),
false => Err(InvoiceError::InvalidNetwork(anyhow!(
"Invoice network does not match config"
))),
}
}

/// Parse a BOLT11 payment request and return a structure contains the parsed fields.
pub fn parse_invoice(bolt11: &str) -> InvoiceResult<LNInvoice> {
if bolt11.trim().is_empty() {
Expand All @@ -200,7 +225,6 @@ pub fn parse_invoice(bolt11: &str) -> InvoiceResult<LNInvoice> {
let bolt11 = re.replace_all(bolt11, "");
let signed = bolt11.parse::<SignedRawInvoice>()?;
let invoice = Invoice::from_signed(signed)?;

let since_the_epoch = invoice.timestamp().duration_since(UNIX_EPOCH)?;

// make sure signature is valid
Expand All @@ -221,6 +245,7 @@ pub fn parse_invoice(bolt11: &str) -> InvoiceResult<LNInvoice> {
// return the parsed invoice
let ln_invoice = LNInvoice {
bolt11: bolt11.to_string(),
network: invoice.network().into(),
payee_pubkey,
expiry: invoice.expiry_time().as_secs(),
amount_msat: invoice.amount_milli_satoshis(),
Expand Down Expand Up @@ -271,4 +296,49 @@ mod tests {
let encoded = add_lsp_routing_hints(payreq, Some(route_hint), 100).unwrap();
print!("{encoded:?}");
}

#[test]
fn test_parse_invoice_network() {
let payreq = String::from("lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz");
assert!(validate_network(&payreq, Network::Bitcoin).is_ok());

let res = parse_invoice(&payreq).unwrap();

let private_key_vec =
hex::decode("3e171115f50b2c355836dc026a6d54d525cf0d796eb50b3460a205d25c9d38fd")
.unwrap();
let mut private_key: [u8; 32] = Default::default();
private_key.copy_from_slice(&private_key_vec[0..32]);
let hint_hop = RouteHintHop {
src_node_id: res.payee_pubkey,
short_channel_id: 1234,
fees_base_msat: 1000,
fees_proportional_millionths: 100,
cltv_expiry_delta: 2000,
htlc_minimum_msat: Some(3000),
htlc_maximum_msat: Some(4000),
};
let route_hint = RouteHint {
hops: vec![hint_hop],
};

let encoded = add_lsp_routing_hints(payreq, Some(route_hint), 100).unwrap();
print!("{encoded:?}");
}

#[test]
fn test_parse_invoice_invalid_bitcoin_network() {
let payreq = String::from("lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz");

assert!(validate_network(&payreq, Network::Testnet).is_err());
assert!(parse_invoice(&payreq).is_ok());
}

#[test]
fn test_parse_invoice_invalid_testnet_network() {
let payreq = String::from("lntb15u1pj53l9tpp5p7kjsjcv3eqa39upytmj6k7ac8rqvdffyqr4um98pq5n4ppwxvnsdpzxysy2umswfjhxum0yppk76twypgxzmnwvyxqrrsscqp79qy9qsqsp53xw4x5ezpzvnheff9mrt0ju72u5a5dnxyh4rq6gtweufv9650d4qwqj3ds5xfg4pxc9h7a2g43fmntr4tt322jzujsycvuvury50u994kzr8539qf658hrp07hyz634qpvkeh378wnvf7lddp2x7yfgyk9cp7f7937");

assert!(validate_network(&payreq, Network::Bitcoin).is_err());
assert!(parse_invoice(&payreq).is_ok());
}
}
Loading

0 comments on commit 0a20576

Please sign in to comment.