Skip to content

Invoice features #876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions lightning-invoice/src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,8 @@ impl FromBase32 for TaggedField {
Ok(TaggedField::Route(RouteHint::from_base32(field_data)?)),
constants::TAG_PAYMENT_SECRET =>
Ok(TaggedField::PaymentSecret(PaymentSecret::from_base32(field_data)?)),
constants::TAG_FEATURES =>
Ok(TaggedField::Features(InvoiceFeatures::from_base32(field_data)?)),
_ => {
// "A reader MUST skip over unknown fields"
Err(ParseError::Skip)
Expand Down Expand Up @@ -993,16 +995,17 @@ mod test {
}

#[test]
fn test_payment_secret_deserialization() {
use bech32::CheckBase32;
fn test_payment_secret_and_features_de_and_ser() {
use lightning::ln::features::InvoiceFeatures;
use secp256k1::recovery::{RecoveryId, RecoverableSignature};
use TaggedField::*;
use {SiPrefix, SignedRawInvoice, Signature, RawInvoice, RawTaggedField, RawHrp, RawDataPart,
use {SiPrefix, SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart,
Currency, Sha256, PositiveTimestamp};

assert_eq!( // BOLT 11 payment secret invoice. The unknown fields are invoice features.
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu".parse(),
Ok(SignedRawInvoice {
// Feature bits 9, 15, and 99 are set.
let expected_features = InvoiceFeatures::from_le_bytes(vec![0, 130, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8]);
let invoice_str = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu";
let invoice = SignedRawInvoice {
raw_invoice: RawInvoice {
hrp: RawHrp {
currency: Currency::Bitcoin,
Expand All @@ -1017,10 +1020,7 @@ mod test {
).unwrap())).into(),
Description(::Description::new("coffee beans".to_owned()).unwrap()).into(),
PaymentSecret(::PaymentSecret([17; 32])).into(),
RawTaggedField::UnknownSemantics(vec![5, 0, 20, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 16,
0].check_base32().unwrap())],
}
Features(expected_features).into()]}
},
hash: [0xb1, 0x96, 0x46, 0xc3, 0xbc, 0x56, 0x76, 0x1d, 0x20, 0x65, 0x6e, 0x0e, 0x32,
0xec, 0xd2, 0x69, 0x27, 0xb7, 0x62, 0x6e, 0x2a, 0x8b, 0xe6, 0x97, 0x71, 0x9f,
Expand All @@ -1033,8 +1033,12 @@ mod test {
0x60, 0x82, 0xea, 0xac, 0x81, 0x39, 0x11, 0xda, 0xe0, 0x1a, 0xf3, 0xc1],
RecoveryId::from_i32(1).unwrap()
).unwrap()),
})
)
};
assert_eq!(invoice_str, invoice.to_string());
assert_eq!(
invoice_str.parse(),
Ok(invoice)
);
}

#[test]
Expand Down
28 changes: 24 additions & 4 deletions lightning-invoice/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extern crate secp256k1;
use bech32::u5;
use bitcoin_hashes::Hash;
use bitcoin_hashes::sha256;
use lightning::ln::features::InvoiceFeatures;
#[cfg(any(doc, test))]
use lightning::routing::network_graph::RoutingFees;
use lightning::routing::router::RouteHintHop;
Expand Down Expand Up @@ -329,6 +330,7 @@ pub enum TaggedField {
Fallback(Fallback),
Route(RouteHint),
PaymentSecret(PaymentSecret),
Features(InvoiceFeatures),
}

/// SHA-256 hash
Expand Down Expand Up @@ -401,6 +403,7 @@ pub mod constants {
pub const TAG_FALLBACK: u8 = 9;
pub const TAG_ROUTE: u8 = 3;
pub const TAG_PAYMENT_SECRET: u8 = 16;
pub const TAG_FEATURES: u8 = 5;
}

impl InvoiceBuilder<tb::False, tb::False, tb::False> {
Expand Down Expand Up @@ -491,6 +494,13 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool> InvoiceBuilder<D, H, T> {
}
self
}

/// Adds a features field which indicates the set of supported protocol extensions which the
/// origin node supports.
pub fn features(mut self, features: InvoiceFeatures) -> Self {
self.tagged_fields.push(TaggedField::Features(features));
self
}
}

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

