Skip to content

Commit a9a1442

Browse files
Add Features feature to invoices.
1 parent 12e93da commit a9a1442

File tree

5 files changed

+107
-13
lines changed

5 files changed

+107
-13
lines changed

lightning-invoice/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ readme = "README.md"
1010

1111
[dependencies]
1212
bech32 = "0.7"
13+
lightning = { version = "0.0.13", path = "../lightning" }
1314
secp256k1 = { version = "0.20", features = ["recovery"] }
1415
num-traits = "0.2.8"
1516
bitcoin_hashes = "0.9.4"

lightning-invoice/src/de.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ impl FromBase32 for TaggedField {
431431
Ok(TaggedField::Route(Route::from_base32(field_data)?)),
432432
constants::TAG_PAYMENT_SECRET =>
433433
Ok(TaggedField::PaymentSecret(PaymentSecret::from_base32(field_data)?)),
434+
constants::TAG_FEATURES =>
435+
Ok(TaggedField::Features(InvoiceFeatures::from_base32(field_data)?)),
434436
_ => {
435437
// "A reader MUST skip over unknown fields"
436438
Err(ParseError::Skip)
@@ -977,16 +979,17 @@ mod test {
977979
}
978980

979981
#[test]
980-
fn test_payment_secret_deserialization() {
981-
use bech32::CheckBase32;
982+
fn test_payment_secret_and_features_de_and_ser() {
983+
use lightning::ln::features::InvoiceFeatures;
982984
use secp256k1::recovery::{RecoveryId, RecoverableSignature};
983985
use TaggedField::*;
984-
use {SiPrefix, SignedRawInvoice, Signature, RawInvoice, RawTaggedField, RawHrp, RawDataPart,
986+
use {SiPrefix, SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart,
985987
Currency, Sha256, PositiveTimestamp};
986988

987-
assert_eq!( // BOLT 11 payment secret invoice. The unknown fields are invoice features.
988-
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu".parse(),
989-
Ok(SignedRawInvoice {
989+
// Feature bits 9, 15, and 99 are set.
990+
let expected_features = InvoiceFeatures::from_le_bytes(vec![0, 130, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8]);
991+
let invoice_str = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu";
992+
let invoice = SignedRawInvoice {
990993
raw_invoice: RawInvoice {
991994
hrp: RawHrp {
992995
currency: Currency::Bitcoin,
@@ -1001,10 +1004,7 @@ mod test {
10011004
).unwrap())).into(),
10021005
Description(::Description::new("coffee beans".to_owned()).unwrap()).into(),
10031006
PaymentSecret(::PaymentSecret([17; 32])).into(),
1004-
RawTaggedField::UnknownSemantics(vec![5, 0, 20, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1005-
0, 0, 0, 0, 1, 0, 16,
1006-
0].check_base32().unwrap())],
1007-
}
1007+
Features(expected_features).into()]}
10081008
},
10091009
hash: [0xb1, 0x96, 0x46, 0xc3, 0xbc, 0x56, 0x76, 0x1d, 0x20, 0x65, 0x6e, 0x0e, 0x32,
10101010
0xec, 0xd2, 0x69, 0x27, 0xb7, 0x62, 0x6e, 0x2a, 0x8b, 0xe6, 0x97, 0x71, 0x9f,
@@ -1017,8 +1017,12 @@ mod test {
10171017
0x60, 0x82, 0xea, 0xac, 0x81, 0x39, 0x11, 0xda, 0xe0, 0x1a, 0xf3, 0xc1],
10181018
RecoveryId::from_i32(1).unwrap()
10191019
).unwrap()),
1020-
})
1021-
)
1020+
};
1021+
assert_eq!(invoice_str, invoice.to_string());
1022+
assert_eq!(
1023+
invoice_str.parse(),
1024+
Ok(invoice)
1025+
);
10221026
}
10231027

10241028
#[test]

lightning-invoice/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
1818
extern crate bech32;
1919
extern crate bitcoin_hashes;
20+
extern crate lightning;
2021
extern crate num_traits;
2122
extern crate secp256k1;
2223

2324
use bech32::u5;
2425
use bitcoin_hashes::Hash;
2526
use bitcoin_hashes::sha256;
27+
use lightning::ln::features::InvoiceFeatures;
2628

2729
use secp256k1::key::PublicKey;
2830
use secp256k1::{Message, Secp256k1};
@@ -325,6 +327,7 @@ pub enum TaggedField {
325327
Fallback(Fallback),
326328
Route(Route),
327329
PaymentSecret(PaymentSecret),
330+
Features(InvoiceFeatures),
328331
}
329332

330333
/// SHA-256 hash
@@ -416,6 +419,7 @@ pub mod constants {
416419
pub const TAG_FALLBACK: u8 = 9;
417420
pub const TAG_ROUTE: u8 = 3;
418421
pub const TAG_PAYMENT_SECRET: u8 = 16;
422+
pub const TAG_FEATURES: u8 = 5;
419423
}
420424

421425
impl InvoiceBuilder<tb::False, tb::False, tb::False> {
@@ -506,6 +510,13 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool> InvoiceBuilder<D, H, T> {
506510
}
507511
self
508512
}
513+
514+
/// Adds a features field which indicates the set of supported protocol extensions which the
515+
/// invoice generator supports.
516+
pub fn features(mut self, features: InvoiceFeatures) -> Self {
517+
self.tagged_fields.push(TaggedField::Features(features));
518+
self
519+
}
509520
}
510521

