Skip to content

Commit f281ddd

Browse files
Add Features feature to invoices.
1 parent f52b617 commit f281ddd

File tree

4 files changed

+139
-14
lines changed

4 files changed

+139
-14
lines changed

lightning-invoice/src/de.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,8 @@ impl FromBase32 for TaggedField {
433433
Ok(TaggedField::Route(RouteHint::from_base32(field_data)?)),
434434
constants::TAG_PAYMENT_SECRET =>
435435
Ok(TaggedField::PaymentSecret(PaymentSecret::from_base32(field_data)?)),
436+
constants::TAG_FEATURES =>
437+
Ok(TaggedField::Features(InvoiceFeatures::from_base32(field_data)?)),
436438
_ => {
437439
// "A reader MUST skip over unknown fields"
438440
Err(ParseError::Skip)
@@ -993,16 +995,17 @@ mod test {
993995
}
994996

995997
#[test]
996-
fn test_payment_secret_deserialization() {
997-
use bech32::CheckBase32;
998+
fn test_payment_secret_and_features_de_and_ser() {
999+
use lightning::ln::features::InvoiceFeatures;
9981000
use secp256k1::recovery::{RecoveryId, RecoverableSignature};
9991001
use TaggedField::*;
1000-
use {SiPrefix, SignedRawInvoice, Signature, RawInvoice, RawTaggedField, RawHrp, RawDataPart,
1002+
use {SiPrefix, SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart,
10011003
Currency, Sha256, PositiveTimestamp};
10021004

1003-
assert_eq!( // BOLT 11 payment secret invoice. The unknown fields are invoice features.
1004-
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu".parse(),
1005-
Ok(SignedRawInvoice {
1005+
// Feature bits 9, 15, and 99 are set.
1006+
let expected_features = InvoiceFeatures::from_le_bytes(vec![0, 130, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8]);
1007+
let invoice_str = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu";
1008+
let invoice = SignedRawInvoice {
10061009
raw_invoice: RawInvoice {
10071010
hrp: RawHrp {
10081011
currency: Currency::Bitcoin,
@@ -1017,10 +1020,7 @@ mod test {
10171020
).unwrap())).into(),
10181021
Description(::Description::new("coffee beans".to_owned()).unwrap()).into(),
10191022
PaymentSecret(::PaymentSecret([17; 32])).into(),
1020-
RawTaggedField::UnknownSemantics(vec![5, 0, 20, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1021-
0, 0, 0, 0, 1, 0, 16,
1022-
0].check_base32().unwrap())],
1023-
}
1023+
Features(expected_features).into()]}
10241024
},
10251025
hash: [0xb1, 0x96, 0x46, 0xc3, 0xbc, 0x56, 0x76, 0x1d, 0x20, 0x65, 0x6e, 0x0e, 0x32,
10261026
0xec, 0xd2, 0x69, 0x27, 0xb7, 0x62, 0x6e, 0x2a, 0x8b, 0xe6, 0x97, 0x71, 0x9f,
@@ -1033,8 +1033,12 @@ mod test {
10331033
0x60, 0x82, 0xea, 0xac, 0x81, 0x39, 0x11, 0xda, 0xe0, 0x1a, 0xf3, 0xc1],
10341034
RecoveryId::from_i32(1).unwrap()
10351035
).unwrap()),
1036-
})
1037-
)
1036+
};
1037+
assert_eq!(invoice_str, invoice.to_string());
1038+
assert_eq!(
1039+
invoice_str.parse(),
1040+
Ok(invoice)
1041+
);
10381042
}
10391043

10401044
#[test]

lightning-invoice/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ extern crate secp256k1;
2424
use bech32::u5;
2525
use bitcoin_hashes::Hash;
2626
use bitcoin_hashes::sha256;
27+
use lightning::ln::features::InvoiceFeatures;
2728
#[cfg(any(doc, test))]
2829
use lightning::routing::network_graph::RoutingFees;
2930
use lightning::routing::router::RouteHintHop;
@@ -329,6 +330,7 @@ pub enum TaggedField {
329330
Fallback(Fallback),
330331
Route(RouteHint),
331332
PaymentSecret(PaymentSecret),
333+
Features(InvoiceFeatures),
332334
}
333335

334336
/// SHA-256 hash
@@ -401,6 +403,7 @@ pub mod constants {
401403
pub const TAG_FALLBACK: u8 = 9;
402404
pub const TAG_ROUTE: u8 = 3;
403405
pub const TAG_PAYMENT_SECRET: u8 = 16;
406+
pub const TAG_FEATURES: u8 = 5;
404407
}
405408

406409
impl InvoiceBuilder<tb::False, tb::False, tb::False> {
@@ -491,6 +494,13 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool> InvoiceBuilder<D, H, T> {
491494
}
492495
self
493496
}
497+
498+
/// Adds a features field which indicates the set of supported protocol extensions which the
499+
/// invoice generator supports.
500+
pub fn features(mut self, features: InvoiceFeatures) -> Self {
501+
self.tagged_fields.push(TaggedField::Features(features));
502+
self
503+
}
494504
}
495505

