Skip to content

Commit bebe2d6

Browse files
committed
Invoice request raw byte encoding and decoding
When reading an offer, an `invoice_request` message is sent over the wire. Implement Writeable for encoding the message and TryFrom for decoding it by defining in terms of TLV streams. These streams represent content for the payer metadata (0), reflected `offer` (1-79), `invoice_request` (80-159), and signature (240).
1 parent 74432fc commit bebe2d6

File tree

7 files changed

+243
-6
lines changed

7 files changed

+243
-6
lines changed

lightning/src/offers/invoice_request.rs

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@
1212
use bitcoin::blockdata::constants::ChainHash;
1313
use bitcoin::secp256k1::PublicKey;
1414
use bitcoin::secp256k1::schnorr::Signature;
15+
use core::convert::TryFrom;
16+
use crate::io;
1517
use crate::ln::features::InvoiceRequestFeatures;
16-
use crate::offers::offer::OfferContents;
17-
use crate::offers::payer::PayerContents;
18+
use crate::ln::msgs::DecodeError;
19+
use crate::offers::merkle::{SignatureTlvStream, self};
20+
use crate::offers::offer::{Amount, OfferContents, OfferTlvStream};
21+
use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
22+
use crate::offers::payer::{PayerContents, PayerTlvStream};
23+
use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
1824
use crate::util::string::PrintableString;
1925

2026
use crate::prelude::*;
@@ -75,9 +81,9 @@ impl InvoiceRequest {
7581
&self.contents.features
7682
}
7783

78-
/// The quantity of the offer's item conforming to [`Offer::supported_quantity`].
84+
/// The quantity of the offer's item conforming to [`Offer::is_valid_quantity`].
7985
///
80-
/// [`Offer::supported_quantity`]: crate::offers::offer::Offer::supported_quantity
86+
/// [`Offer::is_valid_quantity`]: crate::offers::offer::Offer::is_valid_quantity
8187
pub fn quantity(&self) -> Option<u64> {
8288
self.contents.quantity
8389
}
@@ -99,3 +105,111 @@ impl InvoiceRequest {
99105
self.signature
100106
}
101107
}
108+
109+
impl Writeable for InvoiceRequest {
110+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
111+
WithoutLength(&self.bytes).write(writer)
112+
}
113+
}
114+
115+
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, 80..160, {
116+
(80, chain: ChainHash),
117+
(82, amount: (u64, HighZeroBytesDroppedBigSize)),
118+
(84, features: InvoiceRequestFeatures),
119+
(86, quantity: (u64, HighZeroBytesDroppedBigSize)),
120+
(88, payer_id: PublicKey),
121+
(89, payer_note: (String, WithoutLength)),
122+
});
123+
124+
type FullInvoiceRequestTlvStream =
125+
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream);
126+
127+
impl SeekReadable for FullInvoiceRequestTlvStream {
128+
fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
129+
let payer = SeekReadable::read(r)?;
130+
let offer = SeekReadable::read(r)?;
131+
let invoice_request = SeekReadable::read(r)?;
132+
let signature = SeekReadable::read(r)?;
133+
134+
Ok((payer, offer, invoice_request, signature))
135+
}
136+
}
137+
138+
type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream);
139+
140+
impl TryFrom<Vec<u8>> for InvoiceRequest {
141+
type Error = ParseError;
142+
143+
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
144+
let invoice_request = ParsedMessage::<FullInvoiceRequestTlvStream>::try_from(bytes)?;
145+
let ParsedMessage {bytes, tlv_stream } = invoice_request;
146+
let (
147+
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
148+
SignatureTlvStream { signature },
149+
) = tlv_stream;
150+
let contents = InvoiceRequestContents::try_from(
151+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
152+
)?;
153+
154+
if let Some(signature) = &signature {
155+
let tag = concat!("lightning", "invoice_request", "signature");
156+
merkle::verify_signature(signature, tag, &bytes, contents.payer_id)?;
157+
}
158+
159+
Ok(InvoiceRequest { bytes, contents, signature })
160+
}
161+
}
162+
163+
impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
164+
type Error = SemanticError;
165+
166+
fn try_from(tlv_stream: PartialInvoiceRequestTlvStream) -> Result<Self, Self::Error> {
167+
let (
168+
PayerTlvStream { metadata },
169+
offer_tlv_stream,
170+
InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note },
171+
) = tlv_stream;
172+
173+
let payer = PayerContents(metadata);
174+
let offer = OfferContents::try_from(offer_tlv_stream)?;
175+
176+
if !offer.supports_chain(chain.unwrap_or_else(|| offer.implied_chain())) {
177+
return Err(SemanticError::UnsupportedChain);
178+
}
179+
180+
let amount_msats = match (offer.amount(), amount) {
181+
(None, None) => return Err(SemanticError::MissingAmount),
182+
(Some(Amount::Currency { .. }), _) => return Err(SemanticError::UnsupportedCurrency),
183+
(_, amount_msats) => amount_msats,
184+
};
185+
186+
let features = features.unwrap_or_else(InvoiceRequestFeatures::empty);
187+
188+
let expects_quantity = offer.expects_quantity();
189+
let quantity = match quantity {
190+
None if expects_quantity => return Err(SemanticError::MissingQuantity),
191+
Some(_) if !expects_quantity => return Err(SemanticError::UnexpectedQuantity),
192+
Some(quantity) if !offer.is_valid_quantity(quantity) => {
193+
return Err(SemanticError::InvalidQuantity);
194+
}
195+
quantity => quantity,
196+
};
197+
198+
{
199+
let amount_msats = amount_msats.unwrap_or(offer.amount_msats());
200+
let quantity = quantity.unwrap_or(1);
201+
if amount_msats < offer.expected_invoice_amount_msats(quantity) {
202+
return Err(SemanticError::InsufficientAmount);
203+
}
204+
}
205+
206+
let payer_id = match payer_id {
207+
None => return Err(SemanticError::MissingPayerId),
208+
Some(payer_id) => payer_id,
209+
};
210+
211+
Ok(InvoiceRequestContents {
212+
payer, offer, chain, amount_msats, features, quantity, payer_id, payer_note,
213+
})
214+
}
215+
}

