mirror of
https://github.com/lightningdevkit/rust-lightning.git
synced 2025-01-19 05:43:55 +01:00
Merge pull request #3237 from jkczyz/2024-08-offers-experimental-tlvs
Experimental offer TLVs
This commit is contained in:
commit
cb650a8606
@ -121,10 +121,10 @@ use crate::ln::msgs::DecodeError;
|
||||
use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common};
|
||||
#[cfg(test)]
|
||||
use crate::offers::invoice_macros::invoice_builder_methods_test;
|
||||
use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
|
||||
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self};
|
||||
use crate::offers::invoice_request::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
|
||||
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity};
|
||||
use crate::offers::offer::{Amount, EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity};
|
||||
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
|
||||
use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef};
|
||||
use crate::offers::refund::{IV_BYTES_WITH_METADATA as REFUND_IV_BYTES_WITH_METADATA, IV_BYTES_WITHOUT_METADATA as REFUND_IV_BYTES_WITHOUT_METADATA, Refund, RefundContents};
|
||||
@ -363,6 +363,8 @@ macro_rules! invoice_builder_methods { (
|
||||
InvoiceFields {
|
||||
payment_paths, created_at, relative_expiry: None, payment_hash, amount_msats,
|
||||
fallbacks: None, features: Bolt12InvoiceFeatures::empty(), signing_pubkey,
|
||||
#[cfg(test)]
|
||||
experimental_baz: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -461,6 +463,7 @@ for InvoiceBuilder<'a, DerivedSigningPubkey> {
|
||||
#[derive(Clone)]
|
||||
pub struct UnsignedBolt12Invoice {
|
||||
bytes: Vec<u8>,
|
||||
experimental_bytes: Vec<u8>,
|
||||
contents: InvoiceContents,
|
||||
tagged_hash: TaggedHash,
|
||||
}
|
||||
@ -491,19 +494,61 @@ where
|
||||
|
||||
impl UnsignedBolt12Invoice {
|
||||
fn new(invreq_bytes: &[u8], contents: InvoiceContents) -> Self {
|
||||
// TLV record ranges applicable to invreq_bytes.
|
||||
const NON_EXPERIMENTAL_TYPES: core::ops::Range<u64> = 0..INVOICE_REQUEST_TYPES.end;
|
||||
const EXPERIMENTAL_TYPES: core::ops::Range<u64> =
|
||||
EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end;
|
||||
|
||||
let (_, _, _, invoice_tlv_stream, _, _, experimental_invoice_tlv_stream) =
|
||||
contents.as_tlv_stream();
|
||||
|
||||
// Allocate enough space for the invoice, which will include:
|
||||
// - all TLV records from `invreq_bytes` except signatures,
|
||||
// - all invoice-specific TLV records, and
|
||||
// - a signature TLV record once the invoice is signed.
|
||||
//
|
||||
// This assumes both the invoice request and the invoice will each only have one signature
|
||||
// using SIGNATURE_TYPES.start as the TLV record. Thus, it is accounted for by invreq_bytes.
|
||||
let mut bytes = Vec::with_capacity(
|
||||
invreq_bytes.len()
|
||||
+ invoice_tlv_stream.serialized_length()
|
||||
+ if contents.is_for_offer() { 0 } else { SIGNATURE_TLV_RECORD_SIZE }
|
||||
+ experimental_invoice_tlv_stream.serialized_length(),
|
||||
);
|
||||
|
||||
// Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may
|
||||
// have contained unknown TLV records, which are not stored in `InvoiceRequestContents` or
|
||||
// `RefundContents`.
|
||||
let (_, _, _, invoice_tlv_stream) = contents.as_tlv_stream();
|
||||
let invoice_request_bytes = WithoutSignatures(invreq_bytes);
|
||||
let unsigned_tlv_stream = (invoice_request_bytes, invoice_tlv_stream);
|
||||
for record in TlvStream::new(invreq_bytes).range(NON_EXPERIMENTAL_TYPES) {
|
||||
record.write(&mut bytes).unwrap();
|
||||
}
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
unsigned_tlv_stream.write(&mut bytes).unwrap();
|
||||
let remaining_bytes = &invreq_bytes[bytes.len()..];
|
||||
|
||||
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
|
||||
invoice_tlv_stream.write(&mut bytes).unwrap();
|
||||
|
||||
Self { bytes, contents, tagged_hash }
|
||||
let mut experimental_tlv_stream = TlvStream::new(remaining_bytes)
|
||||
.range(EXPERIMENTAL_TYPES)
|
||||
.peekable();
|
||||
let mut experimental_bytes = Vec::with_capacity(
|
||||
remaining_bytes.len()
|
||||
- experimental_tlv_stream
|
||||
.peek()
|
||||
.map_or(remaining_bytes.len(), |first_record| first_record.start)
|
||||
+ experimental_invoice_tlv_stream.serialized_length(),
|
||||
);
|
||||
|
||||
for record in experimental_tlv_stream {
|
||||
record.write(&mut experimental_bytes).unwrap();
|
||||
}
|
||||
|
||||
experimental_invoice_tlv_stream.write(&mut experimental_bytes).unwrap();
|
||||
debug_assert_eq!(experimental_bytes.len(), experimental_bytes.capacity());
|
||||
|
||||
let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
|
||||
let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
|
||||
|
||||
Self { bytes, experimental_bytes, contents, tagged_hash }
|
||||
}
|
||||
|
||||
/// Returns the [`TaggedHash`] of the invoice to sign.
|
||||
@ -528,6 +573,17 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s
|
||||
};
|
||||
signature_tlv_stream.write(&mut $self.bytes).unwrap();
|
||||
|
||||
// Append the experimental bytes after the signature.
|
||||
debug_assert_eq!(
|
||||
// The two-byte overallocation results from SIGNATURE_TLV_RECORD_SIZE accommodating TLV
|
||||
// records with types >= 253.
|
||||
$self.bytes.len()
|
||||
+ $self.experimental_bytes.len()
|
||||
+ if $self.contents.is_for_offer() { 0 } else { 2 },
|
||||
$self.bytes.capacity(),
|
||||
);
|
||||
$self.bytes.extend_from_slice(&$self.experimental_bytes);
|
||||
|
||||
Ok(Bolt12Invoice {
|
||||
#[cfg(not(c_bindings))]
|
||||
bytes: $self.bytes,
|
||||
@ -612,6 +668,8 @@ struct InvoiceFields {
|
||||
fallbacks: Option<Vec<FallbackAddress>>,
|
||||
features: Bolt12InvoiceFeatures,
|
||||
signing_pubkey: PublicKey,
|
||||
#[cfg(test)]
|
||||
experimental_baz: Option<u64>,
|
||||
}
|
||||
|
||||
macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
|
||||
@ -828,7 +886,7 @@ impl Bolt12Invoice {
|
||||
(&refund.payer.0, REFUND_IV_BYTES_WITH_METADATA)
|
||||
},
|
||||
};
|
||||
self.contents.verify(TlvStream::new(&self.bytes), metadata, key, iv_bytes, secp_ctx)
|
||||
self.contents.verify(&self.bytes, metadata, key, iv_bytes, secp_ctx)
|
||||
}
|
||||
|
||||
/// Verifies that the invoice was for a request or refund created using the given key by
|
||||
@ -842,7 +900,8 @@ impl Bolt12Invoice {
|
||||
InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES,
|
||||
InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES_WITHOUT_METADATA,
|
||||
};
|
||||
self.contents.verify(TlvStream::new(&self.bytes), &metadata, key, iv_bytes, secp_ctx)
|
||||
self.contents
|
||||
.verify(&self.bytes, &metadata, key, iv_bytes, secp_ctx)
|
||||
.and_then(|extracted_payment_id| (payment_id == extracted_payment_id)
|
||||
.then(|| payment_id)
|
||||
.ok_or(())
|
||||
@ -850,13 +909,19 @@ impl Bolt12Invoice {
|
||||
}
|
||||
|
||||
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
|
||||
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) =
|
||||
self.contents.as_tlv_stream();
|
||||
let (
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
) = self.contents.as_tlv_stream();
|
||||
let signature_tlv_stream = SignatureTlvStreamRef {
|
||||
signature: Some(&self.signature),
|
||||
};
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
|
||||
signature_tlv_stream)
|
||||
(
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
|
||||
signature_tlv_stream, experimental_offer_tlv_stream,
|
||||
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_for_refund_without_paths(&self) -> bool {
|
||||
@ -882,6 +947,13 @@ impl Hash for Bolt12Invoice {
|
||||
}
|
||||
|
||||
impl InvoiceContents {
|
||||
fn is_for_offer(&self) -> bool {
|
||||
match self {
|
||||
InvoiceContents::ForOffer { .. } => true,
|
||||
InvoiceContents::ForRefund { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the original offer or refund has expired.
|
||||
#[cfg(feature = "std")]
|
||||
fn is_offer_or_refund_expired(&self) -> bool {
|
||||
@ -1086,18 +1158,22 @@ impl InvoiceContents {
|
||||
}
|
||||
|
||||
fn verify<T: secp256k1::Signing>(
|
||||
&self, tlv_stream: TlvStream<'_>, metadata: &Metadata, key: &ExpandedKey,
|
||||
iv_bytes: &[u8; IV_LEN], secp_ctx: &Secp256k1<T>
|
||||
&self, bytes: &[u8], metadata: &Metadata, key: &ExpandedKey, iv_bytes: &[u8; IV_LEN],
|
||||
secp_ctx: &Secp256k1<T>,
|
||||
) -> Result<PaymentId, ()> {
|
||||
let offer_records = tlv_stream.clone().range(OFFER_TYPES);
|
||||
let invreq_records = tlv_stream.range(INVOICE_REQUEST_TYPES).filter(|record| {
|
||||
const EXPERIMENTAL_TYPES: core::ops::Range<u64> =
|
||||
EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end;
|
||||
|
||||
let offer_records = TlvStream::new(bytes).range(OFFER_TYPES);
|
||||
let invreq_records = TlvStream::new(bytes).range(INVOICE_REQUEST_TYPES).filter(|record| {
|
||||
match record.r#type {
|
||||
PAYER_METADATA_TYPE => false, // Should be outside range
|
||||
INVOICE_REQUEST_PAYER_ID_TYPE => !metadata.derives_payer_keys(),
|
||||
_ => true,
|
||||
}
|
||||
});
|
||||
let tlv_stream = offer_records.chain(invreq_records);
|
||||
let experimental_records = TlvStream::new(bytes).range(EXPERIMENTAL_TYPES);
|
||||
let tlv_stream = offer_records.chain(invreq_records).chain(experimental_records);
|
||||
|
||||
let signing_pubkey = self.payer_signing_pubkey();
|
||||
signer::verify_payer_metadata(
|
||||
@ -1106,13 +1182,18 @@ impl InvoiceContents {
|
||||
}
|
||||
|
||||
fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef {
|
||||
let (payer, offer, invoice_request) = match self {
|
||||
let (
|
||||
payer, offer, invoice_request, experimental_offer, experimental_invoice_request,
|
||||
) = match self {
|
||||
InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.as_tlv_stream(),
|
||||
InvoiceContents::ForRefund { refund, .. } => refund.as_tlv_stream(),
|
||||
};
|
||||
let invoice = self.fields().as_tlv_stream();
|
||||
let (invoice, experimental_invoice) = self.fields().as_tlv_stream();
|
||||
|
||||
(payer, offer, invoice_request, invoice)
|
||||
(
|
||||
payer, offer, invoice_request, invoice, experimental_offer,
|
||||
experimental_invoice_request, experimental_invoice,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1160,24 +1241,30 @@ pub(super) fn filter_fallbacks(
|
||||
}
|
||||
|
||||
impl InvoiceFields {
|
||||
fn as_tlv_stream(&self) -> InvoiceTlvStreamRef {
|
||||
fn as_tlv_stream(&self) -> (InvoiceTlvStreamRef, ExperimentalInvoiceTlvStreamRef) {
|
||||
let features = {
|
||||
if self.features == Bolt12InvoiceFeatures::empty() { None }
|
||||
else { Some(&self.features) }
|
||||
};
|
||||
|
||||
InvoiceTlvStreamRef {
|
||||
paths: Some(Iterable(self.payment_paths.iter().map(|path| path.inner_blinded_path()))),
|
||||
blindedpay: Some(Iterable(self.payment_paths.iter().map(|path| &path.payinfo))),
|
||||
created_at: Some(self.created_at.as_secs()),
|
||||
relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
|
||||
payment_hash: Some(&self.payment_hash),
|
||||
amount: Some(self.amount_msats),
|
||||
fallbacks: self.fallbacks.as_ref(),
|
||||
features,
|
||||
node_id: Some(&self.signing_pubkey),
|
||||
message_paths: None,
|
||||
}
|
||||
(
|
||||
InvoiceTlvStreamRef {
|
||||
paths: Some(Iterable(self.payment_paths.iter().map(|path| path.inner_blinded_path()))),
|
||||
blindedpay: Some(Iterable(self.payment_paths.iter().map(|path| &path.payinfo))),
|
||||
created_at: Some(self.created_at.as_secs()),
|
||||
relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32),
|
||||
payment_hash: Some(&self.payment_hash),
|
||||
amount: Some(self.amount_msats),
|
||||
fallbacks: self.fallbacks.as_ref(),
|
||||
features,
|
||||
node_id: Some(&self.signing_pubkey),
|
||||
message_paths: None,
|
||||
},
|
||||
ExperimentalInvoiceTlvStreamRef {
|
||||
#[cfg(test)]
|
||||
experimental_baz: self.experimental_baz,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1211,17 +1298,18 @@ impl TryFrom<Vec<u8>> for UnsignedBolt12Invoice {
|
||||
|
||||
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
let invoice = ParsedMessage::<PartialInvoiceTlvStream>::try_from(bytes)?;
|
||||
let ParsedMessage { bytes, tlv_stream } = invoice;
|
||||
let (
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
|
||||
) = tlv_stream;
|
||||
let contents = InvoiceContents::try_from(
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream)
|
||||
)?;
|
||||
let ParsedMessage { mut bytes, tlv_stream } = invoice;
|
||||
let contents = InvoiceContents::try_from(tlv_stream)?;
|
||||
|
||||
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
|
||||
|
||||
Ok(UnsignedBolt12Invoice { bytes, contents, tagged_hash })
|
||||
let offset = TlvStream::new(&bytes)
|
||||
.range(0..INVOICE_TYPES.end)
|
||||
.last()
|
||||
.map_or(0, |last_record| last_record.end);
|
||||
let experimental_bytes = bytes.split_off(offset);
|
||||
|
||||
Ok(UnsignedBolt12Invoice { bytes, experimental_bytes, contents, tagged_hash })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1234,7 +1322,10 @@ impl TryFrom<Vec<u8>> for Bolt12Invoice {
|
||||
}
|
||||
}
|
||||
|
||||
tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
|
||||
/// Valid type range for invoice TLV records.
|
||||
pub(super) const INVOICE_TYPES: core::ops::Range<u64> = 160..240;
|
||||
|
||||
tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef<'a>, INVOICE_TYPES, {
|
||||
(160, paths: (Vec<BlindedPath>, WithoutLength, Iterable<'a, BlindedPathIter<'a>, BlindedPath>)),
|
||||
(162, blindedpay: (Vec<BlindedPayInfo>, WithoutLength, Iterable<'a, BlindedPayInfoIter<'a>, BlindedPayInfo>)),
|
||||
(164, created_at: (u64, HighZeroBytesDroppedBigSize)),
|
||||
@ -1245,9 +1336,24 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
|
||||
(174, features: (Bolt12InvoiceFeatures, WithoutLength)),
|
||||
(176, node_id: PublicKey),
|
||||
// Only present in `StaticInvoice`s.
|
||||
(238, message_paths: (Vec<BlindedMessagePath>, WithoutLength)),
|
||||
(236, message_paths: (Vec<BlindedMessagePath>, WithoutLength)),
|
||||
});
|
||||
|
||||
/// Valid type range for experimental invoice TLV records.
|
||||
pub(super) const EXPERIMENTAL_INVOICE_TYPES: core::ops::RangeFrom<u64> = 3_000_000_000..;
|
||||
|
||||
#[cfg(not(test))]
|
||||
tlv_stream!(
|
||||
ExperimentalInvoiceTlvStream, ExperimentalInvoiceTlvStreamRef, EXPERIMENTAL_INVOICE_TYPES, {}
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
tlv_stream!(
|
||||
ExperimentalInvoiceTlvStream, ExperimentalInvoiceTlvStreamRef, EXPERIMENTAL_INVOICE_TYPES, {
|
||||
(3_999_999_999, experimental_baz: (u64, HighZeroBytesDroppedBigSize)),
|
||||
}
|
||||
);
|
||||
|
||||
pub(super) type BlindedPathIter<'a> = core::iter::Map<
|
||||
core::slice::Iter<'a, BlindedPaymentPath>,
|
||||
for<'r> fn(&'r BlindedPaymentPath) -> &'r BlindedPath,
|
||||
@ -1267,8 +1373,10 @@ pub(super) struct FallbackAddress {
|
||||
|
||||
impl_writeable!(FallbackAddress, { version, program });
|
||||
|
||||
type FullInvoiceTlvStream =
|
||||
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream);
|
||||
type FullInvoiceTlvStream =(
|
||||
PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream,
|
||||
ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceTlvStream,
|
||||
);
|
||||
|
||||
type FullInvoiceTlvStreamRef<'a> = (
|
||||
PayerTlvStreamRef<'a>,
|
||||
@ -1276,6 +1384,9 @@ type FullInvoiceTlvStreamRef<'a> = (
|
||||
InvoiceRequestTlvStreamRef<'a>,
|
||||
InvoiceTlvStreamRef<'a>,
|
||||
SignatureTlvStreamRef<'a>,
|
||||
ExperimentalOfferTlvStreamRef,
|
||||
ExperimentalInvoiceRequestTlvStreamRef,
|
||||
ExperimentalInvoiceTlvStreamRef,
|
||||
);
|
||||
|
||||
impl CursorReadable for FullInvoiceTlvStream {
|
||||
@ -1285,19 +1396,32 @@ impl CursorReadable for FullInvoiceTlvStream {
|
||||
let invoice_request = CursorReadable::read(r)?;
|
||||
let invoice = CursorReadable::read(r)?;
|
||||
let signature = CursorReadable::read(r)?;
|
||||
let experimental_offer = CursorReadable::read(r)?;
|
||||
let experimental_invoice_request = CursorReadable::read(r)?;
|
||||
let experimental_invoice = CursorReadable::read(r)?;
|
||||
|
||||
Ok((payer, offer, invoice_request, invoice, signature))
|
||||
Ok(
|
||||
(
|
||||
payer, offer, invoice_request, invoice, signature, experimental_offer,
|
||||
experimental_invoice_request, experimental_invoice,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type PartialInvoiceTlvStream =
|
||||
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream);
|
||||
type PartialInvoiceTlvStream = (
|
||||
PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream,
|
||||
ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceTlvStream,
|
||||
);
|
||||
|
||||
type PartialInvoiceTlvStreamRef<'a> = (
|
||||
PayerTlvStreamRef<'a>,
|
||||
OfferTlvStreamRef<'a>,
|
||||
InvoiceRequestTlvStreamRef<'a>,
|
||||
InvoiceTlvStreamRef<'a>,
|
||||
ExperimentalOfferTlvStreamRef,
|
||||
ExperimentalInvoiceRequestTlvStreamRef,
|
||||
ExperimentalInvoiceTlvStreamRef,
|
||||
);
|
||||
|
||||
impl CursorReadable for PartialInvoiceTlvStream {
|
||||
@ -1306,8 +1430,16 @@ impl CursorReadable for PartialInvoiceTlvStream {
|
||||
let offer = CursorReadable::read(r)?;
|
||||
let invoice_request = CursorReadable::read(r)?;
|
||||
let invoice = CursorReadable::read(r)?;
|
||||
let experimental_offer = CursorReadable::read(r)?;
|
||||
let experimental_invoice_request = CursorReadable::read(r)?;
|
||||
let experimental_invoice = CursorReadable::read(r)?;
|
||||
|
||||
Ok((payer, offer, invoice_request, invoice))
|
||||
Ok(
|
||||
(
|
||||
payer, offer, invoice_request, invoice, experimental_offer,
|
||||
experimental_invoice_request, experimental_invoice,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1319,9 +1451,16 @@ impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Bolt12Invoice {
|
||||
let (
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
|
||||
SignatureTlvStream { signature },
|
||||
experimental_offer_tlv_stream,
|
||||
experimental_invoice_request_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
) = tlv_stream;
|
||||
let contents = InvoiceContents::try_from(
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream)
|
||||
(
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
)
|
||||
)?;
|
||||
|
||||
let signature = signature.ok_or(
|
||||
@ -1347,6 +1486,12 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
|
||||
paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks,
|
||||
features, node_id, message_paths,
|
||||
},
|
||||
experimental_offer_tlv_stream,
|
||||
experimental_invoice_request_tlv_stream,
|
||||
ExperimentalInvoiceTlvStream {
|
||||
#[cfg(test)]
|
||||
experimental_baz,
|
||||
},
|
||||
) = tlv_stream;
|
||||
|
||||
if message_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths) }
|
||||
@ -1373,18 +1518,26 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
|
||||
let fields = InvoiceFields {
|
||||
payment_paths, created_at, relative_expiry, payment_hash, amount_msats, fallbacks,
|
||||
features, signing_pubkey,
|
||||
#[cfg(test)]
|
||||
experimental_baz,
|
||||
};
|
||||
|
||||
check_invoice_signing_pubkey(&fields.signing_pubkey, &offer_tlv_stream)?;
|
||||
|
||||
if offer_tlv_stream.issuer_id.is_none() && offer_tlv_stream.paths.is_none() {
|
||||
let refund = RefundContents::try_from(
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
|
||||
(
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
)
|
||||
)?;
|
||||
Ok(InvoiceContents::ForRefund { refund, fields })
|
||||
} else {
|
||||
let invoice_request = InvoiceRequestContents::try_from(
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
|
||||
(
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
)
|
||||
)?;
|
||||
Ok(InvoiceContents::ForOffer { invoice_request, fields })
|
||||
}
|
||||
@ -1437,7 +1590,7 @@ pub(super) fn check_invoice_signing_pubkey(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice};
|
||||
use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, EXPERIMENTAL_INVOICE_TYPES, ExperimentalInvoiceTlvStreamRef, FallbackAddress, FullInvoiceTlvStreamRef, INVOICE_TYPES, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice};
|
||||
|
||||
use bitcoin::{CompressedPublicKey, WitnessProgram, WitnessVersion};
|
||||
use bitcoin::constants::ChainHash;
|
||||
@ -1456,10 +1609,10 @@ mod tests {
|
||||
use crate::types::features::{Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures};
|
||||
use crate::ln::inbound_payment::ExpandedKey;
|
||||
use crate::ln::msgs::DecodeError;
|
||||
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
|
||||
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self};
|
||||
use crate::offers::invoice_request::{ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequestTlvStreamRef};
|
||||
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, TlvStream, self};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::offer::{Amount, OfferTlvStreamRef, Quantity};
|
||||
use crate::offers::offer::{Amount, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity};
|
||||
use crate::prelude::*;
|
||||
#[cfg(not(c_bindings))]
|
||||
use {
|
||||
@ -1627,6 +1780,15 @@ mod tests {
|
||||
message_paths: None,
|
||||
},
|
||||
SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
|
||||
ExperimentalOfferTlvStreamRef {
|
||||
experimental_foo: None,
|
||||
},
|
||||
ExperimentalInvoiceRequestTlvStreamRef {
|
||||
experimental_bar: None,
|
||||
},
|
||||
ExperimentalInvoiceTlvStreamRef {
|
||||
experimental_baz: None,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -1720,6 +1882,15 @@ mod tests {
|
||||
message_paths: None,
|
||||
},
|
||||
SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
|
||||
ExperimentalOfferTlvStreamRef {
|
||||
experimental_foo: None,
|
||||
},
|
||||
ExperimentalInvoiceRequestTlvStreamRef {
|
||||
experimental_bar: None,
|
||||
},
|
||||
ExperimentalInvoiceTlvStreamRef {
|
||||
experimental_baz: None,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -1813,6 +1984,7 @@ mod tests {
|
||||
let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
|
||||
.amount_msats(1000)
|
||||
.path(blinded_path)
|
||||
.experimental_foo(42)
|
||||
.build().unwrap();
|
||||
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
|
||||
.build().unwrap()
|
||||
@ -1834,6 +2006,7 @@ mod tests {
|
||||
let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
|
||||
.amount_msats(1000)
|
||||
// Omit the path so that node_id is used for the signing pubkey instead of deriving it
|
||||
.experimental_foo(42)
|
||||
.build().unwrap();
|
||||
let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
|
||||
.build().unwrap()
|
||||
@ -1855,6 +2028,7 @@ mod tests {
|
||||
let secp_ctx = Secp256k1::new();
|
||||
|
||||
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.experimental_foo(42)
|
||||
.build().unwrap();
|
||||
|
||||
if let Err(e) = refund
|
||||
@ -1913,7 +2087,7 @@ mod tests {
|
||||
.relative_expiry(one_hour.as_secs() as u32)
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
|
||||
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
|
||||
#[cfg(feature = "std")]
|
||||
assert!(!invoice.is_expired());
|
||||
assert_eq!(invoice.relative_expiry(), one_hour);
|
||||
@ -1929,7 +2103,7 @@ mod tests {
|
||||
.relative_expiry(one_hour.as_secs() as u32 - 1)
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
|
||||
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
|
||||
#[cfg(feature = "std")]
|
||||
assert!(invoice.is_expired());
|
||||
assert_eq!(invoice.relative_expiry(), one_hour - Duration::from_secs(1));
|
||||
@ -1948,7 +2122,7 @@ mod tests {
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
|
||||
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
|
||||
assert_eq!(invoice.amount_msats(), 1001);
|
||||
assert_eq!(tlv_stream.amount, Some(1001));
|
||||
}
|
||||
@ -1966,7 +2140,7 @@ mod tests {
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
|
||||
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
|
||||
assert_eq!(invoice.amount_msats(), 2000);
|
||||
assert_eq!(tlv_stream.amount, Some(2000));
|
||||
|
||||
@ -2004,7 +2178,7 @@ mod tests {
|
||||
.fallback_v1_p2tr_tweaked(&tweaked_pubkey)
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
|
||||
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
|
||||
assert_eq!(
|
||||
invoice.fallbacks(),
|
||||
vec![
|
||||
@ -2047,7 +2221,7 @@ mod tests {
|
||||
.allow_mpp()
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
let (_, _, _, tlv_stream, _) = invoice.as_tlv_stream();
|
||||
let (_, _, _, tlv_stream, _, _, _, _) = invoice.as_tlv_stream();
|
||||
assert_eq!(invoice.invoice_features(), &features);
|
||||
assert_eq!(tlv_stream.features, Some(&features));
|
||||
}
|
||||
@ -2494,7 +2668,217 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_invoice_with_extra_tlv_records() {
|
||||
fn parses_invoice_with_unknown_tlv_records() {
|
||||
const UNKNOWN_ODD_TYPE: u64 = INVOICE_TYPES.end - 1;
|
||||
assert!(UNKNOWN_ODD_TYPE % 2 == 1);
|
||||
|
||||
let secp_ctx = Secp256k1::new();
|
||||
let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
|
||||
let mut unsigned_invoice = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap()
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice.bytes.reserve_exact(
|
||||
unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice.bytes.extend_from_slice(&unknown_bytes);
|
||||
unsigned_invoice.tagged_hash =
|
||||
TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes);
|
||||
|
||||
let invoice = unsigned_invoice
|
||||
.sign(|message: &UnsignedBolt12Invoice|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match Bolt12Invoice::try_from(encoded_invoice.clone()) {
|
||||
Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice),
|
||||
Err(e) => panic!("error parsing invoice: {:?}", e),
|
||||
}
|
||||
|
||||
const UNKNOWN_EVEN_TYPE: u64 = INVOICE_TYPES.end - 2;
|
||||
assert!(UNKNOWN_EVEN_TYPE % 2 == 0);
|
||||
|
||||
let mut unsigned_invoice = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap()
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice.bytes.reserve_exact(
|
||||
unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice.bytes.extend_from_slice(&unknown_bytes);
|
||||
unsigned_invoice.tagged_hash =
|
||||
TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes);
|
||||
|
||||
let invoice = unsigned_invoice
|
||||
.sign(|message: &UnsignedBolt12Invoice|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match Bolt12Invoice::try_from(encoded_invoice) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_invoice_with_experimental_tlv_records() {
|
||||
let secp_ctx = Secp256k1::new();
|
||||
let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
|
||||
let invoice = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap()
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.experimental_baz(42)
|
||||
.build().unwrap()
|
||||
.sign(|message: &UnsignedBolt12Invoice|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
assert!(Bolt12Invoice::try_from(encoded_invoice).is_ok());
|
||||
|
||||
const UNKNOWN_ODD_TYPE: u64 = EXPERIMENTAL_INVOICE_TYPES.start + 1;
|
||||
assert!(UNKNOWN_ODD_TYPE % 2 == 1);
|
||||
|
||||
let mut unsigned_invoice = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap()
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice.bytes.reserve_exact(
|
||||
unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice.experimental_bytes.extend_from_slice(&unknown_bytes);
|
||||
|
||||
let tlv_stream = TlvStream::new(&unsigned_invoice.bytes)
|
||||
.chain(TlvStream::new(&unsigned_invoice.experimental_bytes));
|
||||
unsigned_invoice.tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
|
||||
|
||||
let invoice = unsigned_invoice
|
||||
.sign(|message: &UnsignedBolt12Invoice|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match Bolt12Invoice::try_from(encoded_invoice.clone()) {
|
||||
Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice),
|
||||
Err(e) => panic!("error parsing invoice: {:?}", e),
|
||||
}
|
||||
|
||||
const UNKNOWN_EVEN_TYPE: u64 = EXPERIMENTAL_INVOICE_TYPES.start;
|
||||
assert!(UNKNOWN_EVEN_TYPE % 2 == 0);
|
||||
|
||||
let mut unsigned_invoice = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap()
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice.bytes.reserve_exact(
|
||||
unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice.experimental_bytes.extend_from_slice(&unknown_bytes);
|
||||
|
||||
let tlv_stream = TlvStream::new(&unsigned_invoice.bytes)
|
||||
.chain(TlvStream::new(&unsigned_invoice.experimental_bytes));
|
||||
unsigned_invoice.tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
|
||||
|
||||
let invoice = unsigned_invoice
|
||||
.sign(|message: &UnsignedBolt12Invoice|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match Bolt12Invoice::try_from(encoded_invoice) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
|
||||
let invoice = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap()
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(|message: &UnsignedBolt12Invoice|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_invoice).unwrap();
|
||||
BigSize(32).write(&mut encoded_invoice).unwrap();
|
||||
[42u8; 32].write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match Bolt12Invoice::try_from(encoded_invoice) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSignature(secp256k1::Error::IncorrectSignature)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_invoice_with_out_of_range_tlv_records() {
|
||||
let invoice = OfferBuilder::new(recipient_pubkey())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
|
@ -95,6 +95,11 @@ macro_rules! invoice_builder_methods_test { (
|
||||
$return_value
|
||||
}
|
||||
|
||||
#[cfg_attr(c_bindings, allow(dead_code))]
|
||||
pub(super) fn experimental_baz($($self_mut)* $self: $self_type, experimental_baz: u64) -> $return_type {
|
||||
$invoice_fields.experimental_baz = Some(experimental_baz);
|
||||
$return_value
|
||||
}
|
||||
} }
|
||||
|
||||
macro_rules! invoice_accessors_common { ($self: ident, $contents: expr, $invoice_type: ty) => {
|
||||
|
@ -69,9 +69,9 @@ use crate::ln::channelmanager::PaymentId;
|
||||
use crate::types::features::InvoiceRequestFeatures;
|
||||
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN};
|
||||
use crate::ln::msgs::DecodeError;
|
||||
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, self};
|
||||
use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, self, SIGNATURE_TLV_RECORD_SIZE};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::offer::{Offer, OfferContents, OfferId, OfferTlvStream, OfferTlvStreamRef};
|
||||
use crate::offers::offer::{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};
|
||||
@ -241,6 +241,8 @@ macro_rules! invoice_request_builder_methods { (
|
||||
InvoiceRequestContentsWithoutPayerSigningPubkey {
|
||||
payer: PayerContents(metadata), offer, chain: None, amount_msats: None,
|
||||
features: InvoiceRequestFeatures::empty(), quantity: None, payer_note: None,
|
||||
#[cfg(test)]
|
||||
experimental_bar: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,6 +406,12 @@ macro_rules! invoice_request_builder_test_methods { (
|
||||
$return_value
|
||||
}
|
||||
|
||||
#[cfg_attr(c_bindings, allow(dead_code))]
|
||||
pub(super) fn experimental_bar($($self_mut)* $self: $self_type, experimental_bar: u64) -> $return_type {
|
||||
$self.invoice_request.experimental_bar = Some(experimental_bar);
|
||||
$return_value
|
||||
}
|
||||
|
||||
#[cfg_attr(c_bindings, allow(dead_code))]
|
||||
pub(super) fn build_unchecked($self: $self_type) -> UnsignedInvoiceRequest {
|
||||
$self.build_without_checks().0
|
||||
@ -488,6 +496,7 @@ for InvoiceRequestBuilder<'a, 'b, DerivedPayerSigningPubkey, secp256k1::All> {
|
||||
#[derive(Clone)]
|
||||
pub struct UnsignedInvoiceRequest {
|
||||
bytes: Vec<u8>,
|
||||
experimental_bytes: Vec<u8>,
|
||||
contents: InvoiceRequestContents,
|
||||
tagged_hash: TaggedHash,
|
||||
}
|
||||
@ -520,17 +529,55 @@ impl UnsignedInvoiceRequest {
|
||||
fn new(offer: &Offer, contents: InvoiceRequestContents) -> Self {
|
||||
// Use the offer bytes instead of the offer TLV stream as the offer may have contained
|
||||
// unknown TLV records, which are not stored in `OfferContents`.
|
||||
let (payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream) =
|
||||
contents.as_tlv_stream();
|
||||
let offer_bytes = WithoutLength(&offer.bytes);
|
||||
let unsigned_tlv_stream = (payer_tlv_stream, offer_bytes, invoice_request_tlv_stream);
|
||||
let (
|
||||
payer_tlv_stream, _offer_tlv_stream, invoice_request_tlv_stream,
|
||||
_experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
) = contents.as_tlv_stream();
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
unsigned_tlv_stream.write(&mut bytes).unwrap();
|
||||
// Allocate enough space for the invoice_request, which will include:
|
||||
// - all TLV records from `offer.bytes`,
|
||||
// - all invoice_request-specific TLV records, and
|
||||
// - a signature TLV record once the invoice_request is signed.
|
||||
let mut bytes = Vec::with_capacity(
|
||||
offer.bytes.len()
|
||||
+ payer_tlv_stream.serialized_length()
|
||||
+ invoice_request_tlv_stream.serialized_length()
|
||||
+ SIGNATURE_TLV_RECORD_SIZE
|
||||
+ experimental_invoice_request_tlv_stream.serialized_length(),
|
||||
);
|
||||
|
||||
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
|
||||
payer_tlv_stream.write(&mut bytes).unwrap();
|
||||
|
||||
Self { bytes, contents, tagged_hash }
|
||||
for record in TlvStream::new(&offer.bytes).range(OFFER_TYPES) {
|
||||
record.write(&mut bytes).unwrap();
|
||||
}
|
||||
|
||||
let remaining_bytes = &offer.bytes[bytes.len() - payer_tlv_stream.serialized_length()..];
|
||||
|
||||
invoice_request_tlv_stream.write(&mut bytes).unwrap();
|
||||
|
||||
let mut experimental_tlv_stream = TlvStream::new(remaining_bytes)
|
||||
.range(EXPERIMENTAL_OFFER_TYPES)
|
||||
.peekable();
|
||||
let mut experimental_bytes = Vec::with_capacity(
|
||||
remaining_bytes.len()
|
||||
- experimental_tlv_stream
|
||||
.peek()
|
||||
.map_or(remaining_bytes.len(), |first_record| first_record.start)
|
||||
+ experimental_invoice_request_tlv_stream.serialized_length(),
|
||||
);
|
||||
|
||||
for record in experimental_tlv_stream {
|
||||
record.write(&mut experimental_bytes).unwrap();
|
||||
}
|
||||
|
||||
experimental_invoice_request_tlv_stream.write(&mut experimental_bytes).unwrap();
|
||||
debug_assert_eq!(experimental_bytes.len(), experimental_bytes.capacity());
|
||||
|
||||
let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
|
||||
let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
|
||||
|
||||
Self { bytes, experimental_bytes, contents, tagged_hash }
|
||||
}
|
||||
|
||||
/// Returns the [`TaggedHash`] of the invoice to sign.
|
||||
@ -557,6 +604,15 @@ macro_rules! unsigned_invoice_request_sign_method { (
|
||||
};
|
||||
signature_tlv_stream.write(&mut $self.bytes).unwrap();
|
||||
|
||||
// Append the experimental bytes after the signature.
|
||||
debug_assert_eq!(
|
||||
// The two-byte overallocation results from SIGNATURE_TLV_RECORD_SIZE accommodating TLV
|
||||
// records with types >= 253.
|
||||
$self.bytes.len() + $self.experimental_bytes.len() + 2,
|
||||
$self.bytes.capacity(),
|
||||
);
|
||||
$self.bytes.extend_from_slice(&$self.experimental_bytes);
|
||||
|
||||
Ok(InvoiceRequest {
|
||||
#[cfg(not(c_bindings))]
|
||||
bytes: $self.bytes,
|
||||
@ -643,6 +699,8 @@ pub(super) struct InvoiceRequestContentsWithoutPayerSigningPubkey {
|
||||
features: InvoiceRequestFeatures,
|
||||
quantity: Option<u64>,
|
||||
payer_note: Option<String>,
|
||||
#[cfg(test)]
|
||||
experimental_bar: Option<u64>,
|
||||
}
|
||||
|
||||
macro_rules! invoice_request_accessors { ($self: ident, $contents: expr) => {
|
||||
@ -863,12 +921,18 @@ impl InvoiceRequest {
|
||||
}
|
||||
|
||||
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceRequestTlvStreamRef {
|
||||
let (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) =
|
||||
self.contents.as_tlv_stream();
|
||||
let (
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
) = self.contents.as_tlv_stream();
|
||||
let signature_tlv_stream = SignatureTlvStreamRef {
|
||||
signature: Some(&self.signature),
|
||||
};
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, signature_tlv_stream)
|
||||
(
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
|
||||
signature_tlv_stream, experimental_offer_tlv_stream,
|
||||
experimental_invoice_request_tlv_stream,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -940,7 +1004,9 @@ impl VerifiedInvoiceRequest {
|
||||
let InvoiceRequestContents {
|
||||
payer_signing_pubkey,
|
||||
inner: InvoiceRequestContentsWithoutPayerSigningPubkey {
|
||||
payer: _, offer: _, chain: _, amount_msats: _, features: _, quantity, payer_note
|
||||
payer: _, offer: _, chain: _, amount_msats: _, features: _, quantity, payer_note,
|
||||
#[cfg(test)]
|
||||
experimental_bar: _,
|
||||
},
|
||||
} = &self.inner.contents;
|
||||
|
||||
@ -984,9 +1050,10 @@ impl InvoiceRequestContents {
|
||||
}
|
||||
|
||||
pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
|
||||
let (payer, offer, mut invoice_request) = self.inner.as_tlv_stream();
|
||||
let (payer, offer, mut invoice_request, experimental_offer, experimental_invoice_request) =
|
||||
self.inner.as_tlv_stream();
|
||||
invoice_request.payer_id = Some(&self.payer_signing_pubkey);
|
||||
(payer, offer, invoice_request)
|
||||
(payer, offer, invoice_request, experimental_offer, experimental_invoice_request)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1004,7 +1071,7 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey {
|
||||
metadata: self.payer.0.as_bytes(),
|
||||
};
|
||||
|
||||
let offer = self.offer.as_tlv_stream();
|
||||
let (offer, experimental_offer) = self.offer.as_tlv_stream();
|
||||
|
||||
let features = {
|
||||
if self.features == InvoiceRequestFeatures::empty() { None }
|
||||
@ -1021,7 +1088,12 @@ impl InvoiceRequestContentsWithoutPayerSigningPubkey {
|
||||
paths: None,
|
||||
};
|
||||
|
||||
(payer, offer, invoice_request)
|
||||
let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef {
|
||||
#[cfg(test)]
|
||||
experimental_bar: self.experimental_bar,
|
||||
};
|
||||
|
||||
(payer, offer, invoice_request, experimental_offer, experimental_invoice_request)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1061,7 +1133,7 @@ pub(super) const INVOICE_REQUEST_PAYER_ID_TYPE: u64 = 88;
|
||||
|
||||
// This TLV stream is used for both InvoiceRequest and Refund, but not all TLV records are valid for
|
||||
// InvoiceRequest as noted below.
|
||||
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, {
|
||||
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef<'a>, INVOICE_REQUEST_TYPES, {
|
||||
(80, chain: ChainHash),
|
||||
(82, amount: (u64, HighZeroBytesDroppedBigSize)),
|
||||
(84, features: (InvoiceRequestFeatures, WithoutLength)),
|
||||
@ -1072,14 +1144,36 @@ tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, INVOICE_REQUEST
|
||||
(90, paths: (Vec<BlindedMessagePath>, WithoutLength)),
|
||||
});
|
||||
|
||||
type FullInvoiceRequestTlvStream =
|
||||
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream);
|
||||
/// Valid type range for experimental invoice_request TLV records.
|
||||
pub(super) const EXPERIMENTAL_INVOICE_REQUEST_TYPES: core::ops::Range<u64> =
|
||||
2_000_000_000..3_000_000_000;
|
||||
|
||||
#[cfg(not(test))]
|
||||
tlv_stream!(
|
||||
ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef,
|
||||
EXPERIMENTAL_INVOICE_REQUEST_TYPES, {}
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
tlv_stream!(
|
||||
ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef,
|
||||
EXPERIMENTAL_INVOICE_REQUEST_TYPES, {
|
||||
(2_999_999_999, experimental_bar: (u64, HighZeroBytesDroppedBigSize)),
|
||||
}
|
||||
);
|
||||
|
||||
type FullInvoiceRequestTlvStream = (
|
||||
PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream,
|
||||
ExperimentalOfferTlvStream, ExperimentalInvoiceRequestTlvStream,
|
||||
);
|
||||
|
||||
type FullInvoiceRequestTlvStreamRef<'a> = (
|
||||
PayerTlvStreamRef<'a>,
|
||||
OfferTlvStreamRef<'a>,
|
||||
InvoiceRequestTlvStreamRef<'a>,
|
||||
SignatureTlvStreamRef<'a>,
|
||||
ExperimentalOfferTlvStreamRef,
|
||||
ExperimentalInvoiceRequestTlvStreamRef,
|
||||
);
|
||||
|
||||
impl CursorReadable for FullInvoiceRequestTlvStream {
|
||||
@ -1088,17 +1182,29 @@ impl CursorReadable for FullInvoiceRequestTlvStream {
|
||||
let offer = CursorReadable::read(r)?;
|
||||
let invoice_request = CursorReadable::read(r)?;
|
||||
let signature = CursorReadable::read(r)?;
|
||||
let experimental_offer = CursorReadable::read(r)?;
|
||||
let experimental_invoice_request = CursorReadable::read(r)?;
|
||||
|
||||
Ok((payer, offer, invoice_request, signature))
|
||||
Ok(
|
||||
(
|
||||
payer, offer, invoice_request, signature, experimental_offer,
|
||||
experimental_invoice_request,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream);
|
||||
type PartialInvoiceRequestTlvStream = (
|
||||
PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, ExperimentalOfferTlvStream,
|
||||
ExperimentalInvoiceRequestTlvStream,
|
||||
);
|
||||
|
||||
type PartialInvoiceRequestTlvStreamRef<'a> = (
|
||||
PayerTlvStreamRef<'a>,
|
||||
OfferTlvStreamRef<'a>,
|
||||
InvoiceRequestTlvStreamRef<'a>,
|
||||
ExperimentalOfferTlvStreamRef,
|
||||
ExperimentalInvoiceRequestTlvStreamRef,
|
||||
);
|
||||
|
||||
impl TryFrom<Vec<u8>> for UnsignedInvoiceRequest {
|
||||
@ -1106,17 +1212,18 @@ impl TryFrom<Vec<u8>> for UnsignedInvoiceRequest {
|
||||
|
||||
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
let invoice_request = ParsedMessage::<PartialInvoiceRequestTlvStream>::try_from(bytes)?;
|
||||
let ParsedMessage { bytes, tlv_stream } = invoice_request;
|
||||
let (
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
|
||||
) = tlv_stream;
|
||||
let contents = InvoiceRequestContents::try_from(
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
|
||||
)?;
|
||||
let ParsedMessage { mut bytes, tlv_stream } = invoice_request;
|
||||
|
||||
let contents = InvoiceRequestContents::try_from(tlv_stream)?;
|
||||
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
|
||||
|
||||
Ok(UnsignedInvoiceRequest { bytes, contents, tagged_hash })
|
||||
let offset = TlvStream::new(&bytes)
|
||||
.range(0..INVOICE_REQUEST_TYPES.end)
|
||||
.last()
|
||||
.map_or(0, |last_record| last_record.end);
|
||||
let experimental_bytes = bytes.split_off(offset);
|
||||
|
||||
Ok(UnsignedInvoiceRequest { bytes, experimental_bytes, contents, tagged_hash })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1129,9 +1236,14 @@ impl TryFrom<Vec<u8>> for InvoiceRequest {
|
||||
let (
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
|
||||
SignatureTlvStream { signature },
|
||||
experimental_offer_tlv_stream,
|
||||
experimental_invoice_request_tlv_stream,
|
||||
) = tlv_stream;
|
||||
let contents = InvoiceRequestContents::try_from(
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
|
||||
(
|
||||
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
)
|
||||
)?;
|
||||
|
||||
let signature = match signature {
|
||||
@ -1155,13 +1267,18 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
|
||||
InvoiceRequestTlvStream {
|
||||
chain, amount, features, quantity, payer_id, payer_note, paths,
|
||||
},
|
||||
experimental_offer_tlv_stream,
|
||||
ExperimentalInvoiceRequestTlvStream {
|
||||
#[cfg(test)]
|
||||
experimental_bar,
|
||||
},
|
||||
) = tlv_stream;
|
||||
|
||||
let payer = match metadata {
|
||||
None => return Err(Bolt12SemanticError::MissingPayerMetadata),
|
||||
Some(metadata) => PayerContents(Metadata::Bytes(metadata)),
|
||||
};
|
||||
let offer = OfferContents::try_from(offer_tlv_stream)?;
|
||||
let offer = OfferContents::try_from((offer_tlv_stream, experimental_offer_tlv_stream))?;
|
||||
|
||||
if !offer.supports_chain(chain.unwrap_or_else(|| offer.implied_chain())) {
|
||||
return Err(Bolt12SemanticError::UnsupportedChain);
|
||||
@ -1188,6 +1305,8 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
|
||||
Ok(InvoiceRequestContents {
|
||||
inner: InvoiceRequestContentsWithoutPayerSigningPubkey {
|
||||
payer, offer, chain, amount_msats: amount, features, quantity, payer_note,
|
||||
#[cfg(test)]
|
||||
experimental_bar,
|
||||
},
|
||||
payer_signing_pubkey,
|
||||
})
|
||||
@ -1242,7 +1361,7 @@ impl Readable for InvoiceRequestFields {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest};
|
||||
use super::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, InvoiceRequest, InvoiceRequestFields, InvoiceRequestTlvStreamRef, PAYER_NOTE_LIMIT, SIGNATURE_TAG, UnsignedInvoiceRequest};
|
||||
|
||||
use bitcoin::constants::ChainHash;
|
||||
use bitcoin::network::Network;
|
||||
@ -1256,9 +1375,9 @@ mod tests {
|
||||
use crate::ln::inbound_payment::ExpandedKey;
|
||||
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
|
||||
use crate::offers::invoice::{Bolt12Invoice, SIGNATURE_TAG as INVOICE_SIGNATURE_TAG};
|
||||
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self};
|
||||
use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, TlvStream, self};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::offer::{Amount, OfferTlvStreamRef, Quantity};
|
||||
use crate::offers::offer::{Amount, ExperimentalOfferTlvStreamRef, OfferTlvStreamRef, Quantity};
|
||||
#[cfg(not(c_bindings))]
|
||||
use {
|
||||
crate::offers::offer::OfferBuilder,
|
||||
@ -1367,6 +1486,12 @@ mod tests {
|
||||
paths: None,
|
||||
},
|
||||
SignatureTlvStreamRef { signature: Some(&invoice_request.signature()) },
|
||||
ExperimentalOfferTlvStreamRef {
|
||||
experimental_foo: None,
|
||||
},
|
||||
ExperimentalInvoiceRequestTlvStreamRef {
|
||||
experimental_bar: None,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -1414,16 +1539,19 @@ mod tests {
|
||||
|
||||
let offer = OfferBuilder::new(recipient_pubkey())
|
||||
.amount_msats(1000)
|
||||
.experimental_foo(42)
|
||||
.build().unwrap();
|
||||
let invoice_request = offer
|
||||
.request_invoice_deriving_metadata(signing_pubkey, &expanded_key, nonce, payment_id)
|
||||
.unwrap()
|
||||
.experimental_bar(42)
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
assert_eq!(invoice_request.payer_signing_pubkey(), payer_pubkey());
|
||||
|
||||
let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
|
||||
.unwrap()
|
||||
.experimental_baz(42)
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
match invoice.verify_using_metadata(&expanded_key, &secp_ctx) {
|
||||
@ -1437,22 +1565,29 @@ mod tests {
|
||||
// Fails verification with altered fields
|
||||
let (
|
||||
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
|
||||
mut invoice_tlv_stream, mut signature_tlv_stream
|
||||
mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream,
|
||||
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
|
||||
) = invoice.as_tlv_stream();
|
||||
invoice_request_tlv_stream.amount = Some(2000);
|
||||
invoice_tlv_stream.amount = Some(2000);
|
||||
|
||||
let tlv_stream =
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
|
||||
let experimental_tlv_stream = (
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
);
|
||||
let mut bytes = Vec::new();
|
||||
tlv_stream.write(&mut bytes).unwrap();
|
||||
(&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap();
|
||||
|
||||
let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes);
|
||||
let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap();
|
||||
signature_tlv_stream.signature = Some(&signature);
|
||||
|
||||
let mut encoded_invoice = bytes;
|
||||
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
|
||||
let mut encoded_invoice = Vec::new();
|
||||
(tlv_stream, signature_tlv_stream, experimental_tlv_stream)
|
||||
.write(&mut encoded_invoice)
|
||||
.unwrap();
|
||||
|
||||
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
|
||||
assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err());
|
||||
@ -1460,22 +1595,29 @@ mod tests {
|
||||
// Fails verification with altered metadata
|
||||
let (
|
||||
mut payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
|
||||
mut signature_tlv_stream
|
||||
mut signature_tlv_stream, experimental_offer_tlv_stream,
|
||||
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
|
||||
) = invoice.as_tlv_stream();
|
||||
let metadata = payer_tlv_stream.metadata.unwrap().iter().copied().rev().collect();
|
||||
payer_tlv_stream.metadata = Some(&metadata);
|
||||
|
||||
let tlv_stream =
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
|
||||
let experimental_tlv_stream = (
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
);
|
||||
let mut bytes = Vec::new();
|
||||
tlv_stream.write(&mut bytes).unwrap();
|
||||
(&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap();
|
||||
|
||||
let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes);
|
||||
let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap();
|
||||
signature_tlv_stream.signature = Some(&signature);
|
||||
|
||||
let mut encoded_invoice = bytes;
|
||||
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
|
||||
let mut encoded_invoice = Vec::new();
|
||||
(tlv_stream, signature_tlv_stream, experimental_tlv_stream)
|
||||
.write(&mut encoded_invoice)
|
||||
.unwrap();
|
||||
|
||||
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
|
||||
assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err());
|
||||
@ -1491,15 +1633,18 @@ mod tests {
|
||||
|
||||
let offer = OfferBuilder::new(recipient_pubkey())
|
||||
.amount_msats(1000)
|
||||
.experimental_foo(42)
|
||||
.build().unwrap();
|
||||
let invoice_request = offer
|
||||
.request_invoice_deriving_signing_pubkey(&expanded_key, nonce, &secp_ctx, payment_id)
|
||||
.unwrap()
|
||||
.experimental_bar(42)
|
||||
.build_and_sign()
|
||||
.unwrap();
|
||||
|
||||
let invoice = invoice_request.respond_with_no_std(payment_paths(), payment_hash(), now())
|
||||
.unwrap()
|
||||
.experimental_baz(42)
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err());
|
||||
@ -1510,22 +1655,29 @@ mod tests {
|
||||
// Fails verification with altered fields
|
||||
let (
|
||||
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream,
|
||||
mut invoice_tlv_stream, mut signature_tlv_stream
|
||||
mut invoice_tlv_stream, mut signature_tlv_stream, experimental_offer_tlv_stream,
|
||||
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
|
||||
) = invoice.as_tlv_stream();
|
||||
invoice_request_tlv_stream.amount = Some(2000);
|
||||
invoice_tlv_stream.amount = Some(2000);
|
||||
|
||||
let tlv_stream =
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
|
||||
let experimental_tlv_stream = (
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
);
|
||||
let mut bytes = Vec::new();
|
||||
tlv_stream.write(&mut bytes).unwrap();
|
||||
(&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap();
|
||||
|
||||
let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes);
|
||||
let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap();
|
||||
signature_tlv_stream.signature = Some(&signature);
|
||||
|
||||
let mut encoded_invoice = bytes;
|
||||
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
|
||||
let mut encoded_invoice = Vec::new();
|
||||
(tlv_stream, signature_tlv_stream, experimental_tlv_stream)
|
||||
.write(&mut encoded_invoice)
|
||||
.unwrap();
|
||||
|
||||
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
|
||||
assert!(
|
||||
@ -1535,22 +1687,29 @@ mod tests {
|
||||
// Fails verification with altered payer id
|
||||
let (
|
||||
payer_tlv_stream, offer_tlv_stream, mut invoice_request_tlv_stream, invoice_tlv_stream,
|
||||
mut signature_tlv_stream
|
||||
mut signature_tlv_stream, experimental_offer_tlv_stream,
|
||||
experimental_invoice_request_tlv_stream, experimental_invoice_tlv_stream,
|
||||
) = invoice.as_tlv_stream();
|
||||
let payer_id = pubkey(1);
|
||||
invoice_request_tlv_stream.payer_id = Some(&payer_id);
|
||||
|
||||
let tlv_stream =
|
||||
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream);
|
||||
let experimental_tlv_stream = (
|
||||
experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
);
|
||||
let mut bytes = Vec::new();
|
||||
tlv_stream.write(&mut bytes).unwrap();
|
||||
(&tlv_stream, &experimental_tlv_stream).write(&mut bytes).unwrap();
|
||||
|
||||
let message = TaggedHash::from_valid_tlv_stream_bytes(INVOICE_SIGNATURE_TAG, &bytes);
|
||||
let signature = merkle::sign_message(recipient_sign, &message, recipient_pubkey()).unwrap();
|
||||
signature_tlv_stream.signature = Some(&signature);
|
||||
|
||||
let mut encoded_invoice = bytes;
|
||||
signature_tlv_stream.write(&mut encoded_invoice).unwrap();
|
||||
let mut encoded_invoice = Vec::new();
|
||||
(tlv_stream, signature_tlv_stream, experimental_tlv_stream)
|
||||
.write(&mut encoded_invoice)
|
||||
.unwrap();
|
||||
|
||||
let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap();
|
||||
assert!(
|
||||
@ -1570,7 +1729,7 @@ mod tests {
|
||||
.chain(Network::Bitcoin).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.chain(), mainnet);
|
||||
assert_eq!(tlv_stream.chain, None);
|
||||
|
||||
@ -1582,7 +1741,7 @@ mod tests {
|
||||
.chain(Network::Testnet).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.chain(), testnet);
|
||||
assert_eq!(tlv_stream.chain, Some(&testnet));
|
||||
|
||||
@ -1595,7 +1754,7 @@ mod tests {
|
||||
.chain(Network::Bitcoin).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.chain(), mainnet);
|
||||
assert_eq!(tlv_stream.chain, None);
|
||||
|
||||
@ -1609,7 +1768,7 @@ mod tests {
|
||||
.chain(Network::Testnet).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.chain(), testnet);
|
||||
assert_eq!(tlv_stream.chain, Some(&testnet));
|
||||
|
||||
@ -1645,7 +1804,7 @@ mod tests {
|
||||
.amount_msats(1000).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.amount_msats(), Some(1000));
|
||||
assert_eq!(tlv_stream.amount, Some(1000));
|
||||
|
||||
@ -1657,7 +1816,7 @@ mod tests {
|
||||
.amount_msats(1000).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.amount_msats(), Some(1000));
|
||||
assert_eq!(tlv_stream.amount, Some(1000));
|
||||
|
||||
@ -1668,7 +1827,7 @@ mod tests {
|
||||
.amount_msats(1001).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.amount_msats(), Some(1001));
|
||||
assert_eq!(tlv_stream.amount, Some(1001));
|
||||
|
||||
@ -1748,7 +1907,7 @@ mod tests {
|
||||
.features_unchecked(InvoiceRequestFeatures::unknown())
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::unknown());
|
||||
assert_eq!(tlv_stream.features, Some(&InvoiceRequestFeatures::unknown()));
|
||||
|
||||
@ -1760,7 +1919,7 @@ mod tests {
|
||||
.features_unchecked(InvoiceRequestFeatures::empty())
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.invoice_request_features(), &InvoiceRequestFeatures::empty());
|
||||
assert_eq!(tlv_stream.features, None);
|
||||
}
|
||||
@ -1777,7 +1936,7 @@ mod tests {
|
||||
.request_invoice(vec![1; 32], payer_pubkey()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.quantity(), None);
|
||||
assert_eq!(tlv_stream.quantity, None);
|
||||
|
||||
@ -1802,7 +1961,7 @@ mod tests {
|
||||
.quantity(10).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.amount_msats(), Some(10_000));
|
||||
assert_eq!(tlv_stream.amount, Some(10_000));
|
||||
|
||||
@ -1827,7 +1986,7 @@ mod tests {
|
||||
.quantity(2).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.amount_msats(), Some(2_000));
|
||||
assert_eq!(tlv_stream.amount, Some(2_000));
|
||||
|
||||
@ -1863,7 +2022,7 @@ mod tests {
|
||||
.payer_note("bar".into())
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.payer_note(), Some(PrintableString("bar")));
|
||||
assert_eq!(tlv_stream.payer_note, Some(&String::from("bar")));
|
||||
|
||||
@ -1875,7 +2034,7 @@ mod tests {
|
||||
.payer_note("baz".into())
|
||||
.build().unwrap()
|
||||
.sign(payer_sign).unwrap();
|
||||
let (_, _, tlv_stream, _) = invoice_request.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _, _) = invoice_request.as_tlv_stream();
|
||||
assert_eq!(invoice_request.payer_note(), Some(PrintableString("baz")));
|
||||
assert_eq!(tlv_stream.payer_note, Some(&String::from("baz")));
|
||||
}
|
||||
@ -2294,9 +2453,196 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_invoice_request_with_extra_tlv_records() {
|
||||
fn parses_invoice_request_with_unknown_tlv_records() {
|
||||
const UNKNOWN_ODD_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 1;
|
||||
assert!(UNKNOWN_ODD_TYPE % 2 == 1);
|
||||
|
||||
let secp_ctx = Secp256k1::new();
|
||||
let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
|
||||
let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], keys.public_key()).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice_request.bytes.reserve_exact(
|
||||
unsigned_invoice_request.bytes.capacity()
|
||||
- unsigned_invoice_request.bytes.len()
|
||||
+ unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice_request.bytes.extend_from_slice(&unknown_bytes);
|
||||
unsigned_invoice_request.tagged_hash =
|
||||
TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice_request.bytes);
|
||||
|
||||
let invoice_request = unsigned_invoice_request
|
||||
.sign(|message: &UnsignedInvoiceRequest|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice_request = Vec::new();
|
||||
invoice_request.write(&mut encoded_invoice_request).unwrap();
|
||||
|
||||
match InvoiceRequest::try_from(encoded_invoice_request.clone()) {
|
||||
Ok(invoice_request) => assert_eq!(invoice_request.bytes, encoded_invoice_request),
|
||||
Err(e) => panic!("error parsing invoice_request: {:?}", e),
|
||||
}
|
||||
|
||||
const UNKNOWN_EVEN_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 2;
|
||||
assert!(UNKNOWN_EVEN_TYPE % 2 == 0);
|
||||
|
||||
let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], keys.public_key()).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice_request.bytes.reserve_exact(
|
||||
unsigned_invoice_request.bytes.capacity()
|
||||
- unsigned_invoice_request.bytes.len()
|
||||
+ unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice_request.bytes.extend_from_slice(&unknown_bytes);
|
||||
unsigned_invoice_request.tagged_hash =
|
||||
TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice_request.bytes);
|
||||
|
||||
let invoice_request = unsigned_invoice_request
|
||||
.sign(|message: &UnsignedInvoiceRequest|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice_request = Vec::new();
|
||||
invoice_request.write(&mut encoded_invoice_request).unwrap();
|
||||
|
||||
match InvoiceRequest::try_from(encoded_invoice_request) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_invoice_request_with_experimental_tlv_records() {
|
||||
const UNKNOWN_ODD_TYPE: u64 = EXPERIMENTAL_INVOICE_REQUEST_TYPES.start + 1;
|
||||
assert!(UNKNOWN_ODD_TYPE % 2 == 1);
|
||||
|
||||
let secp_ctx = Secp256k1::new();
|
||||
let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
|
||||
let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], keys.public_key()).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice_request.bytes.reserve_exact(
|
||||
unsigned_invoice_request.bytes.capacity()
|
||||
- unsigned_invoice_request.bytes.len()
|
||||
+ unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice_request.experimental_bytes.extend_from_slice(&unknown_bytes);
|
||||
|
||||
let tlv_stream = TlvStream::new(&unsigned_invoice_request.bytes)
|
||||
.chain(TlvStream::new(&unsigned_invoice_request.experimental_bytes));
|
||||
unsigned_invoice_request.tagged_hash =
|
||||
TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
|
||||
|
||||
let invoice_request = unsigned_invoice_request
|
||||
.sign(|message: &UnsignedInvoiceRequest|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice_request = Vec::new();
|
||||
invoice_request.write(&mut encoded_invoice_request).unwrap();
|
||||
|
||||
match InvoiceRequest::try_from(encoded_invoice_request.clone()) {
|
||||
Ok(invoice_request) => assert_eq!(invoice_request.bytes, encoded_invoice_request),
|
||||
Err(e) => panic!("error parsing invoice_request: {:?}", e),
|
||||
}
|
||||
|
||||
const UNKNOWN_EVEN_TYPE: u64 = EXPERIMENTAL_INVOICE_REQUEST_TYPES.start;
|
||||
assert!(UNKNOWN_EVEN_TYPE % 2 == 0);
|
||||
|
||||
let mut unsigned_invoice_request = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], keys.public_key()).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice_request.bytes.reserve_exact(
|
||||
unsigned_invoice_request.bytes.capacity()
|
||||
- unsigned_invoice_request.bytes.len()
|
||||
+ unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice_request.experimental_bytes.extend_from_slice(&unknown_bytes);
|
||||
|
||||
let tlv_stream = TlvStream::new(&unsigned_invoice_request.bytes)
|
||||
.chain(TlvStream::new(&unsigned_invoice_request.experimental_bytes));
|
||||
unsigned_invoice_request.tagged_hash =
|
||||
TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
|
||||
|
||||
let invoice_request = unsigned_invoice_request
|
||||
.sign(|message: &UnsignedInvoiceRequest|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice_request = Vec::new();
|
||||
invoice_request.write(&mut encoded_invoice_request).unwrap();
|
||||
|
||||
match InvoiceRequest::try_from(encoded_invoice_request) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
|
||||
let invoice_request = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
.request_invoice(vec![1; 32], keys.public_key()).unwrap()
|
||||
.build().unwrap()
|
||||
.sign(|message: &UnsignedInvoiceRequest|
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice_request = Vec::new();
|
||||
invoice_request.write(&mut encoded_invoice_request).unwrap();
|
||||
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_invoice_request).unwrap();
|
||||
BigSize(32).write(&mut encoded_invoice_request).unwrap();
|
||||
[42u8; 32].write(&mut encoded_invoice_request).unwrap();
|
||||
|
||||
match InvoiceRequest::try_from(encoded_invoice_request) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSignature(secp256k1::Error::IncorrectSignature)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_invoice_request_with_out_of_range_tlv_records() {
|
||||
let secp_ctx = Secp256k1::new();
|
||||
let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
|
||||
|
||||
let invoice_request = OfferBuilder::new(keys.public_key())
|
||||
.amount_msats(1000)
|
||||
.build().unwrap()
|
||||
@ -2317,6 +2663,17 @@ mod tests {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
|
||||
}
|
||||
|
||||
let mut encoded_invoice_request = Vec::new();
|
||||
invoice_request.write(&mut encoded_invoice_request).unwrap();
|
||||
BigSize(EXPERIMENTAL_INVOICE_REQUEST_TYPES.end).write(&mut encoded_invoice_request).unwrap();
|
||||
BigSize(32).write(&mut encoded_invoice_request).unwrap();
|
||||
[42u8; 32].write(&mut encoded_invoice_request).unwrap();
|
||||
|
||||
match InvoiceRequest::try_from(encoded_invoice_request) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
use bitcoin::hashes::{Hash, HashEngine, sha256};
|
||||
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
|
||||
use bitcoin::secp256k1::constants::SCHNORR_SIGNATURE_SIZE;
|
||||
use bitcoin::secp256k1::schnorr::Signature;
|
||||
use crate::io;
|
||||
use crate::util::ser::{BigSize, Readable, Writeable, Writer};
|
||||
@ -19,12 +20,16 @@ use crate::util::ser::{BigSize, Readable, Writeable, Writer};
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Valid type range for signature TLV records.
|
||||
const SIGNATURE_TYPES: core::ops::RangeInclusive<u64> = 240..=1000;
|
||||
pub(super) const SIGNATURE_TYPES: core::ops::RangeInclusive<u64> = 240..=1000;
|
||||
|
||||
tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, {
|
||||
tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef<'a>, SIGNATURE_TYPES, {
|
||||
(240, signature: Signature),
|
||||
});
|
||||
|
||||
/// Size of a TLV record in `SIGNATURE_TYPES` when the type is 1000. TLV types are encoded using
|
||||
/// BigSize, so a TLV record with type 240 will use two less bytes.
|
||||
pub(super) const SIGNATURE_TLV_RECORD_SIZE: usize = 3 + 1 + SCHNORR_SIGNATURE_SIZE;
|
||||
|
||||
/// A hash for use in a specific context by tweaking with a context-dependent tag as per [BIP 340]
|
||||
/// and computed over the merkle root of a TLV stream to sign as defined in [BOLT 12].
|
||||
///
|
||||
@ -164,7 +169,7 @@ fn root_hash<'a, I: core::iter::Iterator<Item = TlvRecord<'a>>>(tlv_stream: I) -
|
||||
let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes()));
|
||||
|
||||
let mut leaves = Vec::new();
|
||||
for record in TlvStream::skip_signatures(tlv_stream) {
|
||||
for record in tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) {
|
||||
leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record.record_bytes));
|
||||
leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes));
|
||||
}
|
||||
@ -240,21 +245,16 @@ impl<'a> TlvStream<'a> {
|
||||
self.skip_while(move |record| !types.contains(&record.r#type))
|
||||
.take_while(move |record| take_range.contains(&record.r#type))
|
||||
}
|
||||
|
||||
fn skip_signatures(
|
||||
tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>
|
||||
) -> impl core::iter::Iterator<Item = TlvRecord<'a>> {
|
||||
tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type))
|
||||
}
|
||||
}
|
||||
|
||||
/// A slice into a [`TlvStream`] for a record.
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub(super) struct TlvRecord<'a> {
|
||||
pub(super) r#type: u64,
|
||||
type_bytes: &'a [u8],
|
||||
// The entire TLV record.
|
||||
pub(super) record_bytes: &'a [u8],
|
||||
pub(super) start: usize,
|
||||
pub(super) end: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TlvStream<'a> {
|
||||
@ -277,32 +277,25 @@ impl<'a> Iterator for TlvStream<'a> {
|
||||
|
||||
self.data.set_position(end);
|
||||
|
||||
Some(TlvRecord { r#type, type_bytes, record_bytes })
|
||||
Some(TlvRecord {
|
||||
r#type, type_bytes, record_bytes, start: start as usize, end: end as usize,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encoding for a pre-serialized TLV stream that excludes any signature TLV records.
|
||||
///
|
||||
/// Panics if the wrapped bytes are not a well-formed TLV stream.
|
||||
pub(super) struct WithoutSignatures<'a>(pub &'a [u8]);
|
||||
|
||||
impl<'a> Writeable for WithoutSignatures<'a> {
|
||||
impl<'a> Writeable for TlvRecord<'a> {
|
||||
#[inline]
|
||||
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
|
||||
let tlv_stream = TlvStream::new(self.0);
|
||||
for record in TlvStream::skip_signatures(tlv_stream) {
|
||||
writer.write_all(record.record_bytes)?;
|
||||
}
|
||||
Ok(())
|
||||
writer.write_all(self.record_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{SIGNATURE_TYPES, TlvStream, WithoutSignatures};
|
||||
use super::{SIGNATURE_TYPES, TlvStream};
|
||||
|
||||
use bitcoin::hashes::{Hash, sha256};
|
||||
use bitcoin::hex::FromHex;
|
||||
@ -412,7 +405,11 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let mut bytes_without_signature = Vec::new();
|
||||
WithoutSignatures(&invoice_request.bytes).write(&mut bytes_without_signature).unwrap();
|
||||
let tlv_stream_without_signatures = TlvStream::new(&invoice_request.bytes)
|
||||
.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type));
|
||||
for record in tlv_stream_without_signatures {
|
||||
record.write(&mut bytes_without_signature).unwrap();
|
||||
}
|
||||
|
||||
assert_ne!(bytes_without_signature, invoice_request.bytes);
|
||||
assert_eq!(
|
||||
|
@ -91,11 +91,11 @@ use crate::ln::channelmanager::PaymentId;
|
||||
use crate::types::features::OfferFeatures;
|
||||
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN};
|
||||
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
|
||||
use crate::offers::merkle::{TaggedHash, TlvStream};
|
||||
use crate::offers::merkle::{TaggedHash, TlvRecord, TlvStream};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
|
||||
use crate::offers::signer::{Metadata, MetadataMaterial, self};
|
||||
use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer};
|
||||
use crate::util::ser::{CursorReadable, HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer};
|
||||
use crate::util::string::PrintableString;
|
||||
|
||||
#[cfg(not(c_bindings))]
|
||||
@ -130,7 +130,7 @@ impl OfferId {
|
||||
}
|
||||
|
||||
fn from_valid_invreq_tlv_stream(bytes: &[u8]) -> Self {
|
||||
let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES);
|
||||
let tlv_stream = Offer::tlv_stream_iter(bytes);
|
||||
let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, tlv_stream);
|
||||
Self(tagged_hash.to_bytes())
|
||||
}
|
||||
@ -239,6 +239,8 @@ macro_rules! offer_explicit_metadata_builder_methods { (
|
||||
chains: None, metadata: None, amount: None, description: None,
|
||||
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
|
||||
supported_quantity: Quantity::One, issuer_signing_pubkey: Some(signing_pubkey),
|
||||
#[cfg(test)]
|
||||
experimental_foo: None,
|
||||
},
|
||||
metadata_strategy: core::marker::PhantomData,
|
||||
secp_ctx: None,
|
||||
@ -280,6 +282,8 @@ macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => {
|
||||
chains: None, metadata: Some(metadata), amount: None, description: None,
|
||||
features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None,
|
||||
supported_quantity: Quantity::One, issuer_signing_pubkey: Some(node_id),
|
||||
#[cfg(test)]
|
||||
experimental_foo: None,
|
||||
},
|
||||
metadata_strategy: core::marker::PhantomData,
|
||||
secp_ctx: Some(secp_ctx),
|
||||
@ -414,10 +418,10 @@ macro_rules! offer_builder_methods { (
|
||||
};
|
||||
|
||||
let mut tlv_stream = $self.offer.as_tlv_stream();
|
||||
debug_assert_eq!(tlv_stream.metadata, None);
|
||||
tlv_stream.metadata = None;
|
||||
debug_assert_eq!(tlv_stream.0.metadata, None);
|
||||
tlv_stream.0.metadata = None;
|
||||
if metadata.derives_recipient_keys() {
|
||||
tlv_stream.issuer_id = None;
|
||||
tlv_stream.0.issuer_id = None;
|
||||
}
|
||||
|
||||
// Either replace the signing pubkey with the derived pubkey or include the metadata
|
||||
@ -478,6 +482,12 @@ macro_rules! offer_builder_test_methods { (
|
||||
$return_value
|
||||
}
|
||||
|
||||
#[cfg_attr(c_bindings, allow(dead_code))]
|
||||
pub(super) fn experimental_foo($($self_mut)* $self: $self_type, experimental_foo: u64) -> $return_type {
|
||||
$self.offer.experimental_foo = Some(experimental_foo);
|
||||
$return_value
|
||||
}
|
||||
|
||||
#[cfg_attr(c_bindings, allow(dead_code))]
|
||||
pub(super) fn build_unchecked($self: $self_type) -> Offer {
|
||||
$self.build_without_checks()
|
||||
@ -585,6 +595,8 @@ pub(super) struct OfferContents {
|
||||
paths: Option<Vec<BlindedMessagePath>>,
|
||||
supported_quantity: Quantity,
|
||||
issuer_signing_pubkey: Option<PublicKey>,
|
||||
#[cfg(test)]
|
||||
experimental_foo: Option<u64>,
|
||||
}
|
||||
|
||||
macro_rules! offer_accessors { ($self: ident, $contents: expr) => {
|
||||
@ -701,6 +713,13 @@ impl Offer {
|
||||
self.contents.expects_quantity()
|
||||
}
|
||||
|
||||
pub(super) fn tlv_stream_iter<'a>(
|
||||
bytes: &'a [u8]
|
||||
) -> impl core::iter::Iterator<Item = TlvRecord<'a>> {
|
||||
TlvStream::new(bytes).range(OFFER_TYPES)
|
||||
.chain(TlvStream::new(bytes).range(EXPERIMENTAL_OFFER_TYPES))
|
||||
}
|
||||
|
||||
#[cfg(async_payments)]
|
||||
pub(super) fn verify<T: secp256k1::Signing>(
|
||||
&self, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1<T>
|
||||
@ -803,7 +822,7 @@ impl Offer {
|
||||
|
||||
#[cfg(test)]
|
||||
impl Offer {
|
||||
pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
|
||||
pub(super) fn as_tlv_stream(&self) -> FullOfferTlvStreamRef {
|
||||
self.contents.as_tlv_stream()
|
||||
}
|
||||
}
|
||||
@ -971,7 +990,9 @@ impl OfferContents {
|
||||
OFFER_ISSUER_ID_TYPE => !metadata.derives_recipient_keys(),
|
||||
_ => true,
|
||||
}
|
||||
});
|
||||
})
|
||||
.chain(TlvStream::new(bytes).range(EXPERIMENTAL_OFFER_TYPES));
|
||||
|
||||
let signing_pubkey = match self.issuer_signing_pubkey() {
|
||||
Some(signing_pubkey) => signing_pubkey,
|
||||
None => return Err(()),
|
||||
@ -988,7 +1009,7 @@ impl OfferContents {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
|
||||
pub(super) fn as_tlv_stream(&self) -> FullOfferTlvStreamRef {
|
||||
let (currency, amount) = match &self.amount {
|
||||
None => (None, None),
|
||||
Some(Amount::Bitcoin { amount_msats }) => (None, Some(*amount_msats)),
|
||||
@ -1001,7 +1022,7 @@ impl OfferContents {
|
||||
if self.features == OfferFeatures::empty() { None } else { Some(&self.features) }
|
||||
};
|
||||
|
||||
OfferTlvStreamRef {
|
||||
let offer = OfferTlvStreamRef {
|
||||
chains: self.chains.as_ref(),
|
||||
metadata: self.metadata(),
|
||||
currency,
|
||||
@ -1013,7 +1034,14 @@ impl OfferContents {
|
||||
issuer: self.issuer.as_ref(),
|
||||
quantity_max: self.supported_quantity.to_tlv_record(),
|
||||
issuer_id: self.issuer_signing_pubkey.as_ref(),
|
||||
}
|
||||
};
|
||||
|
||||
let experimental_offer = ExperimentalOfferTlvStreamRef {
|
||||
#[cfg(test)]
|
||||
experimental_foo: self.experimental_foo,
|
||||
};
|
||||
|
||||
(offer, experimental_offer)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1091,7 +1119,7 @@ const OFFER_METADATA_TYPE: u64 = 4;
|
||||
/// TLV record type for [`Offer::issuer_signing_pubkey`].
|
||||
const OFFER_ISSUER_ID_TYPE: u64 = 22;
|
||||
|
||||
tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, {
|
||||
tlv_stream!(OfferTlvStream, OfferTlvStreamRef<'a>, OFFER_TYPES, {
|
||||
(2, chains: (Vec<ChainHash>, WithoutLength)),
|
||||
(OFFER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
|
||||
(6, currency: CurrencyCode),
|
||||
@ -1105,6 +1133,31 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef, OFFER_TYPES, {
|
||||
(OFFER_ISSUER_ID_TYPE, issuer_id: PublicKey),
|
||||
});
|
||||
|
||||
/// Valid type range for experimental offer TLV records.
|
||||
pub(super) const EXPERIMENTAL_OFFER_TYPES: core::ops::Range<u64> = 1_000_000_000..2_000_000_000;
|
||||
|
||||
#[cfg(not(test))]
|
||||
tlv_stream!(ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, {
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
tlv_stream!(ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, EXPERIMENTAL_OFFER_TYPES, {
|
||||
(1_999_999_999, experimental_foo: (u64, HighZeroBytesDroppedBigSize)),
|
||||
});
|
||||
|
||||
type FullOfferTlvStream = (OfferTlvStream, ExperimentalOfferTlvStream);
|
||||
|
||||
type FullOfferTlvStreamRef<'a> = (OfferTlvStreamRef<'a>, ExperimentalOfferTlvStreamRef);
|
||||
|
||||
impl CursorReadable for FullOfferTlvStream {
|
||||
fn read<R: AsRef<[u8]>>(r: &mut io::Cursor<R>) -> Result<Self, DecodeError> {
|
||||
let offer = CursorReadable::read(r)?;
|
||||
let experimental_offer = CursorReadable::read(r)?;
|
||||
|
||||
Ok((offer, experimental_offer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Bech32Encode for Offer {
|
||||
const BECH32_HRP: &'static str = "lno";
|
||||
}
|
||||
@ -1121,7 +1174,7 @@ impl TryFrom<Vec<u8>> for Offer {
|
||||
type Error = Bolt12ParseError;
|
||||
|
||||
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
let offer = ParsedMessage::<OfferTlvStream>::try_from(bytes)?;
|
||||
let offer = ParsedMessage::<FullOfferTlvStream>::try_from(bytes)?;
|
||||
let ParsedMessage { bytes, tlv_stream } = offer;
|
||||
let contents = OfferContents::try_from(tlv_stream)?;
|
||||
let id = OfferId::from_valid_offer_tlv_stream(&bytes);
|
||||
@ -1130,14 +1183,20 @@ impl TryFrom<Vec<u8>> for Offer {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<OfferTlvStream> for OfferContents {
|
||||
impl TryFrom<FullOfferTlvStream> for OfferContents {
|
||||
type Error = Bolt12SemanticError;
|
||||
|
||||
fn try_from(tlv_stream: OfferTlvStream) -> Result<Self, Self::Error> {
|
||||
let OfferTlvStream {
|
||||
chains, metadata, currency, amount, description, features, absolute_expiry, paths,
|
||||
issuer, quantity_max, issuer_id,
|
||||
} = tlv_stream;
|
||||
fn try_from(tlv_stream: FullOfferTlvStream) -> Result<Self, Self::Error> {
|
||||
let (
|
||||
OfferTlvStream {
|
||||
chains, metadata, currency, amount, description, features, absolute_expiry, paths,
|
||||
issuer, quantity_max, issuer_id,
|
||||
},
|
||||
ExperimentalOfferTlvStream {
|
||||
#[cfg(test)]
|
||||
experimental_foo,
|
||||
},
|
||||
) = tlv_stream;
|
||||
|
||||
let metadata = metadata.map(|metadata| Metadata::Bytes(metadata));
|
||||
|
||||
@ -1175,6 +1234,8 @@ impl TryFrom<OfferTlvStream> for OfferContents {
|
||||
Ok(OfferContents {
|
||||
chains, metadata, amount, description, features, absolute_expiry, issuer, paths,
|
||||
supported_quantity, issuer_signing_pubkey,
|
||||
#[cfg(test)]
|
||||
experimental_foo,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1187,7 +1248,7 @@ impl core::fmt::Display for Offer {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Amount, Offer, OfferTlvStreamRef, Quantity};
|
||||
use super::{Amount, EXPERIMENTAL_OFFER_TYPES, ExperimentalOfferTlvStreamRef, OFFER_TYPES, Offer, OfferTlvStreamRef, Quantity};
|
||||
#[cfg(not(c_bindings))]
|
||||
use {
|
||||
super::OfferBuilder,
|
||||
@ -1239,19 +1300,24 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
offer.as_tlv_stream(),
|
||||
OfferTlvStreamRef {
|
||||
chains: None,
|
||||
metadata: None,
|
||||
currency: None,
|
||||
amount: None,
|
||||
description: None,
|
||||
features: None,
|
||||
absolute_expiry: None,
|
||||
paths: None,
|
||||
issuer: None,
|
||||
quantity_max: None,
|
||||
issuer_id: Some(&pubkey(42)),
|
||||
},
|
||||
(
|
||||
OfferTlvStreamRef {
|
||||
chains: None,
|
||||
metadata: None,
|
||||
currency: None,
|
||||
amount: None,
|
||||
description: None,
|
||||
features: None,
|
||||
absolute_expiry: None,
|
||||
paths: None,
|
||||
issuer: None,
|
||||
quantity_max: None,
|
||||
issuer_id: Some(&pubkey(42)),
|
||||
},
|
||||
ExperimentalOfferTlvStreamRef {
|
||||
experimental_foo: None,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if let Err(e) = Offer::try_from(buffer) {
|
||||
@ -1270,7 +1336,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(offer.supports_chain(mainnet));
|
||||
assert_eq!(offer.chains(), vec![mainnet]);
|
||||
assert_eq!(offer.as_tlv_stream().chains, None);
|
||||
assert_eq!(offer.as_tlv_stream().0.chains, None);
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.chain(Network::Testnet)
|
||||
@ -1278,7 +1344,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(offer.supports_chain(testnet));
|
||||
assert_eq!(offer.chains(), vec![testnet]);
|
||||
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet]));
|
||||
assert_eq!(offer.as_tlv_stream().0.chains, Some(&vec![testnet]));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.chain(Network::Testnet)
|
||||
@ -1287,7 +1353,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(offer.supports_chain(testnet));
|
||||
assert_eq!(offer.chains(), vec![testnet]);
|
||||
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet]));
|
||||
assert_eq!(offer.as_tlv_stream().0.chains, Some(&vec![testnet]));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.chain(Network::Bitcoin)
|
||||
@ -1297,7 +1363,7 @@ mod tests {
|
||||
assert!(offer.supports_chain(mainnet));
|
||||
assert!(offer.supports_chain(testnet));
|
||||
assert_eq!(offer.chains(), vec![mainnet, testnet]);
|
||||
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![mainnet, testnet]));
|
||||
assert_eq!(offer.as_tlv_stream().0.chains, Some(&vec![mainnet, testnet]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1307,7 +1373,7 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(offer.metadata(), Some(&vec![42; 32]));
|
||||
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32]));
|
||||
assert_eq!(offer.as_tlv_stream().0.metadata, Some(&vec![42; 32]));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.metadata(vec![42; 32]).unwrap()
|
||||
@ -1315,7 +1381,7 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(offer.metadata(), Some(&vec![43; 32]));
|
||||
assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![43; 32]));
|
||||
assert_eq!(offer.as_tlv_stream().0.metadata, Some(&vec![43; 32]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1330,6 +1396,7 @@ mod tests {
|
||||
use super::OfferWithDerivedMetadataBuilder as OfferBuilder;
|
||||
let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
|
||||
.amount_msats(1000)
|
||||
.experimental_foo(42)
|
||||
.build().unwrap();
|
||||
assert!(offer.metadata().is_some());
|
||||
assert_eq!(offer.issuer_signing_pubkey(), Some(node_id));
|
||||
@ -1352,7 +1419,7 @@ mod tests {
|
||||
|
||||
// Fails verification with altered offer field
|
||||
let mut tlv_stream = offer.as_tlv_stream();
|
||||
tlv_stream.amount = Some(100);
|
||||
tlv_stream.0.amount = Some(100);
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
tlv_stream.write(&mut encoded_offer).unwrap();
|
||||
@ -1365,8 +1432,8 @@ mod tests {
|
||||
|
||||
// Fails verification with altered metadata
|
||||
let mut tlv_stream = offer.as_tlv_stream();
|
||||
let metadata = tlv_stream.metadata.unwrap().iter().copied().rev().collect();
|
||||
tlv_stream.metadata = Some(&metadata);
|
||||
let metadata = tlv_stream.0.metadata.unwrap().iter().copied().rev().collect();
|
||||
tlv_stream.0.metadata = Some(&metadata);
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
tlv_stream.write(&mut encoded_offer).unwrap();
|
||||
@ -1399,6 +1466,7 @@ mod tests {
|
||||
let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
|
||||
.amount_msats(1000)
|
||||
.path(blinded_path)
|
||||
.experimental_foo(42)
|
||||
.build().unwrap();
|
||||
assert!(offer.metadata().is_none());
|
||||
assert_ne!(offer.issuer_signing_pubkey(), Some(node_id));
|
||||
@ -1419,7 +1487,7 @@ mod tests {
|
||||
|
||||
// Fails verification with altered offer field
|
||||
let mut tlv_stream = offer.as_tlv_stream();
|
||||
tlv_stream.amount = Some(100);
|
||||
tlv_stream.0.amount = Some(100);
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
tlv_stream.write(&mut encoded_offer).unwrap();
|
||||
@ -1435,7 +1503,7 @@ mod tests {
|
||||
// Fails verification with altered signing pubkey
|
||||
let mut tlv_stream = offer.as_tlv_stream();
|
||||
let issuer_id = pubkey(1);
|
||||
tlv_stream.issuer_id = Some(&issuer_id);
|
||||
tlv_stream.0.issuer_id = Some(&issuer_id);
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
tlv_stream.write(&mut encoded_offer).unwrap();
|
||||
@ -1460,8 +1528,8 @@ mod tests {
|
||||
.unwrap();
|
||||
let tlv_stream = offer.as_tlv_stream();
|
||||
assert_eq!(offer.amount(), Some(bitcoin_amount));
|
||||
assert_eq!(tlv_stream.amount, Some(1000));
|
||||
assert_eq!(tlv_stream.currency, None);
|
||||
assert_eq!(tlv_stream.0.amount, Some(1000));
|
||||
assert_eq!(tlv_stream.0.currency, None);
|
||||
|
||||
#[cfg(not(c_bindings))]
|
||||
let builder = OfferBuilder::new(pubkey(42))
|
||||
@ -1472,8 +1540,8 @@ mod tests {
|
||||
builder.amount(currency_amount.clone());
|
||||
let tlv_stream = builder.offer.as_tlv_stream();
|
||||
assert_eq!(builder.offer.amount, Some(currency_amount.clone()));
|
||||
assert_eq!(tlv_stream.amount, Some(10));
|
||||
assert_eq!(tlv_stream.currency, Some(b"USD"));
|
||||
assert_eq!(tlv_stream.0.amount, Some(10));
|
||||
assert_eq!(tlv_stream.0.currency, Some(b"USD"));
|
||||
match builder.build() {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedCurrency),
|
||||
@ -1485,8 +1553,8 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
let tlv_stream = offer.as_tlv_stream();
|
||||
assert_eq!(tlv_stream.amount, Some(1000));
|
||||
assert_eq!(tlv_stream.currency, None);
|
||||
assert_eq!(tlv_stream.0.amount, Some(1000));
|
||||
assert_eq!(tlv_stream.0.currency, None);
|
||||
|
||||
let invalid_amount = Amount::Bitcoin { amount_msats: MAX_VALUE_MSAT + 1 };
|
||||
match OfferBuilder::new(pubkey(42)).amount(invalid_amount).build() {
|
||||
@ -1502,7 +1570,7 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(offer.description(), Some(PrintableString("foo")));
|
||||
assert_eq!(offer.as_tlv_stream().description, Some(&String::from("foo")));
|
||||
assert_eq!(offer.as_tlv_stream().0.description, Some(&String::from("foo")));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.description("foo".into())
|
||||
@ -1510,14 +1578,14 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(offer.description(), Some(PrintableString("bar")));
|
||||
assert_eq!(offer.as_tlv_stream().description, Some(&String::from("bar")));
|
||||
assert_eq!(offer.as_tlv_stream().0.description, Some(&String::from("bar")));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.amount_msats(1000)
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(offer.description(), Some(PrintableString("")));
|
||||
assert_eq!(offer.as_tlv_stream().description, Some(&String::from("")));
|
||||
assert_eq!(offer.as_tlv_stream().0.description, Some(&String::from("")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1527,7 +1595,7 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(offer.offer_features(), &OfferFeatures::unknown());
|
||||
assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown()));
|
||||
assert_eq!(offer.as_tlv_stream().0.features, Some(&OfferFeatures::unknown()));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.features_unchecked(OfferFeatures::unknown())
|
||||
@ -1535,7 +1603,7 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(offer.offer_features(), &OfferFeatures::empty());
|
||||
assert_eq!(offer.as_tlv_stream().features, None);
|
||||
assert_eq!(offer.as_tlv_stream().0.features, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1552,7 +1620,7 @@ mod tests {
|
||||
assert!(!offer.is_expired());
|
||||
assert!(!offer.is_expired_no_std(now));
|
||||
assert_eq!(offer.absolute_expiry(), Some(future_expiry));
|
||||
assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(future_expiry.as_secs()));
|
||||
assert_eq!(offer.as_tlv_stream().0.absolute_expiry, Some(future_expiry.as_secs()));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.absolute_expiry(future_expiry)
|
||||
@ -1563,7 +1631,7 @@ mod tests {
|
||||
assert!(offer.is_expired());
|
||||
assert!(offer.is_expired_no_std(now));
|
||||
assert_eq!(offer.absolute_expiry(), Some(past_expiry));
|
||||
assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(past_expiry.as_secs()));
|
||||
assert_eq!(offer.as_tlv_stream().0.absolute_expiry, Some(past_expiry.as_secs()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1594,8 +1662,8 @@ mod tests {
|
||||
assert_eq!(offer.paths(), paths.as_slice());
|
||||
assert_eq!(offer.issuer_signing_pubkey(), Some(pubkey(42)));
|
||||
assert_ne!(pubkey(42), pubkey(44));
|
||||
assert_eq!(tlv_stream.paths, Some(&paths));
|
||||
assert_eq!(tlv_stream.issuer_id, Some(&pubkey(42)));
|
||||
assert_eq!(tlv_stream.0.paths, Some(&paths));
|
||||
assert_eq!(tlv_stream.0.issuer_id, Some(&pubkey(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1605,7 +1673,7 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(offer.issuer(), Some(PrintableString("foo")));
|
||||
assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("foo")));
|
||||
assert_eq!(offer.as_tlv_stream().0.issuer, Some(&String::from("foo")));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.issuer("foo".into())
|
||||
@ -1613,7 +1681,7 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(offer.issuer(), Some(PrintableString("bar")));
|
||||
assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("bar")));
|
||||
assert_eq!(offer.as_tlv_stream().0.issuer, Some(&String::from("bar")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1628,7 +1696,7 @@ mod tests {
|
||||
let tlv_stream = offer.as_tlv_stream();
|
||||
assert!(!offer.expects_quantity());
|
||||
assert_eq!(offer.supported_quantity(), Quantity::One);
|
||||
assert_eq!(tlv_stream.quantity_max, None);
|
||||
assert_eq!(tlv_stream.0.quantity_max, None);
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.supported_quantity(Quantity::Unbounded)
|
||||
@ -1637,7 +1705,7 @@ mod tests {
|
||||
let tlv_stream = offer.as_tlv_stream();
|
||||
assert!(offer.expects_quantity());
|
||||
assert_eq!(offer.supported_quantity(), Quantity::Unbounded);
|
||||
assert_eq!(tlv_stream.quantity_max, Some(0));
|
||||
assert_eq!(tlv_stream.0.quantity_max, Some(0));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.supported_quantity(Quantity::Bounded(ten))
|
||||
@ -1646,7 +1714,7 @@ mod tests {
|
||||
let tlv_stream = offer.as_tlv_stream();
|
||||
assert!(offer.expects_quantity());
|
||||
assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten));
|
||||
assert_eq!(tlv_stream.quantity_max, Some(10));
|
||||
assert_eq!(tlv_stream.0.quantity_max, Some(10));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.supported_quantity(Quantity::Bounded(one))
|
||||
@ -1655,7 +1723,7 @@ mod tests {
|
||||
let tlv_stream = offer.as_tlv_stream();
|
||||
assert!(offer.expects_quantity());
|
||||
assert_eq!(offer.supported_quantity(), Quantity::Bounded(one));
|
||||
assert_eq!(tlv_stream.quantity_max, Some(1));
|
||||
assert_eq!(tlv_stream.0.quantity_max, Some(1));
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42))
|
||||
.supported_quantity(Quantity::Bounded(ten))
|
||||
@ -1665,7 +1733,7 @@ mod tests {
|
||||
let tlv_stream = offer.as_tlv_stream();
|
||||
assert!(!offer.expects_quantity());
|
||||
assert_eq!(offer.supported_quantity(), Quantity::One);
|
||||
assert_eq!(tlv_stream.quantity_max, None);
|
||||
assert_eq!(tlv_stream.0.quantity_max, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1703,8 +1771,8 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut tlv_stream = offer.as_tlv_stream();
|
||||
tlv_stream.amount = Some(1000);
|
||||
tlv_stream.currency = Some(b"USD");
|
||||
tlv_stream.0.amount = Some(1000);
|
||||
tlv_stream.0.currency = Some(b"USD");
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
tlv_stream.write(&mut encoded_offer).unwrap();
|
||||
@ -1714,8 +1782,8 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut tlv_stream = offer.as_tlv_stream();
|
||||
tlv_stream.amount = None;
|
||||
tlv_stream.currency = Some(b"USD");
|
||||
tlv_stream.0.amount = None;
|
||||
tlv_stream.0.currency = Some(b"USD");
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
tlv_stream.write(&mut encoded_offer).unwrap();
|
||||
@ -1726,8 +1794,8 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut tlv_stream = offer.as_tlv_stream();
|
||||
tlv_stream.amount = Some(MAX_VALUE_MSAT + 1);
|
||||
tlv_stream.currency = None;
|
||||
tlv_stream.0.amount = Some(MAX_VALUE_MSAT + 1);
|
||||
tlv_stream.0.currency = None;
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
tlv_stream.write(&mut encoded_offer).unwrap();
|
||||
@ -1754,7 +1822,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut tlv_stream = offer.as_tlv_stream();
|
||||
tlv_stream.description = None;
|
||||
tlv_stream.0.description = None;
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
tlv_stream.write(&mut encoded_offer).unwrap();
|
||||
@ -1860,7 +1928,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let mut tlv_stream = offer.as_tlv_stream();
|
||||
tlv_stream.issuer_id = None;
|
||||
tlv_stream.0.issuer_id = None;
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
tlv_stream.write(&mut encoded_offer).unwrap();
|
||||
@ -1874,12 +1942,89 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_offer_with_extra_tlv_records() {
|
||||
fn parses_offer_with_unknown_tlv_records() {
|
||||
const UNKNOWN_ODD_TYPE: u64 = OFFER_TYPES.end - 1;
|
||||
assert!(UNKNOWN_ODD_TYPE % 2 == 1);
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
offer.write(&mut encoded_offer).unwrap();
|
||||
BigSize(80).write(&mut encoded_offer).unwrap();
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_offer).unwrap();
|
||||
BigSize(32).write(&mut encoded_offer).unwrap();
|
||||
[42u8; 32].write(&mut encoded_offer).unwrap();
|
||||
|
||||
match Offer::try_from(encoded_offer.clone()) {
|
||||
Ok(offer) => assert_eq!(offer.bytes, encoded_offer),
|
||||
Err(e) => panic!("error parsing offer: {:?}", e),
|
||||
}
|
||||
|
||||
const UNKNOWN_EVEN_TYPE: u64 = OFFER_TYPES.end - 2;
|
||||
assert!(UNKNOWN_EVEN_TYPE % 2 == 0);
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
offer.write(&mut encoded_offer).unwrap();
|
||||
BigSize(UNKNOWN_EVEN_TYPE).write(&mut encoded_offer).unwrap();
|
||||
BigSize(32).write(&mut encoded_offer).unwrap();
|
||||
[42u8; 32].write(&mut encoded_offer).unwrap();
|
||||
|
||||
match Offer::try_from(encoded_offer) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_offer_with_experimental_tlv_records() {
|
||||
let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
offer.write(&mut encoded_offer).unwrap();
|
||||
BigSize(EXPERIMENTAL_OFFER_TYPES.start + 1).write(&mut encoded_offer).unwrap();
|
||||
BigSize(32).write(&mut encoded_offer).unwrap();
|
||||
[42u8; 32].write(&mut encoded_offer).unwrap();
|
||||
|
||||
match Offer::try_from(encoded_offer.clone()) {
|
||||
Ok(offer) => assert_eq!(offer.bytes, encoded_offer),
|
||||
Err(e) => panic!("error parsing offer: {:?}", e),
|
||||
}
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
offer.write(&mut encoded_offer).unwrap();
|
||||
BigSize(EXPERIMENTAL_OFFER_TYPES.start).write(&mut encoded_offer).unwrap();
|
||||
BigSize(32).write(&mut encoded_offer).unwrap();
|
||||
[42u8; 32].write(&mut encoded_offer).unwrap();
|
||||
|
||||
match Offer::try_from(encoded_offer) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_offer_with_out_of_range_tlv_records() {
|
||||
let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
offer.write(&mut encoded_offer).unwrap();
|
||||
BigSize(OFFER_TYPES.end).write(&mut encoded_offer).unwrap();
|
||||
BigSize(32).write(&mut encoded_offer).unwrap();
|
||||
[42u8; 32].write(&mut encoded_offer).unwrap();
|
||||
|
||||
match Offer::try_from(encoded_offer) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
|
||||
}
|
||||
|
||||
let offer = OfferBuilder::new(pubkey(42)).build().unwrap();
|
||||
|
||||
let mut encoded_offer = Vec::new();
|
||||
offer.write(&mut encoded_offer).unwrap();
|
||||
BigSize(EXPERIMENTAL_OFFER_TYPES.end).write(&mut encoded_offer).unwrap();
|
||||
BigSize(32).write(&mut encoded_offer).unwrap();
|
||||
[42u8; 32].write(&mut encoded_offer).unwrap();
|
||||
|
||||
@ -1953,6 +2098,9 @@ mod bolt12_tests {
|
||||
|
||||
// unknown odd field
|
||||
"lno1pgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxfppf5x2mrvdamk7unvvs",
|
||||
|
||||
// unknown odd experimental field
|
||||
"lno1pgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvx078wdv5gg2dpjkcmr0wahhymry",
|
||||
];
|
||||
for encoded_offer in &offers {
|
||||
if let Err(e) = encoded_offer.parse::<Offer>() {
|
||||
@ -2095,6 +2243,18 @@ mod bolt12_tests {
|
||||
Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
|
||||
);
|
||||
|
||||
// Contains type > 1999999999
|
||||
assert_eq!(
|
||||
"lno1pgz5znzfgdz3vggzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgp06ae4jsq9qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqq".parse::<Offer>(),
|
||||
Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
|
||||
);
|
||||
|
||||
// Contains unknown even type (1000000002)
|
||||
assert_eq!(
|
||||
"lno1pgz5znzfgdz3vggzqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgp06wu6egp9qgr0u2xq4dh3kdevrf4zg6hx8a60jv0gxe0ptgyfc6xkryqqqqqqqq".parse::<Offer>(),
|
||||
Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)),
|
||||
);
|
||||
|
||||
// TODO: Resolved in spec https://github.com/lightning/bolts/pull/798/files#r1334851959
|
||||
// Contains unknown feature 22
|
||||
assert!(
|
||||
|
@ -30,6 +30,6 @@ pub(super) struct PayerContents(pub Metadata);
|
||||
/// [`Refund::payer_metadata`]: crate::offers::refund::Refund::payer_metadata
|
||||
pub(super) const PAYER_METADATA_TYPE: u64 = 0;
|
||||
|
||||
tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, {
|
||||
tlv_stream!(PayerTlvStream, PayerTlvStreamRef<'a>, 0..1, {
|
||||
(PAYER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)),
|
||||
});
|
||||
|
@ -98,9 +98,9 @@ use crate::ln::channelmanager::PaymentId;
|
||||
use crate::types::features::InvoiceRequestFeatures;
|
||||
use crate::ln::inbound_payment::{ExpandedKey, IV_LEN};
|
||||
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
|
||||
use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
|
||||
use crate::offers::invoice_request::{ExperimentalInvoiceRequestTlvStream, ExperimentalInvoiceRequestTlvStreamRef, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
|
||||
use crate::offers::offer::{ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferTlvStream, OfferTlvStreamRef};
|
||||
use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
|
||||
use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
|
||||
use crate::offers::signer::{Metadata, MetadataMaterial, self};
|
||||
@ -176,6 +176,10 @@ macro_rules! refund_explicit_metadata_builder_methods { () => {
|
||||
payer: PayerContents(metadata), description: String::new(), absolute_expiry: None,
|
||||
issuer: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
|
||||
quantity: None, payer_signing_pubkey: signing_pubkey, payer_note: None, paths: None,
|
||||
#[cfg(test)]
|
||||
experimental_foo: None,
|
||||
#[cfg(test)]
|
||||
experimental_bar: None,
|
||||
},
|
||||
secp_ctx: None,
|
||||
})
|
||||
@ -218,6 +222,10 @@ macro_rules! refund_builder_methods { (
|
||||
payer: PayerContents(metadata), description: String::new(), absolute_expiry: None,
|
||||
issuer: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
|
||||
quantity: None, payer_signing_pubkey: node_id, payer_note: None, paths: None,
|
||||
#[cfg(test)]
|
||||
experimental_foo: None,
|
||||
#[cfg(test)]
|
||||
experimental_bar: None,
|
||||
},
|
||||
secp_ctx: Some(secp_ctx),
|
||||
})
|
||||
@ -358,6 +366,18 @@ macro_rules! refund_builder_test_methods { (
|
||||
$self.refund.features = features;
|
||||
$return_value
|
||||
}
|
||||
|
||||
#[cfg_attr(c_bindings, allow(dead_code))]
|
||||
pub(super) fn experimental_foo($($self_mut)* $self: $self_type, experimental_foo: u64) -> $return_type {
|
||||
$self.refund.experimental_foo = Some(experimental_foo);
|
||||
$return_value
|
||||
}
|
||||
|
||||
#[cfg_attr(c_bindings, allow(dead_code))]
|
||||
pub(super) fn experimental_bar($($self_mut)* $self: $self_type, experimental_bar: u64) -> $return_type {
|
||||
$self.refund.experimental_bar = Some(experimental_bar);
|
||||
$return_value
|
||||
}
|
||||
} }
|
||||
|
||||
impl<'a> RefundBuilder<'a, secp256k1::SignOnly> {
|
||||
@ -437,6 +457,10 @@ pub(super) struct RefundContents {
|
||||
payer_signing_pubkey: PublicKey,
|
||||
payer_note: Option<String>,
|
||||
paths: Option<Vec<BlindedMessagePath>>,
|
||||
#[cfg(test)]
|
||||
experimental_foo: Option<u64>,
|
||||
#[cfg(test)]
|
||||
experimental_bar: Option<u64>,
|
||||
}
|
||||
|
||||
impl Refund {
|
||||
@ -770,7 +794,17 @@ impl RefundContents {
|
||||
paths: self.paths.as_ref(),
|
||||
};
|
||||
|
||||
(payer, offer, invoice_request)
|
||||
let experimental_offer = ExperimentalOfferTlvStreamRef {
|
||||
#[cfg(test)]
|
||||
experimental_foo: self.experimental_foo,
|
||||
};
|
||||
|
||||
let experimental_invoice_request = ExperimentalInvoiceRequestTlvStreamRef {
|
||||
#[cfg(test)]
|
||||
experimental_bar: self.experimental_bar,
|
||||
};
|
||||
|
||||
(payer, offer, invoice_request, experimental_offer, experimental_invoice_request)
|
||||
}
|
||||
}
|
||||
|
||||
@ -793,12 +827,17 @@ impl Writeable for RefundContents {
|
||||
}
|
||||
}
|
||||
|
||||
type RefundTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream);
|
||||
type RefundTlvStream = (
|
||||
PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, ExperimentalOfferTlvStream,
|
||||
ExperimentalInvoiceRequestTlvStream,
|
||||
);
|
||||
|
||||
type RefundTlvStreamRef<'a> = (
|
||||
PayerTlvStreamRef<'a>,
|
||||
OfferTlvStreamRef<'a>,
|
||||
InvoiceRequestTlvStreamRef<'a>,
|
||||
ExperimentalOfferTlvStreamRef,
|
||||
ExperimentalInvoiceRequestTlvStreamRef,
|
||||
);
|
||||
|
||||
impl CursorReadable for RefundTlvStream {
|
||||
@ -806,8 +845,10 @@ impl CursorReadable for RefundTlvStream {
|
||||
let payer = CursorReadable::read(r)?;
|
||||
let offer = CursorReadable::read(r)?;
|
||||
let invoice_request = CursorReadable::read(r)?;
|
||||
let experimental_offer = CursorReadable::read(r)?;
|
||||
let experimental_invoice_request = CursorReadable::read(r)?;
|
||||
|
||||
Ok((payer, offer, invoice_request))
|
||||
Ok((payer, offer, invoice_request, experimental_offer, experimental_invoice_request))
|
||||
}
|
||||
}
|
||||
|
||||
@ -849,6 +890,14 @@ impl TryFrom<RefundTlvStream> for RefundContents {
|
||||
InvoiceRequestTlvStream {
|
||||
chain, amount, features, quantity, payer_id, payer_note, paths
|
||||
},
|
||||
ExperimentalOfferTlvStream {
|
||||
#[cfg(test)]
|
||||
experimental_foo,
|
||||
},
|
||||
ExperimentalInvoiceRequestTlvStream {
|
||||
#[cfg(test)]
|
||||
experimental_bar,
|
||||
},
|
||||
) = tlv_stream;
|
||||
|
||||
let payer = match payer_metadata {
|
||||
@ -909,6 +958,10 @@ impl TryFrom<RefundTlvStream> for RefundContents {
|
||||
Ok(RefundContents {
|
||||
payer, description, absolute_expiry, issuer, chain, amount_msats, features, quantity,
|
||||
payer_signing_pubkey, payer_note, paths,
|
||||
#[cfg(test)]
|
||||
experimental_foo,
|
||||
#[cfg(test)]
|
||||
experimental_bar,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -944,9 +997,9 @@ mod tests {
|
||||
use crate::types::features::{InvoiceRequestFeatures, OfferFeatures};
|
||||
use crate::ln::inbound_payment::ExpandedKey;
|
||||
use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT};
|
||||
use crate::offers::invoice_request::InvoiceRequestTlvStreamRef;
|
||||
use crate::offers::invoice_request::{EXPERIMENTAL_INVOICE_REQUEST_TYPES, ExperimentalInvoiceRequestTlvStreamRef, INVOICE_REQUEST_TYPES, InvoiceRequestTlvStreamRef};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::offer::OfferTlvStreamRef;
|
||||
use crate::offers::offer::{ExperimentalOfferTlvStreamRef, OfferTlvStreamRef};
|
||||
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
|
||||
use crate::offers::payer::PayerTlvStreamRef;
|
||||
use crate::offers::test_utils::*;
|
||||
@ -1014,6 +1067,12 @@ mod tests {
|
||||
payer_note: None,
|
||||
paths: None,
|
||||
},
|
||||
ExperimentalOfferTlvStreamRef {
|
||||
experimental_foo: None,
|
||||
},
|
||||
ExperimentalInvoiceRequestTlvStreamRef {
|
||||
experimental_bar: None,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -1042,6 +1101,8 @@ mod tests {
|
||||
let refund = RefundBuilder
|
||||
::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx, 1000, payment_id)
|
||||
.unwrap()
|
||||
.experimental_foo(42)
|
||||
.experimental_bar(42)
|
||||
.build().unwrap();
|
||||
assert_eq!(refund.payer_signing_pubkey(), node_id);
|
||||
|
||||
@ -1049,6 +1110,7 @@ mod tests {
|
||||
let invoice = refund
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
|
||||
.unwrap()
|
||||
.experimental_baz(42)
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
match invoice.verify_using_metadata(&expanded_key, &secp_ctx) {
|
||||
@ -1109,12 +1171,15 @@ mod tests {
|
||||
::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx, 1000, payment_id)
|
||||
.unwrap()
|
||||
.path(blinded_path)
|
||||
.experimental_foo(42)
|
||||
.experimental_bar(42)
|
||||
.build().unwrap();
|
||||
assert_ne!(refund.payer_signing_pubkey(), node_id);
|
||||
|
||||
let invoice = refund
|
||||
.respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now())
|
||||
.unwrap()
|
||||
.experimental_baz(42)
|
||||
.build().unwrap()
|
||||
.sign(recipient_sign).unwrap();
|
||||
assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err());
|
||||
@ -1166,7 +1231,7 @@ mod tests {
|
||||
.absolute_expiry(future_expiry)
|
||||
.build()
|
||||
.unwrap();
|
||||
let (_, tlv_stream, _) = refund.as_tlv_stream();
|
||||
let (_, tlv_stream, _, _, _) = refund.as_tlv_stream();
|
||||
#[cfg(feature = "std")]
|
||||
assert!(!refund.is_expired());
|
||||
assert!(!refund.is_expired_no_std(now));
|
||||
@ -1178,7 +1243,7 @@ mod tests {
|
||||
.absolute_expiry(past_expiry)
|
||||
.build()
|
||||
.unwrap();
|
||||
let (_, tlv_stream, _) = refund.as_tlv_stream();
|
||||
let (_, tlv_stream, _, _, _) = refund.as_tlv_stream();
|
||||
#[cfg(feature = "std")]
|
||||
assert!(refund.is_expired());
|
||||
assert!(refund.is_expired_no_std(now));
|
||||
@ -1210,7 +1275,7 @@ mod tests {
|
||||
.path(paths[1].clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
let (_, _, invoice_request_tlv_stream) = refund.as_tlv_stream();
|
||||
let (_, _, invoice_request_tlv_stream, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.payer_signing_pubkey(), pubkey(42));
|
||||
assert_eq!(refund.paths(), paths.as_slice());
|
||||
assert_ne!(pubkey(42), pubkey(44));
|
||||
@ -1224,7 +1289,7 @@ mod tests {
|
||||
.issuer("bar".into())
|
||||
.build()
|
||||
.unwrap();
|
||||
let (_, tlv_stream, _) = refund.as_tlv_stream();
|
||||
let (_, tlv_stream, _, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.issuer(), Some(PrintableString("bar")));
|
||||
assert_eq!(tlv_stream.issuer, Some(&String::from("bar")));
|
||||
|
||||
@ -1233,7 +1298,7 @@ mod tests {
|
||||
.issuer("baz".into())
|
||||
.build()
|
||||
.unwrap();
|
||||
let (_, tlv_stream, _) = refund.as_tlv_stream();
|
||||
let (_, tlv_stream, _, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.issuer(), Some(PrintableString("baz")));
|
||||
assert_eq!(tlv_stream.issuer, Some(&String::from("baz")));
|
||||
}
|
||||
@ -1246,14 +1311,14 @@ mod tests {
|
||||
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.chain(Network::Bitcoin)
|
||||
.build().unwrap();
|
||||
let (_, _, tlv_stream) = refund.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.chain(), mainnet);
|
||||
assert_eq!(tlv_stream.chain, None);
|
||||
|
||||
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.chain(Network::Testnet)
|
||||
.build().unwrap();
|
||||
let (_, _, tlv_stream) = refund.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.chain(), testnet);
|
||||
assert_eq!(tlv_stream.chain, Some(&testnet));
|
||||
|
||||
@ -1261,7 +1326,7 @@ mod tests {
|
||||
.chain(Network::Regtest)
|
||||
.chain(Network::Testnet)
|
||||
.build().unwrap();
|
||||
let (_, _, tlv_stream) = refund.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.chain(), testnet);
|
||||
assert_eq!(tlv_stream.chain, Some(&testnet));
|
||||
}
|
||||
@ -1271,7 +1336,7 @@ mod tests {
|
||||
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.quantity(10)
|
||||
.build().unwrap();
|
||||
let (_, _, tlv_stream) = refund.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.quantity(), Some(10));
|
||||
assert_eq!(tlv_stream.quantity, Some(10));
|
||||
|
||||
@ -1279,7 +1344,7 @@ mod tests {
|
||||
.quantity(10)
|
||||
.quantity(1)
|
||||
.build().unwrap();
|
||||
let (_, _, tlv_stream) = refund.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.quantity(), Some(1));
|
||||
assert_eq!(tlv_stream.quantity, Some(1));
|
||||
}
|
||||
@ -1289,7 +1354,7 @@ mod tests {
|
||||
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.payer_note("bar".into())
|
||||
.build().unwrap();
|
||||
let (_, _, tlv_stream) = refund.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.payer_note(), Some(PrintableString("bar")));
|
||||
assert_eq!(tlv_stream.payer_note, Some(&String::from("bar")));
|
||||
|
||||
@ -1297,7 +1362,7 @@ mod tests {
|
||||
.payer_note("bar".into())
|
||||
.payer_note("baz".into())
|
||||
.build().unwrap();
|
||||
let (_, _, tlv_stream) = refund.as_tlv_stream();
|
||||
let (_, _, tlv_stream, _, _) = refund.as_tlv_stream();
|
||||
assert_eq!(refund.payer_note(), Some(PrintableString("baz")));
|
||||
assert_eq!(tlv_stream.payer_note, Some(&String::from("baz")));
|
||||
}
|
||||
@ -1522,7 +1587,81 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_refund_with_extra_tlv_records() {
|
||||
fn parses_refund_with_unknown_tlv_records() {
|
||||
const UNKNOWN_ODD_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 1;
|
||||
assert!(UNKNOWN_ODD_TYPE % 2 == 1);
|
||||
|
||||
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut encoded_refund = Vec::new();
|
||||
refund.write(&mut encoded_refund).unwrap();
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_refund).unwrap();
|
||||
BigSize(32).write(&mut encoded_refund).unwrap();
|
||||
[42u8; 32].write(&mut encoded_refund).unwrap();
|
||||
|
||||
match Refund::try_from(encoded_refund.clone()) {
|
||||
Ok(refund) => assert_eq!(refund.bytes, encoded_refund),
|
||||
Err(e) => panic!("error parsing refund: {:?}", e),
|
||||
}
|
||||
|
||||
const UNKNOWN_EVEN_TYPE: u64 = INVOICE_REQUEST_TYPES.end - 2;
|
||||
assert!(UNKNOWN_EVEN_TYPE % 2 == 0);
|
||||
|
||||
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut encoded_refund = Vec::new();
|
||||
refund.write(&mut encoded_refund).unwrap();
|
||||
BigSize(UNKNOWN_EVEN_TYPE).write(&mut encoded_refund).unwrap();
|
||||
BigSize(32).write(&mut encoded_refund).unwrap();
|
||||
[42u8; 32].write(&mut encoded_refund).unwrap();
|
||||
|
||||
match Refund::try_from(encoded_refund) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_refund_with_experimental_tlv_records() {
|
||||
const UNKNOWN_ODD_TYPE: u64 = EXPERIMENTAL_INVOICE_REQUEST_TYPES.start + 1;
|
||||
assert!(UNKNOWN_ODD_TYPE % 2 == 1);
|
||||
|
||||
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut encoded_refund = Vec::new();
|
||||
refund.write(&mut encoded_refund).unwrap();
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_refund).unwrap();
|
||||
BigSize(32).write(&mut encoded_refund).unwrap();
|
||||
[42u8; 32].write(&mut encoded_refund).unwrap();
|
||||
|
||||
match Refund::try_from(encoded_refund.clone()) {
|
||||
Ok(refund) => assert_eq!(refund.bytes, encoded_refund),
|
||||
Err(e) => panic!("error parsing refund: {:?}", e),
|
||||
}
|
||||
|
||||
const UNKNOWN_EVEN_TYPE: u64 = EXPERIMENTAL_INVOICE_REQUEST_TYPES.start;
|
||||
assert!(UNKNOWN_EVEN_TYPE % 2 == 0);
|
||||
|
||||
let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap()
|
||||
.build().unwrap();
|
||||
|
||||
let mut encoded_refund = Vec::new();
|
||||
refund.write(&mut encoded_refund).unwrap();
|
||||
BigSize(UNKNOWN_EVEN_TYPE).write(&mut encoded_refund).unwrap();
|
||||
BigSize(32).write(&mut encoded_refund).unwrap();
|
||||
[42u8; 32].write(&mut encoded_refund).unwrap();
|
||||
|
||||
match Refund::try_from(encoded_refund) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_refund_with_out_of_range_tlv_records() {
|
||||
let secp_ctx = Secp256k1::new();
|
||||
let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap());
|
||||
let refund = RefundBuilder::new(vec![1; 32], keys.public_key(), 1000).unwrap()
|
||||
@ -1538,5 +1677,16 @@ mod tests {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
|
||||
}
|
||||
|
||||
let mut encoded_refund = Vec::new();
|
||||
refund.write(&mut encoded_refund).unwrap();
|
||||
BigSize(EXPERIMENTAL_INVOICE_REQUEST_TYPES.end).write(&mut encoded_refund).unwrap();
|
||||
BigSize(32).write(&mut encoded_refund).unwrap();
|
||||
[42u8; 32].write(&mut encoded_refund).unwrap();
|
||||
|
||||
match Refund::try_from(encoded_refund) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,17 +15,22 @@ use crate::io;
|
||||
use crate::ln::inbound_payment::ExpandedKey;
|
||||
use crate::ln::msgs::DecodeError;
|
||||
use crate::offers::invoice::{
|
||||
check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, FallbackAddress,
|
||||
check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks,
|
||||
ExperimentalInvoiceTlvStream, ExperimentalInvoiceTlvStreamRef, FallbackAddress,
|
||||
InvoiceTlvStream, InvoiceTlvStreamRef,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::offers::invoice_macros::invoice_builder_methods_test;
|
||||
use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common};
|
||||
use crate::offers::invoice_request::InvoiceRequest;
|
||||
use crate::offers::merkle::{
|
||||
self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream,
|
||||
SIGNATURE_TLV_RECORD_SIZE,
|
||||
};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::offer::{
|
||||
Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, OFFER_TYPES,
|
||||
Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, OfferContents,
|
||||
OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES,
|
||||
};
|
||||
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
|
||||
use crate::types::features::{Bolt12InvoiceFeatures, OfferFeatures};
|
||||
@ -79,6 +84,8 @@ struct InvoiceContents {
|
||||
features: Bolt12InvoiceFeatures,
|
||||
signing_pubkey: PublicKey,
|
||||
message_paths: Vec<BlindedMessagePath>,
|
||||
#[cfg(test)]
|
||||
experimental_baz: Option<u64>,
|
||||
}
|
||||
|
||||
/// Builds a [`StaticInvoice`] from an [`Offer`].
|
||||
@ -130,10 +137,9 @@ impl<'a> StaticInvoiceBuilder<'a> {
|
||||
Ok(Self { offer_bytes: &offer.bytes, invoice, keys })
|
||||
}
|
||||
|
||||
/// Builds a signed [`StaticInvoice`] after checking for valid semantics.
|
||||
pub fn build_and_sign<T: secp256k1::Signing>(
|
||||
self, secp_ctx: &Secp256k1<T>,
|
||||
) -> Result<StaticInvoice, Bolt12SemanticError> {
|
||||
/// Builds an [`UnsignedStaticInvoice`] after checking for valid semantics, returning it along with
|
||||
/// the [`Keypair`] needed to sign it.
|
||||
pub fn build(self) -> Result<(UnsignedStaticInvoice, Keypair), Bolt12SemanticError> {
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
if self.invoice.is_offer_expired() {
|
||||
@ -149,7 +155,14 @@ impl<'a> StaticInvoiceBuilder<'a> {
|
||||
}
|
||||
|
||||
let Self { offer_bytes, invoice, keys } = self;
|
||||
let unsigned_invoice = UnsignedStaticInvoice::new(&offer_bytes, invoice);
|
||||
Ok((UnsignedStaticInvoice::new(&offer_bytes, invoice), keys))
|
||||
}
|
||||
|
||||
/// Builds a signed [`StaticInvoice`] after checking for valid semantics.
|
||||
pub fn build_and_sign<T: secp256k1::Signing>(
|
||||
self, secp_ctx: &Secp256k1<T>,
|
||||
) -> Result<StaticInvoice, Bolt12SemanticError> {
|
||||
let (unsigned_invoice, keys) = self.build()?;
|
||||
let invoice = unsigned_invoice
|
||||
.sign(|message: &UnsignedStaticInvoice| {
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.tagged_hash.as_digest(), &keys))
|
||||
@ -159,11 +172,15 @@ impl<'a> StaticInvoiceBuilder<'a> {
|
||||
}
|
||||
|
||||
invoice_builder_methods_common!(self, Self, self.invoice, Self, self, StaticInvoice, mut);
|
||||
|
||||
#[cfg(test)]
|
||||
invoice_builder_methods_test!(self, Self, self.invoice, Self, self, mut);
|
||||
}
|
||||
|
||||
/// A semantically valid [`StaticInvoice`] that hasn't been signed.
|
||||
pub struct UnsignedStaticInvoice {
|
||||
bytes: Vec<u8>,
|
||||
experimental_bytes: Vec<u8>,
|
||||
contents: InvoiceContents,
|
||||
tagged_hash: TaggedHash,
|
||||
}
|
||||
@ -269,16 +286,50 @@ macro_rules! invoice_accessors_signing_pubkey {
|
||||
|
||||
impl UnsignedStaticInvoice {
|
||||
fn new(offer_bytes: &Vec<u8>, contents: InvoiceContents) -> Self {
|
||||
let (_, invoice_tlv_stream) = contents.as_tlv_stream();
|
||||
let offer_bytes = WithoutLength(offer_bytes);
|
||||
let unsigned_tlv_stream = (offer_bytes, invoice_tlv_stream);
|
||||
let (_, invoice_tlv_stream, _, experimental_invoice_tlv_stream) = contents.as_tlv_stream();
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
unsigned_tlv_stream.write(&mut bytes).unwrap();
|
||||
// Allocate enough space for the invoice, which will include:
|
||||
// - all TLV records from `offer_bytes`,
|
||||
// - all invoice-specific TLV records, and
|
||||
// - a signature TLV record once the invoice is signed.
|
||||
let mut bytes = Vec::with_capacity(
|
||||
offer_bytes.len()
|
||||
+ invoice_tlv_stream.serialized_length()
|
||||
+ SIGNATURE_TLV_RECORD_SIZE
|
||||
+ experimental_invoice_tlv_stream.serialized_length(),
|
||||
);
|
||||
|
||||
let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes);
|
||||
// Use the offer bytes instead of the offer TLV stream as the latter may have contained
|
||||
// unknown TLV records, which are not stored in `InvoiceContents`.
|
||||
for record in TlvStream::new(offer_bytes).range(OFFER_TYPES) {
|
||||
record.write(&mut bytes).unwrap();
|
||||
}
|
||||
|
||||
Self { contents, tagged_hash, bytes }
|
||||
let remaining_bytes = &offer_bytes[bytes.len()..];
|
||||
|
||||
invoice_tlv_stream.write(&mut bytes).unwrap();
|
||||
|
||||
let mut experimental_tlv_stream =
|
||||
TlvStream::new(remaining_bytes).range(EXPERIMENTAL_OFFER_TYPES).peekable();
|
||||
let mut experimental_bytes = Vec::with_capacity(
|
||||
remaining_bytes.len()
|
||||
- experimental_tlv_stream
|
||||
.peek()
|
||||
.map_or(remaining_bytes.len(), |first_record| first_record.start)
|
||||
+ experimental_invoice_tlv_stream.serialized_length(),
|
||||
);
|
||||
|
||||
for record in experimental_tlv_stream {
|
||||
record.write(&mut experimental_bytes).unwrap();
|
||||
}
|
||||
|
||||
experimental_invoice_tlv_stream.write(&mut experimental_bytes).unwrap();
|
||||
debug_assert_eq!(experimental_bytes.len(), experimental_bytes.capacity());
|
||||
|
||||
let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes));
|
||||
let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
|
||||
|
||||
Self { bytes, experimental_bytes, contents, tagged_hash }
|
||||
}
|
||||
|
||||
/// Signs the [`TaggedHash`] of the invoice using the given function.
|
||||
@ -292,6 +343,15 @@ impl UnsignedStaticInvoice {
|
||||
let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature) };
|
||||
signature_tlv_stream.write(&mut self.bytes).unwrap();
|
||||
|
||||
// Append the experimental bytes after the signature.
|
||||
debug_assert_eq!(
|
||||
// The two-byte overallocation results from SIGNATURE_TLV_RECORD_SIZE accommodating TLV
|
||||
// records with types >= 253.
|
||||
self.bytes.len() + self.experimental_bytes.len() + 2,
|
||||
self.bytes.capacity(),
|
||||
);
|
||||
self.bytes.extend_from_slice(&self.experimental_bytes);
|
||||
|
||||
Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature })
|
||||
}
|
||||
|
||||
@ -341,12 +401,10 @@ impl StaticInvoice {
|
||||
}
|
||||
|
||||
pub(crate) fn from_same_offer(&self, invreq: &InvoiceRequest) -> bool {
|
||||
let invoice_offer_tlv_stream = TlvStream::new(&self.bytes)
|
||||
.range(OFFER_TYPES)
|
||||
.map(|tlv_record| tlv_record.record_bytes);
|
||||
let invreq_offer_tlv_stream = TlvStream::new(invreq.bytes())
|
||||
.range(OFFER_TYPES)
|
||||
.map(|tlv_record| tlv_record.record_bytes);
|
||||
let invoice_offer_tlv_stream =
|
||||
Offer::tlv_stream_iter(&self.bytes).map(|tlv_record| tlv_record.record_bytes);
|
||||
let invreq_offer_tlv_stream =
|
||||
Offer::tlv_stream_iter(invreq.bytes()).map(|tlv_record| tlv_record.record_bytes);
|
||||
invoice_offer_tlv_stream.eq(invreq_offer_tlv_stream)
|
||||
}
|
||||
}
|
||||
@ -375,6 +433,8 @@ impl InvoiceContents {
|
||||
fallbacks: None,
|
||||
features: Bolt12InvoiceFeatures::empty(),
|
||||
signing_pubkey,
|
||||
#[cfg(test)]
|
||||
experimental_baz: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,7 +460,14 @@ impl InvoiceContents {
|
||||
payment_hash: None,
|
||||
};
|
||||
|
||||
(self.offer.as_tlv_stream(), invoice)
|
||||
let experimental_invoice = ExperimentalInvoiceTlvStreamRef {
|
||||
#[cfg(test)]
|
||||
experimental_baz: self.experimental_baz,
|
||||
};
|
||||
|
||||
let (offer, experimental_offer) = self.offer.as_tlv_stream();
|
||||
|
||||
(offer, invoice, experimental_offer, experimental_invoice)
|
||||
}
|
||||
|
||||
fn chain(&self) -> ChainHash {
|
||||
@ -497,29 +564,54 @@ impl TryFrom<Vec<u8>> for StaticInvoice {
|
||||
}
|
||||
}
|
||||
|
||||
type FullInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream, SignatureTlvStream);
|
||||
type FullInvoiceTlvStream = (
|
||||
OfferTlvStream,
|
||||
InvoiceTlvStream,
|
||||
SignatureTlvStream,
|
||||
ExperimentalOfferTlvStream,
|
||||
ExperimentalInvoiceTlvStream,
|
||||
);
|
||||
|
||||
impl CursorReadable for FullInvoiceTlvStream {
|
||||
fn read<R: AsRef<[u8]>>(r: &mut io::Cursor<R>) -> Result<Self, DecodeError> {
|
||||
let offer = CursorReadable::read(r)?;
|
||||
let invoice = CursorReadable::read(r)?;
|
||||
let signature = CursorReadable::read(r)?;
|
||||
let experimental_offer = CursorReadable::read(r)?;
|
||||
let experimental_invoice = CursorReadable::read(r)?;
|
||||
|
||||
Ok((offer, invoice, signature))
|
||||
Ok((offer, invoice, signature, experimental_offer, experimental_invoice))
|
||||
}
|
||||
}
|
||||
|
||||
type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream);
|
||||
type PartialInvoiceTlvStream =
|
||||
(OfferTlvStream, InvoiceTlvStream, ExperimentalOfferTlvStream, ExperimentalInvoiceTlvStream);
|
||||
|
||||
type PartialInvoiceTlvStreamRef<'a> = (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>);
|
||||
type PartialInvoiceTlvStreamRef<'a> = (
|
||||
OfferTlvStreamRef<'a>,
|
||||
InvoiceTlvStreamRef<'a>,
|
||||
ExperimentalOfferTlvStreamRef,
|
||||
ExperimentalInvoiceTlvStreamRef,
|
||||
);
|
||||
|
||||
impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for StaticInvoice {
|
||||
type Error = Bolt12ParseError;
|
||||
|
||||
fn try_from(invoice: ParsedMessage<FullInvoiceTlvStream>) -> Result<Self, Self::Error> {
|
||||
let ParsedMessage { bytes, tlv_stream } = invoice;
|
||||
let (offer_tlv_stream, invoice_tlv_stream, SignatureTlvStream { signature }) = tlv_stream;
|
||||
let contents = InvoiceContents::try_from((offer_tlv_stream, invoice_tlv_stream))?;
|
||||
let (
|
||||
offer_tlv_stream,
|
||||
invoice_tlv_stream,
|
||||
SignatureTlvStream { signature },
|
||||
experimental_offer_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
) = tlv_stream;
|
||||
let contents = InvoiceContents::try_from((
|
||||
offer_tlv_stream,
|
||||
invoice_tlv_stream,
|
||||
experimental_offer_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
))?;
|
||||
|
||||
let signature = match signature {
|
||||
None => {
|
||||
@ -555,6 +647,11 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
|
||||
payment_hash,
|
||||
amount,
|
||||
},
|
||||
experimental_offer_tlv_stream,
|
||||
ExperimentalInvoiceTlvStream {
|
||||
#[cfg(test)]
|
||||
experimental_baz,
|
||||
},
|
||||
) = tlv_stream;
|
||||
|
||||
if payment_hash.is_some() {
|
||||
@ -587,7 +684,7 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
|
||||
}
|
||||
|
||||
Ok(InvoiceContents {
|
||||
offer: OfferContents::try_from(offer_tlv_stream)?,
|
||||
offer: OfferContents::try_from((offer_tlv_stream, experimental_offer_tlv_stream))?,
|
||||
payment_paths,
|
||||
message_paths,
|
||||
created_at,
|
||||
@ -595,6 +692,8 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
|
||||
fallbacks,
|
||||
features,
|
||||
signing_pubkey,
|
||||
#[cfg(test)]
|
||||
experimental_baz,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -605,14 +704,20 @@ mod tests {
|
||||
use crate::blinded_path::BlindedHop;
|
||||
use crate::ln::inbound_payment::ExpandedKey;
|
||||
use crate::ln::msgs::DecodeError;
|
||||
use crate::offers::invoice::InvoiceTlvStreamRef;
|
||||
use crate::offers::invoice::{
|
||||
ExperimentalInvoiceTlvStreamRef, InvoiceTlvStreamRef, EXPERIMENTAL_INVOICE_TYPES,
|
||||
INVOICE_TYPES,
|
||||
};
|
||||
use crate::offers::merkle;
|
||||
use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash};
|
||||
use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash, TlvStream};
|
||||
use crate::offers::nonce::Nonce;
|
||||
use crate::offers::offer::{Offer, OfferBuilder, OfferTlvStreamRef, Quantity};
|
||||
use crate::offers::offer::{
|
||||
ExperimentalOfferTlvStreamRef, Offer, OfferBuilder, OfferTlvStreamRef, Quantity,
|
||||
};
|
||||
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
|
||||
use crate::offers::static_invoice::{
|
||||
StaticInvoice, StaticInvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, SIGNATURE_TAG,
|
||||
StaticInvoice, StaticInvoiceBuilder, UnsignedStaticInvoice, DEFAULT_RELATIVE_EXPIRY,
|
||||
SIGNATURE_TAG,
|
||||
};
|
||||
use crate::offers::test_utils::*;
|
||||
use crate::sign::KeyMaterial;
|
||||
@ -623,27 +728,47 @@ mod tests {
|
||||
use bitcoin::Network;
|
||||
use core::time::Duration;
|
||||
|
||||
type FullInvoiceTlvStreamRef<'a> =
|
||||
(OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, SignatureTlvStreamRef<'a>);
|
||||
type FullInvoiceTlvStreamRef<'a> = (
|
||||
OfferTlvStreamRef<'a>,
|
||||
InvoiceTlvStreamRef<'a>,
|
||||
SignatureTlvStreamRef<'a>,
|
||||
ExperimentalOfferTlvStreamRef,
|
||||
ExperimentalInvoiceTlvStreamRef,
|
||||
);
|
||||
|
||||
impl StaticInvoice {
|
||||
fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
|
||||
let (offer_tlv_stream, invoice_tlv_stream) = self.contents.as_tlv_stream();
|
||||
let (
|
||||
offer_tlv_stream,
|
||||
invoice_tlv_stream,
|
||||
experimental_offer_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
) = self.contents.as_tlv_stream();
|
||||
(
|
||||
offer_tlv_stream,
|
||||
invoice_tlv_stream,
|
||||
SignatureTlvStreamRef { signature: Some(&self.signature) },
|
||||
experimental_offer_tlv_stream,
|
||||
experimental_invoice_tlv_stream,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn tlv_stream_to_bytes(
|
||||
tlv_stream: &(OfferTlvStreamRef, InvoiceTlvStreamRef, SignatureTlvStreamRef),
|
||||
tlv_stream: &(
|
||||
OfferTlvStreamRef,
|
||||
InvoiceTlvStreamRef,
|
||||
SignatureTlvStreamRef,
|
||||
ExperimentalOfferTlvStreamRef,
|
||||
ExperimentalInvoiceTlvStreamRef,
|
||||
),
|
||||
) -> Vec<u8> {
|
||||
let mut buffer = Vec::new();
|
||||
tlv_stream.0.write(&mut buffer).unwrap();
|
||||
tlv_stream.1.write(&mut buffer).unwrap();
|
||||
tlv_stream.2.write(&mut buffer).unwrap();
|
||||
tlv_stream.3.write(&mut buffer).unwrap();
|
||||
tlv_stream.4.write(&mut buffer).unwrap();
|
||||
buffer
|
||||
}
|
||||
|
||||
@ -773,6 +898,8 @@ mod tests {
|
||||
message_paths: Some(&paths),
|
||||
},
|
||||
SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
|
||||
ExperimentalOfferTlvStreamRef { experimental_foo: None },
|
||||
ExperimentalInvoiceTlvStreamRef { experimental_baz: None },
|
||||
)
|
||||
);
|
||||
|
||||
@ -840,6 +967,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_invoice_from_offer_using_derived_key() {
|
||||
let node_id = recipient_pubkey();
|
||||
let now = now();
|
||||
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
|
||||
let entropy = FixedEntropy {};
|
||||
let nonce = Nonce::from_entropy_source(&entropy);
|
||||
let secp_ctx = Secp256k1::new();
|
||||
|
||||
let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
|
||||
.path(blinded_path())
|
||||
.experimental_foo(42)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys(
|
||||
&offer,
|
||||
payment_paths(),
|
||||
vec![blinded_path()],
|
||||
now,
|
||||
&expanded_key,
|
||||
nonce,
|
||||
&secp_ctx,
|
||||
)
|
||||
.unwrap()
|
||||
.build_and_sign(&secp_ctx)
|
||||
{
|
||||
panic!("error building invoice: {:?}", e);
|
||||
}
|
||||
|
||||
let expanded_key = ExpandedKey::new(&KeyMaterial([41; 32]));
|
||||
if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys(
|
||||
&offer,
|
||||
payment_paths(),
|
||||
vec![blinded_path()],
|
||||
now,
|
||||
&expanded_key,
|
||||
nonce,
|
||||
&secp_ctx,
|
||||
) {
|
||||
assert_eq!(e, Bolt12SemanticError::InvalidMetadata);
|
||||
} else {
|
||||
panic!("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_build_with_missing_paths() {
|
||||
let node_id = recipient_pubkey();
|
||||
@ -887,7 +1060,7 @@ mod tests {
|
||||
|
||||
// Error if offer paths are missing.
|
||||
let mut offer_without_paths = valid_offer.clone();
|
||||
let mut offer_tlv_stream = offer_without_paths.as_tlv_stream();
|
||||
let (mut offer_tlv_stream, _) = offer_without_paths.as_tlv_stream();
|
||||
offer_tlv_stream.paths.take();
|
||||
let mut buffer = Vec::new();
|
||||
offer_tlv_stream.write(&mut buffer).unwrap();
|
||||
@ -923,7 +1096,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let mut offer_missing_issuer_id = valid_offer.clone();
|
||||
let mut offer_tlv_stream = offer_missing_issuer_id.as_tlv_stream();
|
||||
let (mut offer_tlv_stream, _) = offer_missing_issuer_id.as_tlv_stream();
|
||||
offer_tlv_stream.issuer_id.take();
|
||||
let mut buffer = Vec::new();
|
||||
offer_tlv_stream.write(&mut buffer).unwrap();
|
||||
@ -1185,7 +1358,263 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_invoice_with_extra_tlv_records() {
|
||||
fn parses_invoice_with_unknown_tlv_records() {
|
||||
let node_id = recipient_pubkey();
|
||||
let payment_paths = payment_paths();
|
||||
let now = now();
|
||||
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
|
||||
let entropy = FixedEntropy {};
|
||||
let nonce = Nonce::from_entropy_source(&entropy);
|
||||
let secp_ctx = Secp256k1::new();
|
||||
|
||||
let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
|
||||
.path(blinded_path())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
const UNKNOWN_ODD_TYPE: u64 = INVOICE_TYPES.end - 1;
|
||||
assert!(UNKNOWN_ODD_TYPE % 2 == 1);
|
||||
|
||||
let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys(
|
||||
&offer,
|
||||
payment_paths.clone(),
|
||||
vec![blinded_path()],
|
||||
now,
|
||||
&expanded_key,
|
||||
nonce,
|
||||
&secp_ctx,
|
||||
)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice.bytes.reserve_exact(
|
||||
unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice.bytes.extend_from_slice(&unknown_bytes);
|
||||
unsigned_invoice.tagged_hash =
|
||||
TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes);
|
||||
|
||||
let invoice = unsigned_invoice
|
||||
.sign(|message: &UnsignedStaticInvoice| {
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match StaticInvoice::try_from(encoded_invoice.clone()) {
|
||||
Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice),
|
||||
Err(e) => panic!("error parsing invoice: {:?}", e),
|
||||
}
|
||||
|
||||
const UNKNOWN_EVEN_TYPE: u64 = INVOICE_TYPES.end - 2;
|
||||
assert!(UNKNOWN_EVEN_TYPE % 2 == 0);
|
||||
|
||||
let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys(
|
||||
&offer,
|
||||
payment_paths.clone(),
|
||||
vec![blinded_path()],
|
||||
now,
|
||||
&expanded_key,
|
||||
nonce,
|
||||
&secp_ctx,
|
||||
)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice.bytes.reserve_exact(
|
||||
unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice.bytes.extend_from_slice(&unknown_bytes);
|
||||
unsigned_invoice.tagged_hash =
|
||||
TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &unsigned_invoice.bytes);
|
||||
|
||||
let invoice = unsigned_invoice
|
||||
.sign(|message: &UnsignedStaticInvoice| {
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match StaticInvoice::try_from(encoded_invoice) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_invoice_with_experimental_tlv_records() {
|
||||
let node_id = recipient_pubkey();
|
||||
let payment_paths = payment_paths();
|
||||
let now = now();
|
||||
let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
|
||||
let entropy = FixedEntropy {};
|
||||
let nonce = Nonce::from_entropy_source(&entropy);
|
||||
let secp_ctx = Secp256k1::new();
|
||||
|
||||
let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
|
||||
.path(blinded_path())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys(
|
||||
&offer,
|
||||
payment_paths.clone(),
|
||||
vec![blinded_path()],
|
||||
now,
|
||||
&expanded_key,
|
||||
nonce,
|
||||
&secp_ctx,
|
||||
)
|
||||
.unwrap()
|
||||
.experimental_baz(42)
|
||||
.build_and_sign(&secp_ctx)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
assert!(StaticInvoice::try_from(encoded_invoice).is_ok());
|
||||
|
||||
const UNKNOWN_ODD_TYPE: u64 = EXPERIMENTAL_INVOICE_TYPES.start + 1;
|
||||
assert!(UNKNOWN_ODD_TYPE % 2 == 1);
|
||||
|
||||
let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys(
|
||||
&offer,
|
||||
payment_paths.clone(),
|
||||
vec![blinded_path()],
|
||||
now,
|
||||
&expanded_key,
|
||||
nonce,
|
||||
&secp_ctx,
|
||||
)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice.bytes.reserve_exact(
|
||||
unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice.experimental_bytes.extend_from_slice(&unknown_bytes);
|
||||
|
||||
let tlv_stream = TlvStream::new(&unsigned_invoice.bytes)
|
||||
.chain(TlvStream::new(&unsigned_invoice.experimental_bytes));
|
||||
unsigned_invoice.tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
|
||||
|
||||
let invoice = unsigned_invoice
|
||||
.sign(|message: &UnsignedStaticInvoice| {
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match StaticInvoice::try_from(encoded_invoice.clone()) {
|
||||
Ok(invoice) => assert_eq!(invoice.bytes, encoded_invoice),
|
||||
Err(e) => panic!("error parsing invoice: {:?}", e),
|
||||
}
|
||||
|
||||
const UNKNOWN_EVEN_TYPE: u64 = EXPERIMENTAL_INVOICE_TYPES.start;
|
||||
assert!(UNKNOWN_EVEN_TYPE % 2 == 0);
|
||||
|
||||
let (mut unsigned_invoice, keys) = StaticInvoiceBuilder::for_offer_using_derived_keys(
|
||||
&offer,
|
||||
payment_paths.clone(),
|
||||
vec![blinded_path()],
|
||||
now,
|
||||
&expanded_key,
|
||||
nonce,
|
||||
&secp_ctx,
|
||||
)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mut unknown_bytes = Vec::new();
|
||||
BigSize(UNKNOWN_EVEN_TYPE).write(&mut unknown_bytes).unwrap();
|
||||
BigSize(32).write(&mut unknown_bytes).unwrap();
|
||||
[42u8; 32].write(&mut unknown_bytes).unwrap();
|
||||
|
||||
unsigned_invoice.bytes.reserve_exact(
|
||||
unsigned_invoice.bytes.capacity() - unsigned_invoice.bytes.len() + unknown_bytes.len(),
|
||||
);
|
||||
unsigned_invoice.experimental_bytes.extend_from_slice(&unknown_bytes);
|
||||
|
||||
let tlv_stream = TlvStream::new(&unsigned_invoice.bytes)
|
||||
.chain(TlvStream::new(&unsigned_invoice.experimental_bytes));
|
||||
unsigned_invoice.tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream);
|
||||
|
||||
let invoice = unsigned_invoice
|
||||
.sign(|message: &UnsignedStaticInvoice| {
|
||||
Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match StaticInvoice::try_from(encoded_invoice) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::UnknownRequiredFeature)),
|
||||
}
|
||||
|
||||
let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx)
|
||||
.path(blinded_path())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys(
|
||||
&offer,
|
||||
payment_paths.clone(),
|
||||
vec![blinded_path()],
|
||||
now,
|
||||
&expanded_key,
|
||||
nonce,
|
||||
&secp_ctx,
|
||||
)
|
||||
.unwrap()
|
||||
.build_and_sign(&secp_ctx)
|
||||
.unwrap();
|
||||
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
||||
BigSize(UNKNOWN_ODD_TYPE).write(&mut encoded_invoice).unwrap();
|
||||
BigSize(32).write(&mut encoded_invoice).unwrap();
|
||||
[42u8; 32].write(&mut encoded_invoice).unwrap();
|
||||
|
||||
match StaticInvoice::try_from(encoded_invoice) {
|
||||
Ok(_) => panic!("expected error"),
|
||||
Err(e) => assert_eq!(
|
||||
e,
|
||||
Bolt12ParseError::InvalidSignature(secp256k1::Error::IncorrectSignature)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_parsing_invoice_with_out_of_range_tlv_records() {
|
||||
let invoice = invoice();
|
||||
let mut encoded_invoice = Vec::new();
|
||||
invoice.write(&mut encoded_invoice).unwrap();
|
||||
|
@ -1402,53 +1402,38 @@ impl<T: Writeable> Writeable for RwLock<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Readable, B: Readable> Readable for (A, B) {
|
||||
fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
|
||||
let a: A = Readable::read(r)?;
|
||||
let b: B = Readable::read(r)?;
|
||||
Ok((a, b))
|
||||
}
|
||||
}
|
||||
impl<A: Writeable, B: Writeable> Writeable for (A, B) {
|
||||
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
|
||||
self.0.write(w)?;
|
||||
self.1.write(w)
|
||||
macro_rules! impl_tuple_ser {
|
||||
($($i: ident : $type: tt),*) => {
|
||||
impl<$($type),*> Readable for ($($type),*)
|
||||
where $(
|
||||
$type: Readable,
|
||||
)*
|
||||
{
|
||||
fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
|
||||
Ok(($(<$type as Readable>::read(r)?),*))
|
||||
}
|
||||
}
|
||||
|
||||
impl<$($type),*> Writeable for ($($type),*)
|
||||
where $(
|
||||
$type: Writeable,
|
||||
)*
|
||||
{
|
||||
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
|
||||
let ($($i),*) = self;
|
||||
$($i.write(w)?;)*
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Readable, B: Readable, C: Readable> Readable for (A, B, C) {
|
||||
fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
|
||||
let a: A = Readable::read(r)?;
|
||||
let b: B = Readable::read(r)?;
|
||||
let c: C = Readable::read(r)?;
|
||||
Ok((a, b, c))
|
||||
}
|
||||
}
|
||||
impl<A: Writeable, B: Writeable, C: Writeable> Writeable for (A, B, C) {
|
||||
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
|
||||
self.0.write(w)?;
|
||||
self.1.write(w)?;
|
||||
self.2.write(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Readable, B: Readable, C: Readable, D: Readable> Readable for (A, B, C, D) {
|
||||
fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
|
||||
let a: A = Readable::read(r)?;
|
||||
let b: B = Readable::read(r)?;
|
||||
let c: C = Readable::read(r)?;
|
||||
let d: D = Readable::read(r)?;
|
||||
Ok((a, b, c, d))
|
||||
}
|
||||
}
|
||||
impl<A: Writeable, B: Writeable, C: Writeable, D: Writeable> Writeable for (A, B, C, D) {
|
||||
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
|
||||
self.0.write(w)?;
|
||||
self.1.write(w)?;
|
||||
self.2.write(w)?;
|
||||
self.3.write(w)
|
||||
}
|
||||
}
|
||||
impl_tuple_ser!(a: A, b: B);
|
||||
impl_tuple_ser!(a: A, b: B, c: C);
|
||||
impl_tuple_ser!(a: A, b: B, c: C, d: D);
|
||||
impl_tuple_ser!(a: A, b: B, c: C, d: D, e: E);
|
||||
impl_tuple_ser!(a: A, b: B, c: C, d: D, e: E, f: F);
|
||||
impl_tuple_ser!(a: A, b: B, c: C, d: D, e: E, f: F, g: G);
|
||||
|
||||
impl Writeable for () {
|
||||
fn write<W: Writer>(&self, _: &mut W) -> Result<(), io::Error> {
|
||||
|
@ -952,7 +952,7 @@ macro_rules! impl_writeable_tlv_based {
|
||||
/// [`Readable`]: crate::util::ser::Readable
|
||||
/// [`Writeable`]: crate::util::ser::Writeable
|
||||
macro_rules! tlv_stream {
|
||||
($name:ident, $nameref:ident, $range:expr, {
|
||||
($name:ident, $nameref:ident $(<$lifetime:lifetime>)?, $range:expr, {
|
||||
$(($type:expr, $field:ident : $fieldty:tt)),* $(,)*
|
||||
}) => {
|
||||
#[derive(Debug)]
|
||||
@ -964,13 +964,13 @@ macro_rules! tlv_stream {
|
||||
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct $nameref<'a> {
|
||||
pub(crate) struct $nameref<$($lifetime)*> {
|
||||
$(
|
||||
pub(super) $field: Option<tlv_record_ref_type!($fieldty)>,
|
||||
)*
|
||||
}
|
||||
|
||||
impl<'a> $crate::util::ser::Writeable for $nameref<'a> {
|
||||
impl<$($lifetime)*> $crate::util::ser::Writeable for $nameref<$($lifetime)*> {
|
||||
fn write<W: $crate::util::ser::Writer>(&self, writer: &mut W) -> Result<(), $crate::io::Error> {
|
||||
encode_tlv_stream!(writer, {
|
||||
$(($type, self.$field, (option, encoding: $fieldty))),*
|
||||
|
Loading…
Reference in New Issue
Block a user