From: Valentine Wallace Date: Fri, 16 Apr 2021 19:29:33 +0000 (-0400) Subject: Add Features feature to invoices. X-Git-Tag: v0.0.14~16^2 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=b24d02c6a24042d03cd87e6979d754445258f262;p=rust-lightning Add Features feature to invoices. --- diff --git a/lightning-invoice/src/de.rs b/lightning-invoice/src/de.rs index fe77a93a9..c06281e4b 100644 --- a/lightning-invoice/src/de.rs +++ b/lightning-invoice/src/de.rs @@ -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) @@ -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, @@ -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, @@ -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] diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 315cf641c..3d75d4d65 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -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; @@ -329,6 +330,7 @@ pub enum TaggedField { Fallback(Fallback), Route(RouteHint), PaymentSecret(PaymentSecret), + Features(InvoiceFeatures), } /// SHA-256 hash @@ -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 { @@ -491,6 +494,13 @@ impl InvoiceBuilder { } 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 InvoiceBuilder { @@ -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), @@ -997,6 +1011,11 @@ impl Invoice { 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 { self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0 @@ -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") diff --git a/lightning-invoice/src/ser.rs b/lightning-invoice/src/ser.rs index 83888e826..fdff7f4f6 100644 --- a/lightning-invoice/src/ser.rs +++ b/lightning-invoice/src/ser.rs @@ -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) + }, } } } diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index 4a9526aff..36f29ada1 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -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}; @@ -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 { @@ -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 { /// Note that, for convenience, flags is LITTLE endian (despite being big-endian on the wire) flags: Vec, @@ -395,6 +399,68 @@ impl InvoiceFeatures { } } +impl ToBase32 for InvoiceFeatures { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + // 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 = 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 { + // 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 = 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 Features { /// Create a blank Features with no features set pub fn empty() -> Self { @@ -427,7 +493,8 @@ impl Features { Features:: { flags, mark: PhantomData, } } - /// 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) -> Features { Features { flags, @@ -627,6 +694,7 @@ impl Readable for Features { #[cfg(test)] mod tests { use super::{ChannelFeatures, InitFeatures, InvoiceFeatures, NodeFeatures}; + use bitcoin::bech32::{Base32Len, FromBase32, ToBase32, u5}; #[test] fn sanity_test_known_features() { @@ -741,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); + } }