511522
impl<D: tb::Bool, H: tb::Bool> InvoiceBuilder<D, H, tb::True> {
@@ -825,6 +836,10 @@ impl RawInvoice {
825836
find_extract!(self.known_tagged_fields(), TaggedField::PaymentSecret(ref x), x)
826837
}
827838

839+
pub fn features(&self) -> Option<&InvoiceFeatures> {
840+
find_extract!(self.known_tagged_fields(), TaggedField::Features(ref x), x)
841+
}
842+
828843
pub fn fallbacks(&self) -> Vec<&Fallback> {
829844
self.known_tagged_fields().filter_map(|tf| match tf {
830845
&TaggedField::Fallback(ref f) => Some(f),
@@ -1012,6 +1027,11 @@ impl Invoice {
10121027
self.signed_invoice.payment_secret()
10131028
}
10141029

1030+
/// Get the invoice features if they were included in the invoice
1031+
pub fn features(&self) -> Option<&InvoiceFeatures> {
1032+
self.signed_invoice.features()
1033+
}
1034+
10151035
/// Recover the payee's public key (only to be used if none was included in the invoice)
10161036
pub fn recover_payee_pub_key(&self) -> PublicKey {
10171037
self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0
@@ -1069,6 +1089,7 @@ impl TaggedField {
10691089
TaggedField::Fallback(_) => constants::TAG_FALLBACK,
10701090
TaggedField::Route(_) => constants::TAG_ROUTE,
10711091
TaggedField::PaymentSecret(_) => constants::TAG_PAYMENT_SECRET,
1092+
TaggedField::Features(_) => constants::TAG_FEATURES,
10721093
};
10731094

10741095
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
@@ -450,7 +450,9 @@ impl ToBase32 for TaggedField {
450450
TaggedField::PaymentSecret(ref payment_secret) => {
451451
write_tagged_field(writer, constants::TAG_PAYMENT_SECRET, payment_secret)
452452
},
453-
453+
TaggedField::Features(ref features) => {
454+
write_tagged_field(writer, constants::TAG_FEATURES, features)
455+
},
454456
}
455457
}
456458
}

lightning/src/ln/features.rs

Lines changed: 66 additions & 0 deletions
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 be_u5s: Vec<u5> = vec![u5::try_from_u8(0).unwrap(); total_u5s];
408+
for (u8_idx, byte) in self.flags.iter().enumerate() {
409+
for bit_pos in 0..8 {
410+
if 1 << bit_pos & byte != 0 {
411+
let bit_pos_from_left_0_indexed = u8_idx * 8 + (bit_pos);
412+
let new_u5_idx = total_u5s - (bit_pos_from_left_0_indexed as f64 / 5 as f64).floor() as usize - 1;
413+
let new_bit_pos = bit_pos_from_left_0_indexed % 5;
414+
be_u5s[new_u5_idx] = u5::try_from_u8(be_u5s[new_u5_idx].to_u8() | 1 << new_bit_pos).unwrap();
415+
}
416+
}
417+
}
418+
419+
// Trim the starting 0s (they're are the highest feature bits, so if they're unset they should
420+
// be dropped)
421+
while be_u5s.len() > 0 && be_u5s[0].to_u8() == 0 {
422+
be_u5s.remove(0);
423+
}
424+
writer.write(&be_u5s)
425+
}
426+
}
427+
428+
impl Base32Len for InvoiceFeatures {
429+
fn base32_len(&self) -> usize {
430+
self.to_base32().len()
431+
}
432+
}
433+
434+
impl FromBase32 for InvoiceFeatures {
435+
type Err = bech32::Error;
436+
437+
fn from_base32(field_data: &[u5]) -> Result<InvoiceFeatures, bech32::Error> {
438+
// Explanation for the "7": the normal way to round up when dividing is to add the divisor
439+
// minus one before dividing
440+
let total_bytes = (field_data.len() * 5 + 7) / 8 as usize;
441+
let mut le_bytes: Vec<u8> = vec![0; total_bytes];
442+
for (u5_idx, chunk) in field_data.iter().enumerate() {
443+
let chunk_as_u8 = chunk.to_u8();
444+
for bit_pos in 0..5 {
445+
if 1 << bit_pos & chunk_as_u8 != 0 {
446+
let bit_pos_from_right_0_indexed = (field_data.len() - u5_idx - 1) * 5 + bit_pos;
447+
let new_byte_idx = (bit_pos_from_right_0_indexed as f64 / 8 as f64).floor() as usize;
448+
let new_bit_pos = bit_pos_from_right_0_indexed % 8;
449+
le_bytes[new_byte_idx] = le_bytes[new_byte_idx] | 1 << new_bit_pos;
450+
}
451+
}
452+
}
453+
// Trim the tailing 0s (they're the highest feature bits, so if they're unset they should be
454+
// dropped).
455+
let mut len = le_bytes.len();
456+
while len > 0 && le_bytes[len - 1] == 0 {
457+
le_bytes.remove(len - 1);
458+
len = le_bytes.len();
459+
}
460+
Ok(InvoiceFeatures::from_le_bytes(le_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 {

0 commit comments

Comments
 (0)