mirror of
https://github.com/lightningdevkit/rust-lightning.git
synced 2025-01-18 21:34:48 +01:00
Merge pull request #3535 from jkczyz/2025-01-invoice-amount
Validate `amount_msats` against invreq amount
This commit is contained in:
commit
6d604c52b2
@ -566,7 +566,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() {
|
||||
human_readable_name: None,
|
||||
},
|
||||
});
|
||||
assert_eq!(invoice_request.amount_msats(), None);
|
||||
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
|
||||
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
|
||||
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id));
|
||||
|
||||
@ -727,7 +727,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() {
|
||||
human_readable_name: None,
|
||||
},
|
||||
});
|
||||
assert_eq!(invoice_request.amount_msats(), None);
|
||||
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
|
||||
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
|
||||
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id));
|
||||
|
||||
@ -1116,7 +1116,7 @@ fn creates_and_pays_for_offer_with_retry() {
|
||||
human_readable_name: None,
|
||||
},
|
||||
});
|
||||
assert_eq!(invoice_request.amount_msats(), None);
|
||||
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
|
||||
assert_ne!(invoice_request.payer_signing_pubkey(), bob_id);
|
||||
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id));
|
||||
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
|
||||
@ -1411,7 +1411,7 @@ fn fails_authentication_when_handling_invoice_request() {
|
||||
alice.onion_messenger.handle_onion_message(david_id, &onion_message);
|
||||
|
||||
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
|
||||
assert_eq!(invoice_request.amount_msats(), None);
|
||||
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
|
||||
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
|
||||
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id));
|
||||
|
||||
@ -1441,7 +1441,7 @@ fn fails_authentication_when_handling_invoice_request() {
|
||||
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
|
||||
|
||||
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
|
||||
assert_eq!(invoice_request.amount_msats(), None);
|
||||
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
|
||||
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
|
||||
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id));
|
||||
|
||||
@ -1543,7 +1543,7 @@ fn fails_authentication_when_handling_invoice_for_offer() {
|
||||
alice.onion_messenger.handle_onion_message(bob_id, &onion_message);
|
||||
|
||||
let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message);
|
||||
assert_eq!(invoice_request.amount_msats(), None);
|
||||
assert_eq!(invoice_request.amount_msats(), Some(10_000_000));
|
||||
assert_ne!(invoice_request.payer_signing_pubkey(), david_id);
|
||||
assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(charlie_id));
|
||||
|
||||
|
@ -342,7 +342,7 @@ macro_rules! invoice_builder_methods { (
|
||||
pub(crate) fn amount_msats(
|
||||
invoice_request: &InvoiceRequest
|
||||
) -> Result<u64, Bolt12SemanticError> {
|
||||
match invoice_request.amount_msats() {
|
||||
match invoice_request.contents.inner.amount_msats() {
|
||||
Some(amount_msats) => Ok(amount_msats),
|
||||
None => match invoice_request.contents.inner.offer.amount() {
|
||||
Some(Amount::Bitcoin { amount_msats }) => {
|
||||
@ -1531,6 +1531,11 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
)
|
||||
)?;
|
||||
|
||||
if amount_msats != refund.amount_msats() {
|
||||
return Err(Bolt12SemanticError::InvalidAmount);
|
||||
}
|
||||
|
||||
Ok(InvoiceContents::ForRefund { refund, fields })
|
||||
} else {
|
||||
let invoice_request = InvoiceRequestContents::try_from(
|
||||
@ -1539,6 +1544,13 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
)
|
||||
)?;
|
||||
|
||||
if let Some(requested_amount_msats) = invoice_request.amount_msats() {
|
||||
if amount_msats != requested_amount_msats {
|
||||
return Err(Bolt12SemanticError::InvalidAmount);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(InvoiceContents::ForOffer { invoice_request, fields })
|
||||
}
|
||||
}
|
||||
@ -2707,6 +2719,69 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_invoice_with_wrong_amount() {
|
||||
let expanded_key = ExpandedKey::new([42; 32]);
|
||||
let entropy = FixedEntropy {};
|
||||
let nonce = Nonce::from_entropy_source(&entropy);
|
||||
let secp_ctx = Secp256k1::new();
|
||||
let payment_id = PaymentId([1; 32]);
|
||||
|
||||
let invoice = OfferBuilder::new(recipient_pubkey())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
|
||||
.build_and_sign().unwrap()
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.amount_msats_unchecked(2000)
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
invoice.write(&mut buffer).unwrap();
|
||||
|
||||
match Bolt12Invoice::try_from(buffer) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)),
|
||||
}
|
||||
|
||||
let invoice = OfferBuilder::new(recipient_pubkey())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
|
||||
.amount_msats(1000).unwrap()
|
||||
.build_and_sign().unwrap()
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.amount_msats_unchecked(2000)
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
invoice.write(&mut buffer).unwrap();
|
||||
|
||||
match Bolt12Invoice::try_from(buffer) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)),
|
||||
}
|
||||
|
||||
let invoice = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.build().unwrap()
|
||||
.respond_using_derived_keys_no_std(
|
||||
payment_paths(), payment_hash(), now(), &expanded_key, &entropy
|
||||
)
|
||||
.unwrap()
|
||||
.amount_msats_unchecked(2000)
|
||||
.build_and_sign(&secp_ctx).unwrap();
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
invoice.write(&mut buffer).unwrap();
|
||||
|
||||
match Bolt12Invoice::try_from(buffer) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_invoice_without_signature() {
|
||||
let expanded_key = ExpandedKey::new([42; 32]);
|
||||
|
@ -87,6 +87,14 @@ macro_rules! invoice_builder_methods_test { (
|
||||
$self: ident, $self_type: ty, $invoice_fields: expr, $return_type: ty, $return_value: expr
|
||||
$(, $self_mut: tt)?
|
||||
) => {
|
||||
#[cfg_attr(c_bindings, allow(dead_code))]
|
||||
pub(crate) fn amount_msats_unchecked(
|
||||
$($self_mut)* $self: $self_type, amount_msats: u64,
|
||||
) -> $return_type {
|
||||
$invoice_fields.amount_msats = amount_msats;
|
||||
$return_value
|
||||
}
|
||||
|
||||
#[cfg_attr(c_bindings, allow(dead_code))]
|
||||
pub(crate) fn features_unchecked(
|
||||
$($self_mut)* $self: $self_type, features: Bolt12InvoiceFeatures
|
||||
|
@ -79,7 +79,7 @@ use crate::ln::inbound_payment::{ExpandedKey, IV_LEN};
|
||||
use crate::ln::msgs::DecodeError;
|
||||
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::offer::{EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OFFER_TYPES, Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef};
|
||||
use crate::offers::offer::{Amount, EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OFFER_TYPES, Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef};
|
||||
use crate::offers::parse::{Bolt12ParseError, ParsedMessage, Bolt12SemanticError};
|
||||
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
|
||||
use crate::offers::signer::{Metadata, MetadataMaterial};
|
||||
@ -665,6 +665,15 @@ macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => {
|
||||
$contents.amount_msats()
|
||||
}
|
||||
|
||||
/// Returns whether an amount was set in the request; otherwise, if [`amount_msats`] is `Some`
|
||||
/// then it was inferred from the [`Offer::amount`] and [`quantity`].
|
||||
///
|
||||
/// [`amount_msats`]: Self::amount_msats
|
||||
/// [`quantity`]: Self::quantity
|
||||
pub fn has_amount_msats(&$self) -> bool {
|
||||
$contents.has_amount_msats()
|
||||
}
|
||||
|
||||
/// Features pertaining to requesting an invoice.
|
||||
pub fn invoice_request_features(&$self) -> &InvoiceRequestFeatures {
|
||||
&$contents.features()
|
||||
@ -974,7 +983,19 @@ impl InvoiceRequestContents {
|
||||
}
|
||||
|
||||
pub(super) fn amount_msats(&self) -> Option<u64> {
|
||||
self.inner.amount_msats
|
||||
self.inner
|
||||
.amount_msats()
|
||||
.or_else(|| match self.inner.offer.amount() {
|
||||
Some(Amount::Bitcoin { amount_msats }) => {
|
||||
Some(amount_msats.saturating_mul(self.quantity().unwrap_or(1)))
|
||||
},
|
||||
Some(Amount::Currency { .. }) => None,
|
||||
None => { debug_assert!(false); None},
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn has_amount_msats(&self) -> bool {
|
||||
self.inner.amount_msats().is_some()
|
||||
}
|
||||
|
||||
pub(super) fn features(&self) -> &InvoiceRequestFeatures {
|
||||
@ -1015,6 +1036,10 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey {
|
||||
self.chain.unwrap_or_else(|| self.offer.implied_chain())
|
||||
}
|
||||
|
||||
pub(super) fn amount_msats(&self) -> Option<u64> {
|
||||
self.amount_msats
|
||||
}
|
||||
|
||||
pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
|
||||
let payer = PayerTlvStreamRef {
|
||||
metadata: self.payer.0.as_bytes(),
|
||||
@ -1381,7 +1406,7 @@ mod tests {
|
||||
assert_eq!(invoice_request.supported_quantity(), Quantity::One);
|
||||
assert_eq!(invoice_request.issuer_signing_pubkey(), Some(recipient_pubkey()));
|
||||
assert_eq!(invoice_request.chain(), ChainHash::using_genesis_block(Network::Bitcoin));
|
||||
assert_eq!(invoice_request.amount_msats(), None);
|
||||
assert_eq!(invoice_request.amount_msats(), Some(1000));
|
||||
assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty());
|
||||
assert_eq!(invoice_request.quantity(), None);
|
||||
assert_eq!(invoice_request.payer_note(), None);
|
||||
@ -1657,6 +1682,7 @@ mod tests {
|
||||
.amount_msats(1000).unwrap()
|
||||
.build_and_sign().unwrap();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert!(invoice_request.has_amount_msats());
|
||||
assert_eq!(invoice_request.amount_msats(), Some(1000));
|
||||
assert_eq!(tlv_stream.amount, Some(1000));
|
||||
|
||||
@ -1668,6 +1694,7 @@ mod tests {
|
||||
.amount_msats(1000).unwrap()
|
||||
.build_and_sign().unwrap();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert!(invoice_request.has_amount_msats());
|
||||
assert_eq!(invoice_request.amount_msats(), Some(1000));
|
||||
assert_eq!(tlv_stream.amount, Some(1000));
|
||||
|
||||
@ -1678,6 +1705,7 @@ mod tests {
|
||||
.amount_msats(1001).unwrap()
|
||||
.build_and_sign().unwrap();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert!(invoice_request.has_amount_msats());
|
||||
assert_eq!(invoice_request.amount_msats(), Some(1001));
|
||||
assert_eq!(tlv_stream.amount, Some(1001));
|
||||
|
||||
@ -1748,6 +1776,47 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_invoice_request_without_amount() {
|
||||
let expanded_key = ExpandedKey::new([42; 32]);
|
||||
let entropy = FixedEntropy {};
|
||||
let nonce = Nonce::from_entropy_source(&entropy);
|
||||
let secp_ctx = Secp256k1::new();
|
||||
let payment_id = PaymentId([1; 32]);
|
||||
|
||||
let invoice_request = OfferBuilder::new(recipient_pubkey())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
|
||||
.build_and_sign().unwrap();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert!(!invoice_request.has_amount_msats());
|
||||
assert_eq!(invoice_request.amount_msats(), Some(1000));
|
||||
assert_eq!(tlv_stream.amount, None);
|
||||
|
||||
let invoice_request = OfferBuilder::new(recipient_pubkey())
|
||||
.amount_msats(1000)
|
||||
.supported_quantity(Quantity::Unbounded)
|
||||
.build().unwrap()
|
||||
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
|
||||
.quantity(2).unwrap()
|
||||
.build_and_sign().unwrap();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert!(!invoice_request.has_amount_msats());
|
||||
assert_eq!(invoice_request.amount_msats(), Some(2000));
|
||||
assert_eq!(tlv_stream.amount, None);
|
||||
|
||||
let invoice_request = OfferBuilder::new(recipient_pubkey())
|
||||
.amount(Amount::Currency { iso4217_code: *b"USD", amount: 10 })
|
||||
.build_unchecked()
|
||||
.request_invoice(&expanded_key, nonce, &secp_ctx, payment_id).unwrap()
|
||||
.build_unchecked_and_sign();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert!(!invoice_request.has_amount_msats());
|
||||
assert_eq!(invoice_request.amount_msats(), None);
|
||||
assert_eq!(tlv_stream.amount, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_invoice_request_with_features() {
|
||||
let expanded_key = ExpandedKey::new([42; 32]);
|
||||
|
@ -147,7 +147,7 @@ pub enum Bolt12SemanticError {
|
||||
UnexpectedChain,
|
||||
/// An amount was expected but was missing.
|
||||
MissingAmount,
|
||||
/// The amount exceeded the total bitcoin supply.
|
||||
/// The amount exceeded the total bitcoin supply or didn't match an expected amount.
|
||||
InvalidAmount,
|
||||
/// An amount was provided but was not sufficient in value.
|
||||
InsufficientAmount,
|
||||
|
Loading…
Reference in New Issue
Block a user