Skip to content

Commit 42831da

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 769b3b6 commit 42831da

File tree

7 files changed

+237
-6
lines changed

7 files changed

+237
-6
lines changed

lightning/src/offers/invoice_request.rs

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@
1212
use bitcoin::blockdata::constants::ChainHash;
1313
use bitcoin::secp256k1::PublicKey;
1414
use bitcoin::secp256k1::schnorr::Signature;
15+
use core::convert::TryFrom;
16+
use io;
1517
use ln::features::OfferFeatures;
16-
use offers::offer::OfferContents;
17-
use offers::payer::PayerContents;
18+
use offers::merkle::{SignatureTlvStream, self};
19+
use offers::offer::{Amount, OfferContents, OfferTlvStream};
20+
use offers::parse::{ParseError, SemanticError};
21+
use offers::payer::{PayerContents, PayerTlvStream};
22+
use util::ser::{Readable, WithoutLength, Writeable, Writer};
1823

1924
use prelude::*;
2025

@@ -49,7 +54,7 @@ impl InvoiceRequest {
4954
/// [`payer_id`].
5055
///
5156
/// [`payer_id`]: Self::payer_id
52-
pub fn payer_info(&self) -> Option<&Vec<u8>> {
57+
pub fn metadata(&self) -> Option<&Vec<u8>> {
5358
self.contents.payer.0.as_ref()
5459
}
5560

@@ -100,3 +105,116 @@ impl InvoiceRequest {
100105
self.signature
101106
}
102107
}
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+
impl TryFrom<Vec<u8>> for InvoiceRequest {
116+
type Error = ParseError;
117+
118+
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
119+
let tlv_stream: FullInvoiceRequestTlvStream = Readable::read(&mut &bytes[..])?;
120+
InvoiceRequest::try_from((bytes, tlv_stream))
121+
}
122+
}
123+
124+
tlv_stream!(InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef, {
125+
(80, chain: ChainHash),
126+
(82, amount: u64),
127+
(84, features: OfferFeatures),
128+
(86, quantity: u64),
129+
(88, payer_id: PublicKey),
130+
(89, payer_note: String),
131+
});
132+
133+
type ParsedInvoiceRequest = (Vec<u8>, FullInvoiceRequestTlvStream);
134+
135+
type FullInvoiceRequestTlvStream =
136+
(PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, SignatureTlvStream);
137+
138+
type PartialInvoiceRequestTlvStream = (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream);
139+
140+
impl TryFrom<ParsedInvoiceRequest> for InvoiceRequest {
141+
type Error = ParseError;
142+
143+
fn try_from(invoice_request: ParsedInvoiceRequest) -> Result<Self, Self::Error> {
144+
let (bytes, tlv_stream) = invoice_request;
145+
let (
146+
payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream,
147+
SignatureTlvStream { signature },
148+
) = tlv_stream;
149+
let contents = InvoiceRequestContents::try_from(
150+
(payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
151+
)?;
152+
153+
if let Some(signature) = &signature {
154+
let tag = concat!("lightning", "invoice_request", "signature");
155+
merkle::verify_signature(signature, tag, &bytes, contents.payer_id)?;
156+
}
157+
158+
Ok(InvoiceRequest { bytes, contents, signature })
159+
}
160+
}
161+
162+
impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
163+
type Error = SemanticError;
164+
165+
fn try_from(tlv_stream: PartialInvoiceRequestTlvStream) -> Result<Self, Self::Error> {
166+
let (
167+
PayerTlvStream { metadata },
168+
offer_tlv_stream,
169+
InvoiceRequestTlvStream { chain, amount, features, quantity, payer_id, payer_note },
170+
) = tlv_stream;
171+
172+
let payer = PayerContents(metadata);
173+
let offer = OfferContents::try_from(offer_tlv_stream)?;
174+
175+
if !offer.supports_chain(chain.unwrap_or_else(|| offer.implied_chain())) {
176+
return Err(SemanticError::UnsupportedChain);
177+
}
178+
179+
let amount_msats = match (offer.amount(), amount) {
180+
(None, None) => return Err(SemanticError::MissingAmount),
181+
// TODO: Handle currency case
182+
(Some(Amount::Currency { .. }), _) => return Err(SemanticError::UnsupportedCurrency),
183+
(_, amount_msats) => amount_msats,
184+
};
185+
186+
if let Some(features) = &features {
187+
if features.requires_unknown_bits() {
188+
return Err(SemanticError::UnknownRequiredFeatures);
189+
}
190+
}
191+
192+
let expects_quantity = offer.expects_quantity();
193+
let quantity = match quantity {
194+
None if expects_quantity => return Err(SemanticError::MissingQuantity),
195+
Some(_) if !expects_quantity => return Err(SemanticError::UnexpectedQuantity),
196+
Some(quantity) if !offer.is_valid_quantity(quantity) => {
197+
return Err(SemanticError::InvalidQuantity);
198+
}
199+
quantity => quantity,
200+
};
201+
202+
{
203+
let amount_msats = amount_msats.unwrap_or(offer.amount_msats());
204+
let quantity = quantity.unwrap_or(1);
205+
if amount_msats < offer.base_invoice_amount_msats(quantity) {
206+
return Err(SemanticError::InsufficientAmount);
207+
}
208+
}
209+
210+
211+
let payer_id = match payer_id {
212+
None => return Err(SemanticError::MissingPayerId),
213+
Some(payer_id) => payer_id,
214+
};
215+
216+
Ok(InvoiceRequestContents {
217+
payer, offer, chain, amount_msats, features, quantity, payer_id, payer_note,
218+
})
219+
}
220+
}