496506
impl<D: tb::Bool, H: tb::Bool> InvoiceBuilder<D, H, tb::True> {
@@ -810,6 +820,10 @@ impl RawInvoice {
810820
find_extract!(self.known_tagged_fields(), TaggedField::PaymentSecret(ref x), x)
811821
}
812822

823+
pub fn features(&self) -> Option<&InvoiceFeatures> {
824+
find_extract!(self.known_tagged_fields(), TaggedField::Features(ref x), x)
825+
}
826+
813827
pub fn fallbacks(&self) -> Vec<&Fallback> {
814828
self.known_tagged_fields().filter_map(|tf| match tf {
815829
&TaggedField::Fallback(ref f) => Some(f),
@@ -997,6 +1011,11 @@ impl Invoice {
9971011
self.signed_invoice.payment_secret()
9981012
}
9991013

1014+
/// Get the invoice features if they were included in the invoice
1015+
pub fn features(&self) -> Option<&InvoiceFeatures> {
1016+
self.signed_invoice.features()
1017+
}
1018+
10001019
/// Recover the payee's public key (only to be used if none was included in the invoice)
10011020
pub fn recover_payee_pub_key(&self) -> PublicKey {
10021021
self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0
@@ -1054,6 +1073,7 @@ impl TaggedField {
10541073
TaggedField::Fallback(_) => constants::TAG_FALLBACK,
10551074
TaggedField::Route(_) => constants::TAG_ROUTE,
10561075
TaggedField::PaymentSecret(_) => constants::TAG_PAYMENT_SECRET,
1076+
TaggedField::Features(_) => constants::TAG_FEATURES,
10571077
};
10581078

10591079
u5::try_from_u8(tag).expect("all tags defined are <32")

lightning-invoice/src/ser.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,9 @@ impl ToBase32 for TaggedField {
454454
TaggedField::PaymentSecret(ref payment_secret) => {
455455
write_tagged_field(writer, constants::TAG_PAYMENT_SECRET, payment_secret)
456456
},
457-
457+
TaggedField::Features(ref features) => {
458+
write_tagged_field(writer, constants::TAG_FEATURES, features)
459+
},
458460
}
459461
}
460462
}

lightning/src/ln/features.rs

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
use std::{cmp, fmt};
2626
use std::marker::PhantomData;
2727

28+
use bitcoin::bech32;
29+
use bitcoin::bech32::{Base32Len, FromBase32, ToBase32, u5, WriteBase32};
2830
use ln::msgs::DecodeError;
2931
use util::ser::{Readable, Writeable, Writer};
3032

@@ -51,6 +53,7 @@ mod sealed {
5153
required_features: [$( $( $required_feature: ident )|*, )*],
5254
optional_features: [$( $( $optional_feature: ident )|*, )*],
5355
}) => {
56+
#[derive(Eq, PartialEq)]
5457
pub struct $context {}
5558

5659
impl Context for $context {
@@ -318,6 +321,7 @@ mod sealed {
318321
/// appears.
319322
///
320323
/// (C-not exported) as we map the concrete feature types below directly instead
324+
#[derive(Eq)]
321325
pub struct Features<T: sealed::Context> {
322326
/// Note that, for convenience, flags is LITTLE endian (despite being big-endian on the wire)
323327
flags: Vec<u8>,
@@ -395,6 +399,68 @@ impl InvoiceFeatures {
395399
}
396400
}
397401

402+
impl ToBase32 for InvoiceFeatures {
403+
fn write_base32<W: WriteBase32>(&self, writer: &mut W) -> Result<(), <W as WriteBase32>::Err> {
404+
// Explanation for the "4": the normal way to round up when dividing is to add the divisor
405+
// minus one before dividing
406+
let total_u5s = (self.flags.len() * 8 + 4) / 5 as usize;
407+
let mut res_u5s: Vec<u8> = vec![0; total_u5s];
408+
for (byte_idx, byte) in self.flags.iter().enumerate() {
409+
let bit_pos_from_left_0_indexed = byte_idx * 8;
410+
let new_u5_idx = total_u5s - (bit_pos_from_left_0_indexed / 5) as usize - 1;
411+
let new_bit_pos = bit_pos_from_left_0_indexed % 5;
412+
let shifted_chunk_u16 = (*byte as u16) << new_bit_pos;
413+
res_u5s[new_u5_idx] |= (shifted_chunk_u16 & 0x001f) as u8;
414+
if new_u5_idx > 0 {
415+
res_u5s[new_u5_idx - 1] |= ((shifted_chunk_u16 >> 5) & 0x001f) as u8;
416+
} if new_u5_idx > 1 {
417+
res_u5s[new_u5_idx - 2] |= ((shifted_chunk_u16 >> 10) & 0x001f) as u8;
418+
}
419+
}
420+
// Trim the highest feature bits.
421+
while !res_u5s.is_empty() && res_u5s[0] == 0 {
422+
res_u5s.remove(0);
423+
}
424+
let mut final_u5s = vec![];
425+
for chunk in res_u5s.iter() {
426+
final_u5s.push(u5::try_from_u8(*chunk).unwrap());
427+
}
428+
writer.write(&final_u5s)
429+
}
430+
}
431+
432+
impl Base32Len for InvoiceFeatures {
433+
fn base32_len(&self) -> usize {
434+
self.to_base32().len()
435+
}
436+
}
437+
438+
impl FromBase32 for InvoiceFeatures {
439+
type Err = bech32::Error;
440+
441+
fn from_base32(field_data: &[u5]) -> Result<InvoiceFeatures, bech32::Error> {
442+
// Explanation for the "7": the normal way to round up when dividing is to add the divisor
443+
// minus one before dividing
444+
let total_bytes = (field_data.len() * 5 + 7) / 8 as usize;
445+
let mut res_bytes: Vec<u8> = vec![0; total_bytes];
446+
for (u5_idx, chunk) in field_data.iter().enumerate() {
447+
let bit_pos_from_right_0_indexed = (field_data.len() - u5_idx - 1) * 5;
448+
let new_byte_idx = (bit_pos_from_right_0_indexed / 8) as usize;
449+
let new_bit_pos = bit_pos_from_right_0_indexed % 8;
450+
let chunk_u16 = chunk.to_u8() as u16;
451+
res_bytes[new_byte_idx] |= ((chunk_u16 << new_bit_pos) & 0xff) as u8;
452+
if new_byte_idx != total_bytes - 1 {
453+
res_bytes[new_byte_idx + 1] |= ((chunk_u16 >> (8-new_bit_pos)) & 0xff) as u8;
454+
}
455+
}
456+
// Trim the highest feature bits.
457+
while !res_bytes.is_empty() && res_bytes[res_bytes.len() - 1] == 0 {
458+
res_bytes.pop();
459+
}
460+
Ok(InvoiceFeatures::from_le_bytes(res_bytes))
461+
}
462+
}
463+
398464
impl<T: sealed::Context> Features<T> {
399465
/// Create a blank Features with no features set
400466
pub fn empty() -> Self {
@@ -427,7 +493,8 @@ impl<T: sealed::Context> Features<T> {
427493
Features::<C> { flags, mark: PhantomData, }
428494
}
429495

430-
/// Create a Features given a set of flags, in LE.
496+
/// Create a Features given a set of flags, in little-endian. This is in reverse bit order from
497+
/// the most on-the-wire encodings.
431498
pub fn from_le_bytes(flags: Vec<u8>) -> Features<T> {
432499
Features {
433500
flags,
@@ -627,6 +694,7 @@ impl<T: sealed::Context> Readable for Features<T> {
627694
#[cfg(test)]
628695
mod tests {
629696
use super::{ChannelFeatures, InitFeatures, InvoiceFeatures, NodeFeatures};
697+
use bitcoin::bech32::{Base32Len, FromBase32, ToBase32, u5};
630698

631699
#[test]
632700
fn sanity_test_known_features() {
@@ -741,4 +809,35 @@ mod tests {
741809
assert!(features.requires_payment_secret());
742810
assert!(features.supports_payment_secret());
743811
}
812+
813+
#[test]
814+
fn invoice_features_encoding() {
815+
let features_as_u5s = vec![
816+
u5::try_from_u8(6).unwrap(),
817+
u5::try_from_u8(10).unwrap(),
818+
u5::try_from_u8(25).unwrap(),
819+
u5::try_from_u8(1).unwrap(),
820+
u5::try_from_u8(10).unwrap(),
821+
u5::try_from_u8(0).unwrap(),
822+
u5::try_from_u8(20).unwrap(),
823+
u5::try_from_u8(2).unwrap(),
824+
u5::try_from_u8(0).unwrap(),
825+
u5::try_from_u8(6).unwrap(),
826+
u5::try_from_u8(0).unwrap(),
827+
u5::try_from_u8(16).unwrap(),
828+
u5::try_from_u8(1).unwrap(),
829+
];
830+
let features = InvoiceFeatures::from_le_bytes(vec![1, 2, 3, 4, 5, 42, 100, 101]);
831+
832+
// Test length calculation.
833+
assert_eq!(features.base32_len(), 13);
834+
835+
// Test serialization.
836+
let features_serialized = features.to_base32();
837+
assert_eq!(features_as_u5s, features_serialized);
838+
839+
// Test deserialization.
840+
let features_deserialized = InvoiceFeatures::from_base32(&features_as_u5s).unwrap();
841+
assert_eq!(features, features_deserialized);
842+
}
744843
}

0 commit comments

Comments
 (0)