Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate bolt11 network #586

Merged
merged 2 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
18 changes: 16 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 @@ -243,6 +246,9 @@ impl BreezServices {
let invoice_amount_msat = parsed_invoice.amount_msat.unwrap_or_default();
let provided_amount_msat = req.amount_msat.unwrap_or_default();

// Valid the invoice network against the config network
validate_network(parsed_invoice.clone(), self.config.network)?;

// Ensure amount is provided for zero invoice
if provided_amount_msat == 0 && invoice_amount_msat == 0 {
return Err(SendPaymentError::InvalidAmount {
Expand Down Expand Up @@ -307,7 +313,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 +1686,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
9 changes: 7 additions & 2 deletions 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 @@ -706,6 +706,9 @@ impl NodeAPI for Greenlight {
let last_hop = invoice.routing_hints.first().and_then(|rh| rh.hops.first());
let mut client: node::ClnClient = self.get_node_client().await?;

// Valid the invoice network against the config network
validate_network(invoice.clone(), self.sdk_config.network)?;

// We first calculate for each channel the max amount to pay (at the receiver)
let mut max_amount_per_channel = self
.max_sendable_amount(Some(hex::decode(invoice.payee_pubkey)?), max_hops, last_hop)
Expand Down Expand Up @@ -818,7 +821,9 @@ impl NodeAPI for Greenlight {
) -> NodeResult<PaymentResponse> {
let mut description = None;
if !bolt11.is_empty() {
description = parse_invoice(&bolt11)?.description;
let invoice = parse_invoice(&bolt11)?;
validate_network(invoice.clone(), self.sdk_config.network)?;
description = invoice.description;
}

let mut client: node::ClnClient = self.get_node_client().await?;
Expand Down
64 changes: 63 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,16 @@ pub fn add_lsp_routing_hints(
Ok(invoice_builder.build_raw()?)
}

// Validate that the LNInvoice network matches the provided network
pub fn validate_network(invoice: LNInvoice, network: Network) -> InvoiceResult<()> {
match invoice.network == network {
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 +216,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 +236,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 +287,50 @@ 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");
let res: LNInvoice = parse_invoice(&payreq).unwrap();
assert!(validate_network(res.clone(), Network::Bitcoin).is_ok());

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");
let res = parse_invoice(&payreq);

assert!(res.is_ok());
assert!(validate_network(res.unwrap(), Network::Testnet).is_err());
}

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

assert!(res.is_ok());
assert!(validate_network(res.unwrap(), Network::Bitcoin).is_err());
}
}
Loading
Loading