lightning/src/offers/merkle.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,32 @@
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 util::ser::{BigSize, Readable};
1416

1517
use prelude::*;
1618

1719
/// Valid type range for signature TLV records.
1820
const SIGNATURE_TYPES: core::ops::RangeInclusive<u64> = 240..=1000;
1921

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

lightning/src/offers/offer.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,11 @@ impl Offer {
294294
self.contents.chains()
295295
}
296296

297+
/// Returns whether the given chain is supported by the offer.
298+
pub fn supports_chain(&self, chain: ChainHash) -> bool {
299+
self.contents.supports_chain(chain)
300+
}
301+
297302
// TODO: Link to corresponding method in `InvoiceRequest`.
298303
/// Metadata set by the originator. Useful for authentication and validating fields since it is
299304
/// reflected in `invoice_request` messages along with all the other fields from the `offer`.
@@ -303,7 +308,12 @@ impl Offer {
303308

304309
/// The minimum amount required for a successful payment of a single item.
305310
pub fn amount(&self) -> Option<&Amount> {
306-
self.contents.amount.as_ref()
311+
self.contents.amount()
312+
}
313+
314+
/// Returns the minimum amount in msats required for the given quantity.
315+
pub fn base_invoice_amount_msats(&self, quantity: u64) -> u64 {
316+
self.contents.base_invoice_amount_msats(quantity)
307317
}
308318

309319
/// A complete description of the purpose of the payment.
@@ -357,6 +367,18 @@ impl Offer {
357367
self.contents.quantity_max()
358368
}
359369

370+
/// Returns whether the given quantity is valid for the offer.
371+
pub fn is_valid_quantity(&self, quantity: u64) -> bool {
372+
self.contents.is_valid_quantity(quantity)
373+
}
374+
375+
/// Returns whether a quantity is expected in an [`InvoiceRequest`] for the offer.
376+
///
377+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
378+
pub fn expects_quantity(&self) -> bool {
379+
self.contents.expects_quantity()
380+
}
381+
360382
/// The public key used by the recipient to sign invoices.
361383
pub fn signing_pubkey(&self) -> PublicKey {
362384
self.contents.signing_pubkey.unwrap()
@@ -383,8 +405,20 @@ impl OfferContents {
383405
ChainHash::using_genesis_block(Network::Bitcoin)
384406
}
385407

408+
pub fn supports_chain(&self, chain: ChainHash) -> bool {
409+
self.chains().contains(&chain)
410+
}
411+
412+
pub fn amount(&self) -> Option<&Amount> {
413+
self.amount.as_ref()
414+
}
415+
386416
pub fn amount_msats(&self) -> u64 {
387-
self.amount.as_ref().map(Amount::as_msats).unwrap_or(0)
417+
self.amount().map(Amount::as_msats).unwrap_or(0)
418+
}
419+
420+
pub fn base_invoice_amount_msats(&self, quantity: u64) -> u64 {
421+
self.amount_msats() * quantity
388422
}
389423

390424
pub fn quantity_min(&self) -> u64 {
@@ -396,6 +430,16 @@ impl OfferContents {
396430
self.quantity_min.map_or(1, |_| u64::max_value()))
397431
}
398432

433+
pub fn is_valid_quantity(&self, quantity: u64) -> bool {
434+
self.expects_quantity()
435+
&& quantity >= self.quantity_min()
436+
&& quantity <= self.quantity_max()
437+
}
438+
439+
pub fn expects_quantity(&self) -> bool {
440+
self.quantity_min.is_some() || self.quantity_max.is_some()
441+
}
442+
399443
fn as_tlv_stream(&self) -> OfferTlvStreamRef {
400444
let (currency, amount) = match &self.amount {
401445
None => (None, None),
@@ -621,6 +665,7 @@ mod tests {
621665

622666
assert_eq!(offer.bytes, buffer.as_slice());
623667
assert_eq!(offer.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)]);
668+
assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin)));
624669
assert_eq!(offer.metadata(), None);
625670
assert_eq!(offer.amount(), None);
626671
assert_eq!(offer.description(), "foo");
@@ -664,6 +709,7 @@ mod tests {
664709
.chain(Network::Bitcoin)
665710
.build()
666711
.unwrap();
712+
assert!(offer.supports_chain(chain));
667713
assert_eq!(offer.chains(), vec![chain]);
668714
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![chain]));
669715