pub fn features(&self) -> Option<&InvoiceFeatures> {
find_extract!(self.known_tagged_fields(), TaggedField::Features(ref x), x)
}

pub fn fallbacks(&self) -> Vec<&Fallback> {
self.known_tagged_fields().filter_map(|tf| match tf {
&TaggedField::Fallback(ref f) => Some(f),
Expand Down Expand Up @@ -992,10 +1006,15 @@ impl Invoice {
self.signed_invoice.payee_pub_key().map(|x| &x.0)
}

/// Get the payment secret if one was included in the invoice
pub fn payment_secret(&self) -> Option<&PaymentSecret> {
self.signed_invoice.payment_secret()
}
/// Get the payment secret if one was included in the invoice
pub fn payment_secret(&self) -> Option<&PaymentSecret> {
self.signed_invoice.payment_secret()
}

/// Get the invoice features if they were included in the invoice
pub fn features(&self) -> Option<&InvoiceFeatures> {
self.signed_invoice.features()
}

/// Recover the payee's public key (only to be used if none was included in the invoice)
pub fn recover_payee_pub_key(&self) -> PublicKey {
Expand Down Expand Up @@ -1054,6 +1073,7 @@ impl TaggedField {
TaggedField::Fallback(_) => constants::TAG_FALLBACK,
TaggedField::Route(_) => constants::TAG_ROUTE,
TaggedField::PaymentSecret(_) => constants::TAG_PAYMENT_SECRET,
TaggedField::Features(_) => constants::TAG_FEATURES,
};

u5::try_from_u8(tag).expect("all tags defined are <32")
Expand Down
4 changes: 3 additions & 1 deletion lightning-invoice/src/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,9 @@ impl ToBase32 for TaggedField {
TaggedField::PaymentSecret(ref payment_secret) => {
write_tagged_field(writer, constants::TAG_PAYMENT_SECRET, payment_secret)
},

TaggedField::Features(ref features) => {
write_tagged_field(writer, constants::TAG_FEATURES, features)
},
}
}
}
Expand Down
102 changes: 100 additions & 2 deletions lightning/src/ln/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
use std::{cmp, fmt};
use std::marker::PhantomData;

use bitcoin::bech32;
use bitcoin::bech32::{Base32Len, FromBase32, ToBase32, u5, WriteBase32};
use ln::msgs::DecodeError;
use util::ser::{Readable, Writeable, Writer};

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

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

impl ToBase32 for InvoiceFeatures {
fn write_base32<W: WriteBase32>(&self, writer: &mut W) -> Result<(), <W as WriteBase32>::Err> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some tests for these conversions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is test_payment_secret_and_features_de_and_ser which uses those methods, and invoice fuzzing. Added more unit tests though.

// Explanation for the "4": the normal way to round up when dividing is to add the divisor
// minus one before dividing
let length_u5s = (self.flags.len() * 8 + 4) / 5 as usize;
let mut res_u5s: Vec<u5> = vec![u5::try_from_u8(0).unwrap(); length_u5s];
for (byte_idx, byte) in self.flags.iter().enumerate() {
let bit_pos_from_left_0_indexed = byte_idx * 8;
let new_u5_idx = length_u5s - (bit_pos_from_left_0_indexed / 5) as usize - 1;
let new_bit_pos = bit_pos_from_left_0_indexed % 5;
let shifted_chunk_u16 = (*byte as u16) << new_bit_pos;
let curr_u5_as_u8 = res_u5s[new_u5_idx].to_u8();
res_u5s[new_u5_idx] = u5::try_from_u8(curr_u5_as_u8 | ((shifted_chunk_u16 & 0x001f) as u8)).unwrap();
if new_u5_idx > 0 {
let curr_u5_as_u8 = res_u5s[new_u5_idx - 1].to_u8();
res_u5s[new_u5_idx - 1] = u5::try_from_u8(curr_u5_as_u8 | (((shifted_chunk_u16 >> 5) & 0x001f) as u8)).unwrap();
}
if new_u5_idx > 1 {
let curr_u5_as_u8 = res_u5s[new_u5_idx - 2].to_u8();
res_u5s[new_u5_idx - 2] = u5::try_from_u8(curr_u5_as_u8 | (((shifted_chunk_u16 >> 10) & 0x001f) as u8)).unwrap();
}
}
// Trim the highest feature bits.
while !res_u5s.is_empty() && res_u5s[0] == u5::try_from_u8(0).unwrap() {
res_u5s.remove(0);
}
writer.write(&res_u5s)
}
}

impl Base32Len for InvoiceFeatures {
fn base32_len(&self) -> usize {
self.to_base32().len()
}
}

impl FromBase32 for InvoiceFeatures {
type Err = bech32::Error;

fn from_base32(field_data: &[u5]) -> Result<InvoiceFeatures, bech32::Error> {
// Explanation for the "7": the normal way to round up when dividing is to add the divisor
// minus one before dividing
let length_bytes = (field_data.len() * 5 + 7) / 8 as usize;
let mut res_bytes: Vec<u8> = vec![0; length_bytes];
for (u5_idx, chunk) in field_data.iter().enumerate() {
let bit_pos_from_right_0_indexed = (field_data.len() - u5_idx - 1) * 5;
let new_byte_idx = (bit_pos_from_right_0_indexed / 8) as usize;
let new_bit_pos = bit_pos_from_right_0_indexed % 8;
let chunk_u16 = chunk.to_u8() as u16;
res_bytes[new_byte_idx] |= ((chunk_u16 << new_bit_pos) & 0xff) as u8;
if new_byte_idx != length_bytes - 1 {
res_bytes[new_byte_idx + 1] |= ((chunk_u16 >> (8-new_bit_pos)) & 0xff) as u8;
}
}
// Trim the highest feature bits.
while !res_bytes.is_empty() && res_bytes[res_bytes.len() - 1] == 0 {
res_bytes.pop();
}
Ok(InvoiceFeatures::from_le_bytes(res_bytes))
}
}

impl<T: sealed::Context> Features<T> {
/// Create a blank Features with no features set
pub fn empty() -> Self {
Expand Down Expand Up @@ -427,8 +493,8 @@ impl<T: sealed::Context> Features<T> {
Features::<C> { flags, mark: PhantomData, }
}

#[cfg(test)]
/// Create a Features given a set of flags, in LE.
/// Create a Features given a set of flags, in little-endian. This is in reverse byte order from
/// most on-the-wire encodings.
pub fn from_le_bytes(flags: Vec<u8>) -> Features<T> {
Features {
flags,
Expand Down Expand Up @@ -628,6 +694,7 @@ impl<T: sealed::Context> Readable for Features<T> {
#[cfg(test)]
mod tests {
use super::{ChannelFeatures, InitFeatures, InvoiceFeatures, NodeFeatures};
use bitcoin::bech32::{Base32Len, FromBase32, ToBase32, u5};

#[test]
fn sanity_test_known_features() {
Expand Down Expand Up @@ -742,4 +809,35 @@ mod tests {
assert!(features.requires_payment_secret());
assert!(features.supports_payment_secret());
}

#[test]
fn invoice_features_encoding() {
let features_as_u5s = vec![
u5::try_from_u8(6).unwrap(),
u5::try_from_u8(10).unwrap(),
u5::try_from_u8(25).unwrap(),
u5::try_from_u8(1).unwrap(),
u5::try_from_u8(10).unwrap(),
u5::try_from_u8(0).unwrap(),
u5::try_from_u8(20).unwrap(),
u5::try_from_u8(2).unwrap(),
u5::try_from_u8(0).unwrap(),
u5::try_from_u8(6).unwrap(),
u5::try_from_u8(0).unwrap(),
u5::try_from_u8(16).unwrap(),
u5::try_from_u8(1).unwrap(),
];
let features = InvoiceFeatures::from_le_bytes(vec![1, 2, 3, 4, 5, 42, 100, 101]);

// Test length calculation.
assert_eq!(features.base32_len(), 13);

// Test serialization.
let features_serialized = features.to_base32();
assert_eq!(features_as_u5s, features_serialized);

// Test deserialization.
let features_deserialized = InvoiceFeatures::from_base32(&features_as_u5s).unwrap();
assert_eq!(features, features_deserialized);
}
}