From babfa97d959716539073b03b148378e1b744169d Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:03:55 +0200 Subject: [PATCH 1/3] Struggle with features serialization/deserialization with iterators --- lightning-invoice/src/de.rs | 22 ++++++++++++++++++++++ lightning-invoice/src/ser.rs | 14 ++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lightning-invoice/src/de.rs b/lightning-invoice/src/de.rs index 0650a1117aa..fe62076cf89 100644 --- a/lightning-invoice/src/de.rs +++ b/lightning-invoice/src/de.rs @@ -85,6 +85,27 @@ impl FromBase32 for Bolt11InvoiceFeatures { /// and taking the resulting 8-bit values (right to left), /// with the leading 0's skipped. fn from_base32(field_data: &[Fe32]) -> Result { + // fes_to_bytes() trims, input needs to be padded, find padding size + let input_len = field_data.len(); + let mut padding = 0; + while ((input_len + padding) * 5) % 8 != 0 { + padding += 1; + } + let mut output = field_data + .iter() + .map(|f| Fe32::try_from(f.to_u8().reverse_bits() >> 3).expect("<32")) + .rev() + .chain(core::iter::repeat(Fe32::Q).take(padding)) + .fes_to_bytes() + .map(|b| b.reverse_bits()) + .collect::>(); + // Trim the highest feature bits -<-- COULD NOT DO WITH ITER + while !output.is_empty() && output[output.len() - 1] == 0 { + output.pop(); + } + Ok(Bolt11InvoiceFeatures::from_le_bytes(output)) + + /* // Fe32 conversion cannot be used, because this unpacks from right, right-to-left // Carry bits, 0, 1, 2, 3, or 4 bits let mut carry_bits = 0; @@ -116,6 +137,7 @@ impl FromBase32 for Bolt11InvoiceFeatures { output.pop(); } Ok(Bolt11InvoiceFeatures::from_le_bytes(output)) + */ } } diff --git a/lightning-invoice/src/ser.rs b/lightning-invoice/src/ser.rs index 4000241f557..4bfd2689103 100644 --- a/lightning-invoice/src/ser.rs +++ b/lightning-invoice/src/ser.rs @@ -85,6 +85,19 @@ impl Base32Iterable for Bolt11InvoiceFeatures { /// and taking the resulting 5-bit values in reverse (left-to-right), /// with the leading 0's skipped. fn fe_iter<'s>(&'s self) -> Box + 's> { + Box::new( + self + .le_flags() + .iter() + .map(|b| b.reverse_bits()) + .bytes_to_fes() + .collect::>().into_iter() // <-- COULD NOT DO WITHOUT COLLECT + .rev() + .map(|f| Fe32::try_from(f.to_u8().reverse_bits() >> 3).expect("<32")) + .skip_while(|e| *e == Fe32::Q) + ) + + /* // Fe32 conversion cannot be used, because this packs from right, right-to-left let mut input_iter = self.le_flags().iter(); // Carry bits, 0..7 bits @@ -123,6 +136,7 @@ impl Base32Iterable for Bolt11InvoiceFeatures { } // Take result in reverse order, and skip leading 0s Box::new(output.into_iter().rev().skip_while(|e| *e == Fe32::Q)) + */ } } From 48bd3d77144b9802da8c8d9ae6541f764986c0df Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Fri, 6 Sep 2024 23:49:17 +0200 Subject: [PATCH 2/3] FesPadder and others --- lightning-invoice/src/de.rs | 34 ++++----- lightning-invoice/src/lib.rs | 141 +++++++++++++++++++++++++++++++---- 2 files changed, 141 insertions(+), 34 deletions(-) diff --git a/lightning-invoice/src/de.rs b/lightning-invoice/src/de.rs index fe62076cf89..f0f358f6f14 100644 --- a/lightning-invoice/src/de.rs +++ b/lightning-invoice/src/de.rs @@ -85,21 +85,18 @@ impl FromBase32 for Bolt11InvoiceFeatures { /// and taking the resulting 8-bit values (right to left), /// with the leading 0's skipped. fn from_base32(field_data: &[Fe32]) -> Result { - // fes_to_bytes() trims, input needs to be padded, find padding size - let input_len = field_data.len(); - let mut padding = 0; - while ((input_len + padding) * 5) % 8 != 0 { - padding += 1; - } + use crate::FesPaddable; + let mut output = field_data .iter() .map(|f| Fe32::try_from(f.to_u8().reverse_bits() >> 3).expect("<32")) .rev() - .chain(core::iter::repeat(Fe32::Q).take(padding)) + // fes_to_bytes() trims, input needs to be padded + .pad_fes() .fes_to_bytes() .map(|b| b.reverse_bits()) .collect::>(); - // Trim the highest feature bits -<-- COULD NOT DO WITH ITER + // Trim the highest feature bits <-- COULD NOT DO WITH ITER while !output.is_empty() && output[output.len() - 1] == 0 { output.pop(); } @@ -386,10 +383,8 @@ impl FromStr for SignedRawBolt11Invoice { fn from_str(s: &str) -> Result { let parsed = CheckedHrpstring::new::(s)?; let hrp = parsed.hrp(); - // Access original non-packed 32 byte values (as ascii + conversion) - let data: Vec<_> = parsed.data_part_ascii_no_checksum().iter() - .map(|ch| Fe32::from_char(char::from(*ch)).expect("value should be < 32")) - .collect(); + // Access original non-packed 32 byte values (as Fe32s) + let data: Vec<_> = parsed.fe32_iter::>>().collect(); const SIGNATURE_LEN5: usize = 104; // 32-bit values, 65 bytes if data.len() < SIGNATURE_LEN5 { @@ -398,16 +393,15 @@ impl FromStr for SignedRawBolt11Invoice { let raw_hrp: RawHrp = hrp.to_string().to_lowercase().parse()?; let data_part = RawDataPart::from_base32(&data[..data.len() - SIGNATURE_LEN5])?; + let raw_invoice = RawBolt11Invoice { + hrp: raw_hrp, + data: data_part, + }; + let hash = raw_invoice.signable_hash(); Ok(SignedRawBolt11Invoice { - raw_invoice: RawBolt11Invoice { - hrp: raw_hrp, - data: data_part, - }, - hash: RawBolt11Invoice::hash_from_parts( - hrp.to_string().as_bytes(), - &data[..data.len() - SIGNATURE_LEN5], - ), + raw_invoice, + hash, signature: Bolt11InvoiceSignature::from_base32(&data[data.len() - SIGNATURE_LEN5..])?, }) } diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 0f78aad61c6..456cf7b779e 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -528,6 +528,31 @@ pub enum Fallback { #[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct Bolt11InvoiceSignature(pub RecoverableSignature); +/* +impl Bolt11InvoiceSignature { + fn from_bytes(data: &[u8]) -> Result { + use bitcoin::secp256k1::ecdsa::{RecoveryId, RecoverableSignature}; + + const SIGNATURE_LEN: usize = 65; + if data.len() != SIGNATURE_LEN { + return Err(Bolt11ParseError::InvalidSliceLength( + data.len(), + SIGNATURE_LEN, + "Bolt11InvoiceSignature::from_bytes()".into(), + )); + } + let signature = &data[0..64]; + let recovery_id = RecoveryId::from_i32(data[64] as i32)?; + debug_assert_eq!(recovery_id.to_i32(), 0); + + Ok(Bolt11InvoiceSignature(RecoverableSignature::from_compact( + signature, + recovery_id + )?)) + } +} +*/ + impl PartialOrd for Bolt11InvoiceSignature { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -998,6 +1023,84 @@ macro_rules! find_all_extract { }; } +/// Adaptor to pad a Fe32 iter +// #[derive(Clone, PartialEq, Eq)] +pub struct FesPadder> { + end_reached: bool, + fe32_count: usize, + pad_count: u8, + iter: I, +} + +impl FesPadder +where + I: Iterator, +{ + // type Item = u8; + + fn new(iter: I) -> Self { + Self { + end_reached: false, + fe32_count: 0, + pad_count: 0, + iter, + } + } + + fn pad_count_from_fe32_count(fe32_count: usize) -> u8 { + let remainder = (fe32_count * 5) % 8; + if remainder == 0 { 0 } else if remainder < 3 { 2 } else { 1 } + } + + fn padded_count(fe32_count: usize) -> usize { + fe32_count + Self::pad_count_from_fe32_count(fe32_count) as usize + } +} + +impl Iterator for FesPadder +where + I: Iterator, +{ + type Item = Fe32; + + fn next(&mut self) -> Option { + if let Some(elem) = self.iter.next() { + self.fe32_count += 1; + Some(elem) + } else { + // end reached + if !self.end_reached { + self.end_reached = true; + self.pad_count = Self::pad_count_from_fe32_count(self.fe32_count); + } + if self.pad_count > 0 { + self.pad_count -= 1; + Some(Fe32::Q) + } else { + None + } + } + } + + fn size_hint(&self) -> (usize, Option) { + let (fes_min, fes_max) = self.iter.size_hint(); + // +1 because we set last_fe with call to `next`. + let min = Self::padded_count(fes_min + 1); + let max = fes_max.map(|max| Self::padded_count(max)); + (min, max) + } +} + +/// Trait to pad an Fe32 iterator +pub trait FesPaddable: Sized + Iterator { + /// Pad the iterator + fn pad_fes(self) -> FesPadder { + FesPadder::new(self) + } +} + +impl FesPaddable for I where I: Iterator {} + #[allow(missing_docs)] impl RawBolt11Invoice { /// Hash the HRP (as bytes) and signatureless data part (as Fe32 iterator) @@ -1008,35 +1111,45 @@ impl RawBolt11Invoice { /// Hash the HRP as bytes and signatureless data part. fn hash_from_parts(hrp_bytes: &[u8], data_without_signature: &[Fe32]) -> [u8; 32] { - use crate::de::FromBase32; + use crate::bech32::Fe32IterExt; + let data_part = Vec::from(data_without_signature); let mut preimage = Vec::::from(hrp_bytes); + preimage.extend_from_slice( + &data_part + .iter() + .copied() + // fes_to_bytes() trims, input needs to be padded + .pad_fes() + .fes_to_bytes() + .collect::>() + ); - let mut data_part = Vec::from(data_without_signature); - let overhang = (data_part.len() * 5) % 8; - if overhang > 0 { - // add padding if data does not end at a byte boundary - data_part.push(Fe32::try_from(0).unwrap()); + let mut hash: [u8; 32] = Default::default(); + hash.copy_from_slice(&sha256::Hash::hash(&preimage)[..]); + hash + } - // if overhang is in (1..3) we need to add Fe32(0) padding two times - if overhang < 3 { - data_part.push(Fe32::try_from(0).unwrap()); - } - } + /* + /// Hash the HRP as bytes and signatureless data part. + fn hash_from_parts_u8(hrp_bytes: &[u8], data_without_signature: &[u8]) -> [u8; 32] { + // use crate::de::FromBase32; - preimage.extend_from_slice(&Vec::::from_base32(&data_part) - .expect("No padding error may occur due to appended zero above.")); + let mut preimage = Vec::::from(hrp_bytes); + let data_part = Vec::from(data_without_signature); + preimage.extend(data_part); let mut hash: [u8; 32] = Default::default(); hash.copy_from_slice(&sha256::Hash::hash(&preimage)[..]); hash } + */ /// Calculate the hash of the encoded `RawBolt11Invoice` which should be signed. pub fn signable_hash(&self) -> [u8; 32] { use crate::ser::Base32Iterable; - RawBolt11Invoice::hash_from_parts_iter( + Self::hash_from_parts_iter( self.hrp.to_string().as_bytes(), self.data.fe_iter(), ) From 7136a4f093f4872c79d75a3ee898321774dd6474 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:45:18 +0200 Subject: [PATCH 3/3] Add extra tests for example --- lightning-invoice/src/lib.rs | 34 ++++++++ lightning-invoice/src/test_ser_de.rs | 123 +++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 456cf7b779e..582f51c89e9 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -1130,6 +1130,30 @@ impl RawBolt11Invoice { hash } + /// Hash ... + fn preimage_from_parts_iter<'s>(hrp_bytes: &[u8], data_without_signature_iter: Box + 's>) -> Vec { + let data_part_signature = data_without_signature_iter.collect::>(); + Self::preimage_from_parts(hrp_bytes, &data_part_signature[..]) + } + + /// Hash ... + fn preimage_from_parts(hrp_bytes: &[u8], data_without_signature: &[Fe32]) -> Vec { + use crate::bech32::Fe32IterExt; + + let data_part = Vec::from(data_without_signature); + let mut preimage = Vec::::from(hrp_bytes); + preimage.extend_from_slice( + &data_part + .iter() + .copied() + // fes_to_bytes() trims, input needs to be padded + .pad_fes() + .fes_to_bytes() + .collect::>() + ); + preimage + } + /* /// Hash the HRP as bytes and signatureless data part. fn hash_from_parts_u8(hrp_bytes: &[u8], data_without_signature: &[u8]) -> [u8; 32] { @@ -1155,6 +1179,16 @@ impl RawBolt11Invoice { ) } + /// Calculate the hash of the encoded `RawBolt11Invoice` which should be signed. + pub fn hash_preimage(&self) -> Vec { + use crate::ser::Base32Iterable; + + Self::preimage_from_parts_iter( + self.hrp.to_string().as_bytes(), + self.data.fe_iter(), + ) + } + /// Signs the invoice using the supplied `sign_method`. This function MAY fail with an error of /// type `E`. Since the signature of a [`SignedRawBolt11Invoice`] is not required to be valid there /// are no constraints regarding the validity of the produced signature. diff --git a/lightning-invoice/src/test_ser_de.rs b/lightning-invoice/src/test_ser_de.rs index d27ba1660ac..ee2cff99171 100644 --- a/lightning-invoice/src/test_ser_de.rs +++ b/lightning-invoice/src/test_ser_de.rs @@ -259,3 +259,126 @@ fn private_route() { "qgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqgzqvzq2ps8pqqqqqqpqqqqq9qqqv", ); } + +#[test] +fn test_invoice_hash() { + use crate::{RawBolt11Invoice, RawHrp, RawDataPart, Currency, PositiveTimestamp}; + use crate::TaggedField::*; + + let invoice = RawBolt11Invoice { + hrp: RawHrp { + currency: Currency::Bitcoin, + raw_amount: None, + si_prefix: None, + }, + data: RawDataPart { + timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(), + tagged_fields: vec![ + PaymentHash(crate::Sha256(sha256::Hash::from_str( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap())).into(), + Description(crate::Description::new( + "Please consider supporting this project".to_owned() + ).unwrap()).into(), + ], + }, + }; + + let expected_hash = [ + 0xc3, 0xd4, 0xe8, 0x3f, 0x64, 0x6f, 0xa7, 0x9a, 0x39, 0x3d, 0x75, 0x27, 0x7b, 0x1d, + 0x85, 0x8d, 0xb1, 0xd1, 0xf7, 0xab, 0x71, 0x37, 0xdc, 0xb7, 0x83, 0x5d, 0xb2, 0xec, + 0xd5, 0x18, 0xe1, 0xc9 + ]; + + assert_eq!(invoice.signable_hash(), expected_hash) +} + +#[test] +fn test_invoice_construct() { + use crate::TaggedField::*; + use bitcoin::secp256k1::ecdsa::{RecoveryId, RecoverableSignature}; + use crate::{SignedRawBolt11Invoice, Bolt11InvoiceSignature, RawBolt11Invoice, RawHrp, RawDataPart, Currency, Sha256, + PositiveTimestamp}; + // use bitcoin::bitcoin_hashes::Hash; + + let raw_invoice = RawBolt11Invoice { + hrp: RawHrp { + currency: Currency::Bitcoin, + raw_amount: None, + si_prefix: None, + }, + data: RawDataPart { + timestamp: PositiveTimestamp::from_unix_timestamp(100_000).unwrap(), + tagged_fields: vec ! [ + Description( + crate::Description::new( + "AAAAAABCD".to_owned() + ).unwrap() + ).into(), + PaymentHash(Sha256(*sha256::Hash::from_bytes_mut(&mut [2; 32]))).into(), + ], + }, + }; + let hash_preimage = raw_invoice.hash_preimage(); + let hash = raw_invoice.signable_hash(); + + let inv = SignedRawBolt11Invoice { + raw_invoice, + hash, + signature: Bolt11InvoiceSignature(RecoverableSignature::from_compact( + & [ + 0x38u8, 0xec, 0x68, 0x91, 0x34, 0x5e, 0x20, 0x41, 0x45, 0xbe, 0x8a, + 0x3a, 0x99, 0xde, 0x38, 0xe9, 0x8a, 0x39, 0xd6, 0xa5, 0x69, 0x43, + 0x4e, 0x18, 0x45, 0xc8, 0xaf, 0x72, 0x05, 0xaf, 0xcf, 0xcc, 0x7f, + 0x42, 0x5f, 0xcd, 0x14, 0x63, 0xe9, 0x3c, 0x32, 0x88, 0x1e, 0xad, + 0x0d, 0x6e, 0x35, 0x6d, 0x46, 0x7e, 0xc8, 0xc0, 0x25, 0x53, 0xf9, + 0xaa, 0xb1, 0x5e, 0x57, 0x38, 0xb1, 0x1f, 0x12, 0x7f + ], + RecoveryId::from_i32(0).unwrap() + ).unwrap()), + }; + + assert_eq!( + hash_preimage, + [108, 110, 98, 99, 0, 0, 48, 212, 13, 3, 208, 80, 80, 80, 80, 80, 80, 144, 209, 0, 67, 64, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32] + ); + assert_eq!( + hash, + [179, 238, 131, 75, 134, 121, 79, 11, 95, 166, 105, 177, 151, 125, 17, 5, 63, 122, 194, 196, 216, 130, 118, 180, 188, 246, 83, 232, 147, 210, 29, 10] + ); + assert_eq!( + inv.to_string(), + "lnbc1qqqrp4qdq0g9q5zs2pg9pyx3qpp5qgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqpk8ql4" + ); + + assert_eq!( + >::from_base32( + &"qqqrp4q".to_string().chars().map(|c| Fe32::from_char(c).unwrap()).collect::>()[..] + ) + .unwrap(), + vec![0, 0, 48, 212] + ); + assert_eq!( + >::from_base32( + &"g9q5zs2pg9pyx3q".to_string().chars().map(|c| Fe32::from_char(c).unwrap()).collect::>()[..] + ) + .unwrap(), + vec![65, 65, 65, 65, 65, 65, 66, 67, 68] + ); + assert_eq!( + >::from_base32( + &"qgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpq".to_string().chars().map(|c| Fe32::from_char(c).unwrap()).collect::>()[..] + ) + .unwrap(), + vec![2; 32] + ); + assert_eq!( + >::from_base32( + &"qqqrp4qdq0g9q5zs2pg9pyx3qpp5qgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpq".to_string().chars().map(|c| Fe32::from_char(c).unwrap()).collect::>()[..] + ) + .unwrap(), + vec! + [0, 0, 48, 212, 13, 3, 208, 80, 80, 80, 80, 80, 80, 144, 209, 0, 67, 64, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32] + ); +} +