@@ -672,6 +718,7 @@ mod tests {
672718
.chain(Network::Bitcoin)
673719
.build()
674720
.unwrap();
721+
assert!(offer.supports_chain(chain));
675722
assert_eq!(offer.chains(), vec![chain]);
676723
assert_eq!(offer.as_tlv_stream().chains, Some(&vec![chain]));
677724

@@ -680,6 +727,8 @@ mod tests {
680727
.chain(Network::Testnet)
681728
.build()
682729
.unwrap();
730+
assert!(offer.supports_chain(chains[0]));
731+
assert!(offer.supports_chain(chains[1]));
683732
assert_eq!(offer.chains(), chains);
684733
assert_eq!(offer.as_tlv_stream().chains, Some(&chains));
685734
}

lightning/src/offers/parse.rs

Lines changed: 23 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::fmt;
1516
use ln::msgs::DecodeError;
1617
use util::ser::Readable;
@@ -70,24 +71,40 @@ pub enum ParseError {
7071
Decode(DecodeError),
7172
/// The parsed message has invalid semantics.
7273
InvalidSemantics(SemanticError),
74+
/// The parsed message has an invalid signature.
75+
InvalidSignature(secp256k1::Error),
7376
}
7477

7578
/// Error when interpreting a TLV stream as a specific type.
7679
#[derive(Debug, PartialEq)]
7780
pub enum SemanticError {
81+
/// The provided chain hash does not correspond to a supported chain.
82+
UnsupportedChain,
7883
/// An amount was expected but was missing.
7984
MissingAmount,
8085
/// An amount exceeded the maximum number of bitcoin.
8186
InvalidAmount,
87+
/// An amount was provided but was not sufficient in value.
88+
InsufficientAmount,
89+
/// A currency was provided that is not supported.
90+
UnsupportedCurrency,
91+
/// A feature was required but is unknown.
92+
UnknownRequiredFeatures,
8293
/// A required description was not provided.
8394
MissingDescription,
8495
/// A node id was not provided.
8596
MissingNodeId,
8697
/// An empty set of blinded paths was provided.
8798
MissingPaths,
99+
/// A quantity was not provided.
100+
MissingQuantity,
88101
/// A quantity constituting part of an empty range or lying outside of a valid range was
89102
/// provided.
90103
InvalidQuantity,
104+
/// A quantity or quantity bounds was provided but was not expected.
105+
UnexpectedQuantity,
106+
/// A payer id was expected but was missing.
107+
MissingPayerId,
91108
}
92109

93110
impl From<bech32::Error> for ParseError {
@@ -107,3 +124,9 @@ impl From<SemanticError> for ParseError {
107124
Self::InvalidSemantics(error)
108125
}
109126
}
127+
128+
impl From<secp256k1::Error> for ParseError {
129+
fn from(error: secp256k1::Error) -> Self {
130+
Self::InvalidSignature(error)
131+
}
132+
}

lightning/src/offers/payer.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ use prelude::*;
1717
/// [`InvoiceRequestContents::payer_id`]: invoice_request::InvoiceRequestContents::payer_id
1818
#[derive(Clone, Debug)]
1919
pub(crate) struct PayerContents(pub Option<Vec<u8>>);
20+
21+
tlv_stream!(PayerTlvStream, PayerTlvStreamRef, {
22+
(0, metadata: Vec<u8>),
23+
});

lightning/src/util/ser.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,24 @@ impl<A: Writeable, B: Writeable, C: Writeable> Writeable for (A, B, C) {
10081008
}
10091009
}
10101010

1011+
impl<A: Readable, B: Readable, C: Readable, D: Readable> Readable for (A, B, C, D) {
1012+
fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> {
1013+
let a: A = Readable::read(r)?;
1014+
let b: B = Readable::read(r)?;
1015+
let c: C = Readable::read(r)?;
1016+
let d: D = Readable::read(r)?;
1017+
Ok((a, b, c, d))
1018+
}
1019+
}
1020+
impl<A: Writeable, B: Writeable, C: Writeable, D: Writeable> Writeable for (A, B, C, D) {
1021+
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
1022+
self.0.write(w)?;
1023+
self.1.write(w)?;
1024+
self.2.write(w)?;
1025+
self.3.write(w)
1026+
}
1027+
}
1028+
10111029
impl Writeable for () {
10121030
fn write<W: Writer>(&self, _: &mut W) -> Result<(), io::Error> {
10131031
Ok(())

lightning/src/util/ser_macros.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ macro_rules! tlv_stream {
464464
#[derive(Debug)]
465465
pub(crate) struct $name {
466466
$(
467-
$field: Option<tlv_record_type!($fieldty)>,
467+
pub(crate) $field: Option<tlv_record_type!($fieldty)>,
468468
)*
469469
}
470470

0 commit comments

Comments
 (0)