lightning/src/offers/merkle.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//! Tagged hashes for use in signature calculation and verification.
1111
1212
use bitcoin::hashes::{Hash, HashEngine, sha256};
13+
use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
14+
use bitcoin::secp256k1::schnorr::Signature;
1315
use crate::io;
1416
use crate::util::ser::{BigSize, Readable};
1517

@@ -18,6 +20,23 @@ use crate::prelude::*;
1820
/// Valid type range for signature TLV records.
1921
const SIGNATURE_TYPES: core::ops::RangeInclusive<u64> = 240..=1000;
2022

23+
tlv_stream!(SignatureTlvStream, SignatureTlvStreamRef, SIGNATURE_TYPES, {
24+
(240, signature: Signature),
25+
});
26+
27+
/// Verifies the signature with a pubkey over the given bytes using a tagged hash as the message
28+
/// digest.
29+
pub(super) fn verify_signature(
30+
signature: &Signature, tag: &str, bytes: &[u8], pubkey: PublicKey,
31+
) -> Result<(), secp256k1::Error> {
32+
let tag = sha256::Hash::hash(tag.as_bytes());
33+
let merkle_root = root_hash(bytes);
34+
let digest = Message::from_slice(&tagged_hash(tag, merkle_root)).unwrap();
35+
let pubkey = pubkey.into();
36+
let secp_ctx = Secp256k1::verification_only();
37+
secp_ctx.verify_schnorr(signature, &digest, &pubkey)
38+
}
39+
2140
/// Computes a merkle root hash for the given data, which must be a well-formed TLV stream
2241
/// containing at least one TLV record.
2342
fn root_hash(data: &[u8]) -> sha256::Hash {

lightning/src/offers/offer.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ impl Offer {
270270
self.contents.chains()
271271
}
272272

273+
/// Returns whether the given chain is supported by the offer.
274+
pub fn supports_chain(&self, chain: ChainHash) -> bool {
275+
self.contents.supports_chain(chain)
276+
}
277+
273278
// TODO: Link to corresponding method in `InvoiceRequest`.
274279
/// Opaque bytes set by the originator. Useful for authentication and validating fields since it
275280
/// is reflected in `invoice_request` messages along with all the other fields from the `offer`.
@@ -279,7 +284,7 @@ impl Offer {
279284

280285
/// The minimum amount required for a successful payment of a single item.
281286
pub fn amount(&self) -> Option<&Amount> {
282-
self.contents.amount.as_ref()
287+
self.contents.amount()
283288
}
284289

285290
/// A complete description of the purpose of the payment. Intended to be displayed to the user
@@ -329,6 +334,18 @@ impl Offer {
329334
self.contents.supported_quantity()
330335
}
331336

337+
/// Returns whether the given quantity is valid for the offer.
338+
pub fn is_valid_quantity(&self, quantity: u64) -> bool {
339+
self.contents.is_valid_quantity(quantity)
340+
}
341+
342+
/// Returns whether a quantity is expected in an [`InvoiceRequest`] for the offer.
343+
///
344+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
345+
pub fn expects_quantity(&self) -> bool {
346+
self.contents.expects_quantity()
347+
}
348+
332349
/// The public key used by the recipient to sign invoices.
333350
pub fn signing_pubkey(&self) -> PublicKey {
334351
self.contents.signing_pubkey.unwrap()
@@ -355,10 +372,48 @@ impl OfferContents {
355372
ChainHash::using_genesis_block(Network::Bitcoin)
356373
}
357374

375+
pub fn supports_chain(&self, chain: ChainHash) -> bool {
376+
self.chains().contains(&chain)
377+
}
378+
379+
pub fn amount(&self) -> Option<&Amount> {
380+
self.amount.as_ref()
381+
}
382+
383+
pub fn amount_msats(&self) -> u64 {
384+
match self.amount() {
385+
None => 0,
386+
Some(&Amount::Bitcoin { amount_msats }) => amount_msats,
387+
Some(&Amount::Currency { .. }) => unreachable!(),
388+
}
389+
}
390+
391+
pub fn expected_invoice_amount_msats(&self, quantity: u64) -> u64 {
392+
self.amount_msats() * quantity
393+
}
394+
358395
pub fn supported_quantity(&self) -> Quantity {
359396
self.supported_quantity
360397
}
361398

399+
pub fn is_valid_quantity(&self, quantity: u64) -> bool {
400+
match self.supported_quantity {
401+
Quantity::Bounded(n) => {
402+
let n = n.get();
403+
if n == 1 { false }
404+
else { quantity > 0 && quantity <= n }
405+
},
406+
Quantity::Unbounded => quantity > 0,
407+
}
408+
}
409+
410+
pub fn expects_quantity(&self) -> bool {
411+
match self.supported_quantity {
412+
Quantity::Bounded(n) => n.get() != 1,
413+
Quantity::Unbounded => true,
414+
}
415+
}
416+
362417
fn as_tlv_stream(&self) -> OfferTlvStreamRef {
363418
let (currency, amount) = match &self.amount {
364419
None => (None, None),
@@ -572,6 +627,7 @@ mod tests {
572627

573628
assert_eq!(offer.bytes, buffer.as_slice());
574629
assert_eq!(offer.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]);
630+
assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin)));
575631
assert_eq!(offer.metadata(), None);
576632
assert_eq!(offer.amount(), None);
577633
assert_eq!(offer.description(), PrintableString("foo"));
@@ -610,13 +666,15 @@ mod tests {
610666
.chain(Network::Bitcoin)
611667
.build()
612668
.unwrap();
669+
assert!(offer.supports_chain(mainnet));
613670
assert_eq!(offer.chains(), vec![mainnet]);
614671
assert_eq!(offer.as_tlv_stream().chains, None);
615672

616673
let offer = OfferBuilder::new("foo".into(), pubkey(42))
617674
.chain(Network::Testnet)
618675
.build()
619676
.unwrap();
677+
assert!(offer.supports_chain(testnet));
620678
assert_eq!(offer.chains(), vec![testnet]);
621679
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet]));
622680

@@ -625,6 +683,7 @@ mod tests {
625683
.chain(Network::Testnet)
626684
.build()
627685
.unwrap();
686+
assert!(offer.supports_chain(testnet));
628687
assert_eq!(offer.chains(), vec![testnet]);
629688
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet]));
630689

@@ -633,6 +692,8 @@ mod tests {
633692
.chain(Network::Testnet)
634693
.build()
635694
.unwrap();
695+
assert!(offer.supports_chain(mainnet));
696+
assert!(offer.supports_chain(testnet));
636697
assert_eq!(offer.chains(), vec![mainnet, testnet]);
637698
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![mainnet, testnet]));
638699
}

lightning/src/offers/parse.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
1212
use bitcoin::bech32;
1313
use bitcoin::bech32::{FromBase32, ToBase32};
14+
use bitcoin::secp256k1;
1415
use core::convert::TryFrom;
1516
use core::fmt;
1617
use crate::io;
@@ -115,23 +116,35 @@ pub enum ParseError {
115116
Decode(DecodeError),
116117
/// The parsed message has invalid semantics.
117118
InvalidSemantics(SemanticError),
119+
/// The parsed message has an invalid signature.
120+
InvalidSignature(secp256k1::Error),
118121
}
119122

120123
/// Error when interpreting a TLV stream as a specific type.
121124
#[derive(Debug, PartialEq)]
122125
pub enum SemanticError {
126+
/// The provided chain hash does not correspond to a supported chain.
127+
UnsupportedChain,
123128
/// An amount was expected but was missing.
124129
MissingAmount,
125130
/// The amount exceeded the total bitcoin supply.
126131
InvalidAmount,
132+
/// An amount was provided but was not sufficient in value.
133+
InsufficientAmount,
127134
/// A currency was provided that is not supported.
128135
UnsupportedCurrency,
129136
/// A required description was not provided.
130137
MissingDescription,
131138
/// A signing pubkey was not provided.
132139
MissingSigningPubkey,
140+
/// A quantity was not provided.
141+
MissingQuantity,
133142
/// An unsupported quantity was provided.
134143
InvalidQuantity,
144+
/// A quantity or quantity bounds was provided but was not expected.
145+
UnexpectedQuantity,
146+
/// A payer id was expected but was missing.
147+
MissingPayerId,
135148
}
136149

137150
impl From<bech32::Error> for ParseError {
@@ -151,3 +164,9 @@ impl From<SemanticError> for ParseError {
151164
Self::InvalidSemantics(error)
152165
}
153166
}
167+
168+
impl From<secp256k1::Error> for ParseError {
169+
fn from(error: secp256k1::Error) -> Self {
170+
Self::InvalidSignature(error)
171+
}
172+
}

lightning/src/offers/payer.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
//! Data structures and encoding for `invoice_request_metadata` records.
1111
12+
use crate::util::ser::WithoutLength;
13+
1214
use crate::prelude::*;
1315

1416
/// An unpredictable sequence of bytes typically containing information needed to derive
@@ -17,3 +19,7 @@ use crate::prelude::*;
1719
/// [`InvoiceRequestContents::payer_id`]: crate::offers::invoice_request::InvoiceRequestContents::payer_id
1820
#[derive(Clone, Debug)]
1921
pub(crate) struct PayerContents(pub Option<Vec<u8>>);
22+
23+
tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, {
24+
(0, metadata: (Vec<u8>, WithoutLength)),
25+
});

0 commit comments

Comments
 (0)