From 6dea9e56e4948b7c60c07f25c3bc53e6ad358040 Mon Sep 17 00:00:00 2001 From: Sebastian Geisler Date: Wed, 7 Mar 2018 15:57:07 +0100 Subject: [PATCH 1/3] move partial BOLT 11 implementation into rust-lightning --- Cargo.toml | 9 ++ src/lib.rs | 11 ++ src/ln/invoice/mod.rs | 187 ++++++++++++++++++++++++++++++++++ src/ln/invoice/parsers.rs | 205 ++++++++++++++++++++++++++++++++++++++ src/ln/mod.rs | 1 + 5 files changed, 413 insertions(+) create mode 100644 src/ln/invoice/mod.rs create mode 100644 src/ln/invoice/parsers.rs diff --git a/Cargo.toml b/Cargo.toml index ff6451f7316..608df5a4a2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,12 @@ bitcoin = "0.11" rust-crypto = "0.2" rand = "0.4" secp256k1 = "0.8" +bech32 = {git = "https://github.com/sgeisler/rust-bech32"} # https://github.com/clarkmoody/rust-bech32/pull/4 +chrono = "0.4" +regex = "0.2" +bit-vec = "0.4.4" +nom = "3.2.1" +error-chain = "0.11.0" + +[dev-dependencies] +hex = "0.3.1" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c01321776e0..a4be857e2bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,17 @@ extern crate bitcoin; extern crate secp256k1; extern crate rand; extern crate crypto; +extern crate bech32; +extern crate chrono; +extern crate regex; +extern crate bit_vec; +#[macro_use] +extern crate nom; +#[macro_use] +extern crate error_chain; + +#[cfg(test)] +extern crate hex; pub mod chain; pub mod ln; diff --git a/src/ln/invoice/mod.rs b/src/ln/invoice/mod.rs new file mode 100644 index 00000000000..824f1cc0447 --- /dev/null +++ b/src/ln/invoice/mod.rs @@ -0,0 +1,187 @@ +use std::str::FromStr; +use std::num::ParseIntError; + +use bech32; +use bech32::Bech32; + +use chrono::{DateTime, Utc, Duration}; + +use regex::Regex; + +use secp256k1; +use secp256k1::key::PublicKey; +use secp256k1::{Signature, Secp256k1}; + +mod parsers; + +/// An Invoice for a payment on the lightning network as defined in +/// [BOLT #11](https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#examples). + +#[derive(Eq, PartialEq, Debug)] +pub struct Invoice { + /// The currency deferred from the 3rd and 4th character of the bech32 transaction + pub currency: Currency, + + /// The amount to pay in pico-satoshis + pub amount: Option, + + pub timestamp: DateTime, + + /// tagged fields of the payment request + pub tagged: Vec, + + pub signature: Signature, +} + +#[derive(Eq, PartialEq, Debug)] +pub enum Currency { + Bitcoin, + BitcoinTestnet, +} + +#[derive(Eq, PartialEq, Debug)] +pub enum TaggedField { + PaymentHash([u8; 32]), + Description(String), + PayeePubKey(PublicKey), + DescriptionHash([u8; 32]), + ExpiryTime(Duration), + MinFinalCltvExpiry(u64), + Fallback(Fallback), + Route { + pubkey: PublicKey, + short_channel_id: u64, + fee_base_msat: i32, + fee_proportional_millionths: i32, + cltv_expiry_delta: u16, + } +} + +// TODO: better types instead onf byte arrays +#[derive(Eq, PartialEq, Debug)] +pub enum Fallback { + SegWitScript { + version: u8, + script: Vec, + }, + PubKeyHash([u8; 20]), + ScriptHash([u8; 20]), +} + +impl Invoice { + // TODO: maybe rewrite using nom + fn parse_hrp(hrp: &str) -> Result<(Currency, Option)> { + let re = Regex::new(r"^ln([^0-9]*)([0-9]*)([munp]?)$").unwrap(); + let parts = match re.captures(&hrp) { + Some(capture_group) => capture_group, + None => return Err(ErrorKind::MalformedHRP.into()) + }; + + let currency = parts[0].parse::()?; + + let amount = if !parts[1].is_empty() { + Some(parts[1].parse::()?) + } else { + None + }; + + /// `get_multiplier(x)` will only return `None` if `x` is not "m", "u", "n" or "p", which + /// due to the above regex ensures that `get_multiplier(x)` iif `x == ""`, so it's ok to + /// convert a none to 1BTC aka 10^12pBTC. + let multiplier = parts[2].chars().next().and_then(|suffix| { + get_multiplier(&suffix) + }).unwrap_or(1_000_000_000_000); + + Ok((currency, amount.map(|amount| amount * multiplier))) + } +} + +impl FromStr for Invoice { + type Err = Error; + + fn from_str(s: &str) -> Result { + let Bech32 {hrp, data} = s.parse()?; + + let (currency, amount) = Invoice::parse_hrp(&hrp)?; + + Ok(Invoice { + currency, + amount, + timestamp: Utc::now(), + tagged: vec![], + signature: Signature::from_der(&Secp256k1::new(), &[0; 65])?, + }) + } +} + +fn get_multiplier(multiplier: &char) -> Option { + match multiplier { + &'m' => Some(1_000_000_000), + &'u' => Some(1_000_000), + &'n' => Some(1_000), + &'p' => Some(1), + _ => None + } +} + +impl Currency { + pub fn get_currency_prefix(&self) -> &'static str { + match self { + &Currency::Bitcoin => "bc", + &Currency::BitcoinTestnet => "tb", + } + } + + pub fn from_prefix(prefix: &str) -> Result { + match prefix { + "bc" => Ok(Currency::Bitcoin), + "tb" => Ok(Currency::BitcoinTestnet), + _ => Err(ErrorKind::BadCurrencyPrefix.into()) + } + } +} + +impl FromStr for Currency { + type Err = Error; + + fn from_str(s: &str) -> Result { + Currency::from_prefix(s) + } +} + +error_chain! { + foreign_links { + Bech32Error(bech32::Error); + ParseIntError(ParseIntError); + MalformedSignature(secp256k1::Error); + } + + errors { + BadLnPrefix { + description("The invoice did not begin with 'ln'."), + display("The invoice did not begin with 'ln'."), + } + + BadCurrencyPrefix { + description("unsupported currency"), + display("unsupported currency"), + } + + MalformedHRP { + description("malformed human readable part"), + display("malformed human readable part"), + } + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_currency_code() { + use super::Currency; + assert_eq!("bc", Currency::Bitcoin.get_currency_prefix()); + assert_eq!("tb", Currency::BitcoinTestnet.get_currency_prefix()); + } + + // TODO: add more tests once parsers are finished +} \ No newline at end of file diff --git a/src/ln/invoice/parsers.rs b/src/ln/invoice/parsers.rs new file mode 100644 index 00000000000..8ac27a12718 --- /dev/null +++ b/src/ln/invoice/parsers.rs @@ -0,0 +1,205 @@ +use std::fmt::Debug; + +use bit_vec::BitVec; + +use secp256k1::Secp256k1; +use secp256k1::key::PublicKey; + +use super::TaggedField; +use super::TaggedField::*; + +named!(timestamp <&[u8], u32>, fold_many_m_n!(7, 7, take!(1), 0, |acc, place: &[u8]| { + (acc * 32 + (place[0] as u32)) +})); + +named!(data_length <&[u8], u16>, fold_many_m_n!(2, 2, take!(1), 0, |acc, place: &[u8]| { + (acc * 32 + (place[0] as u16)) +})); + +// 0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| +// 0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4| +// TODO: more efficient 5->8 bit/byte conversion (see https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py#L80) +named_args!(parse_bytes(len8: usize, len5: usize) >, map!( + fold_many_m_n!( + len5, + len5, + take!(1), + BitVec::new(), + |mut acc: BitVec, byte: &[u8]| { + let byte = byte[0]; + for bit in (0..5).rev() { + acc.push((byte & (1u8 << bit)) != 0); + } + acc + } + ), + |mut bitvec| { + bitvec.truncate(len8 * 8); + bitvec.to_bytes() + } +)); + +trait ToArray { + fn to_array_32(&self) -> [T; 32]; +} + +impl ToArray for Vec { + /// panics if vec is to small + fn to_array_32(&self) -> [T; 32] { + let mut array = [T::default(); 32]; + for pos in 0..array.len() { + array[pos] = self[pos]; + } + array + } +} + +named!(payment_hash <&[u8], TaggedField>, + do_parse!( + tag!(&[1u8]) >> + verify!(data_length, |len: u16| {len == 52}) >> + hash: call!(parse_bytes, 32, 52) >> + (PaymentHash(hash.to_array_32())) + ) +); + +named!(description <&[u8], TaggedField>, + do_parse!( + tag!(&[13_u8]) >> + data_length: data_length >> + text: map_res!(call!(parse_bytes, (data_length * 5 / 8) as usize, data_length as usize), |bytes| { + String::from_utf8(bytes) + }) >> + (Description(text)) + ) +); + +named!(payee_public_key <&[u8], TaggedField>, map_res!( + do_parse!( + tag!(&[19_u8]) >> + verify!(data_length, |len: u16| {len == 53}) >> + key: call!(parse_bytes, 33, 53) >> + (key) + ), |key: Vec| { + PublicKey::from_slice(&Secp256k1::without_caps(), &key).map(|key| { + PayeePubKey(key) + }) + } +)); + +named!(description_hash <&[u8], TaggedField>, + do_parse!( + tag!(&[23u8]) >> + verify!(data_length, |len: u16| {len == 52}) >> + hash: call!(parse_bytes, 32, 52) >> + (DescriptionHash(hash.to_array_32())) + ) +); + +#[cfg(test)] +mod test { + // Reverse character set. Maps ASCII byte -> CHARSET index on [0,31] + // Copied from rust-bech32 + const CHARSET_REV: [i8; 128] = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 + ]; + + #[test] + // parsing example timestamp "pvjluez" = 1496314658 from BOLT 11 + fn test_timestamp_parser() { + use super::timestamp; + use nom::IResult::Done; + + let bytes = "pvjluez".bytes().map( + |c| CHARSET_REV[c as usize] as u8 + ).collect::>(); + assert_eq!(timestamp(&bytes), Done(&[][..], 1496314658)); + } + + #[test] + fn test_payment_hash_parser() { + use super::TaggedField::PaymentHash; + use super::payment_hash; + use nom::IResult::Done; + use hex::decode; + use super::ToArray; + + let bytes = "pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq".bytes().map( + |c| CHARSET_REV[c as usize] as u8 + ).collect::>(); + + + let expected = PaymentHash( + decode("0001020304050607080900010203040506070809000102030405060708090102") + .unwrap() + .to_array_32() + ); + assert_eq!(payment_hash(&bytes), Done(&[][..], expected)); + } + + #[test] + fn test_description_parser() { + use super::TaggedField::Description; + use super::description; + use nom::IResult::Done; + + let bytes = "dq5xysxxatsyp3k7enxv4js".bytes().map( + |c| CHARSET_REV[c as usize] as u8 + ).collect::>(); + + assert_eq!(description(&bytes), Done(&[][..], Description("1 cup coffee".into()))); + } + + #[test] + fn test_payee_public_key_parser() { + use super::TaggedField::PayeePubKey; + use super::payee_public_key; + use nom::IResult::Done; + use hex::decode; + use secp256k1::key::PublicKey; + use secp256k1::Secp256k1; + + let bytes = "np4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66".bytes().map( + |c| CHARSET_REV[c as usize] as u8 + ).collect::>(); + + let expected = PublicKey::from_slice( + &Secp256k1::without_caps(), + &decode("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad").unwrap() + ).unwrap(); + + assert_eq!(payee_public_key(&bytes), Done(&[][..], PayeePubKey(expected))); + } + + #[test] + fn test_description_hash_parser() { + use super::TaggedField::DescriptionHash; + use super::description_hash; + use nom::IResult::Done; + use hex::decode; + use super::ToArray; + + let bytes = "hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs".bytes().map( + |c| CHARSET_REV[c as usize] as u8 + ).collect::>(); + + + // 3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1 = sha256( + // "One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, \ + // one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, \ + // and one slice of watermelon") + let expected = DescriptionHash( + decode("3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1") + .unwrap() + .to_array_32() + ); + assert_eq!(description_hash(&bytes), Done(&[][..], expected)); + } +} \ No newline at end of file diff --git a/src/ln/mod.rs b/src/ln/mod.rs index 1f5fa460afb..0e3919231f0 100644 --- a/src/ln/mod.rs +++ b/src/ln/mod.rs @@ -5,5 +5,6 @@ pub mod msgs; pub mod router; pub mod peer_channel_encryptor; pub mod peer_handler; +pub mod invoice; mod chan_utils; From 95b4470da1e7b83a9b956aa9e1b053abed287d65 Mon Sep 17 00:00:00 2001 From: Sebastian Geisler Date: Thu, 8 Mar 2018 13:29:08 +0100 Subject: [PATCH 2/3] implement expiry time parser --- src/ln/invoice/parsers.rs | 42 +++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/ln/invoice/parsers.rs b/src/ln/invoice/parsers.rs index 8ac27a12718..2eb8937d8d5 100644 --- a/src/ln/invoice/parsers.rs +++ b/src/ln/invoice/parsers.rs @@ -8,14 +8,16 @@ use secp256k1::key::PublicKey; use super::TaggedField; use super::TaggedField::*; -named!(timestamp <&[u8], u32>, fold_many_m_n!(7, 7, take!(1), 0, |acc, place: &[u8]| { - (acc * 32 + (place[0] as u32)) -})); +use chrono::Duration; -named!(data_length <&[u8], u16>, fold_many_m_n!(2, 2, take!(1), 0, |acc, place: &[u8]| { - (acc * 32 + (place[0] as u16)) +named_args!(parse_u64(len: usize) , fold_many_m_n!(len, len, take!(1), 0u64, |acc, place: &[u8]| { + (acc * 32 + (place[0] as u64)) })); +named!(timestamp <&[u8], u32>, map!(call!(parse_u64, 7), |x| x as u32)); + +named!(data_length <&[u8], usize>, map!(call!(parse_u64, 2), |x| x as usize)); + // 0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7| // 0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4|0 1 2 3 4| // TODO: more efficient 5->8 bit/byte conversion (see https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py#L80) @@ -57,7 +59,7 @@ impl ToArray for Vec { named!(payment_hash <&[u8], TaggedField>, do_parse!( tag!(&[1u8]) >> - verify!(data_length, |len: u16| {len == 52}) >> + verify!(data_length, |len: usize| {len == 52}) >> hash: call!(parse_bytes, 32, 52) >> (PaymentHash(hash.to_array_32())) ) @@ -77,7 +79,7 @@ named!(description <&[u8], TaggedField>, named!(payee_public_key <&[u8], TaggedField>, map_res!( do_parse!( tag!(&[19_u8]) >> - verify!(data_length, |len: u16| {len == 53}) >> + verify!(data_length, |len: usize| {len == 53}) >> key: call!(parse_bytes, 33, 53) >> (key) ), |key: Vec| { @@ -90,12 +92,21 @@ named!(payee_public_key <&[u8], TaggedField>, map_res!( named!(description_hash <&[u8], TaggedField>, do_parse!( tag!(&[23u8]) >> - verify!(data_length, |len: u16| {len == 52}) >> + verify!(data_length, |len: usize| {len == 52}) >> hash: call!(parse_bytes, 32, 52) >> (DescriptionHash(hash.to_array_32())) ) ); +named!(expiry_time <&[u8], TaggedField>, + do_parse!( + tag!(&[6u8]) >> + data_length: data_length >> + expiry_time_seconds: call!(parse_u64, data_length) >> + (ExpiryTime(Duration::seconds(expiry_time_seconds as i64))) + ) +); + #[cfg(test)] mod test { // Reverse character set. Maps ASCII byte -> CHARSET index on [0,31] @@ -202,4 +213,19 @@ mod test { ); assert_eq!(description_hash(&bytes), Done(&[][..], expected)); } + + #[test] + fn test_expiry_time_parser() { + use super::TaggedField::ExpiryTime; + use chrono::Duration; + use super::expiry_time; + use nom::IResult::Done; + + let bytes = "xqzpu".bytes().map( + |c| CHARSET_REV[c as usize] as u8 + ).collect::>(); + + let expected = ExpiryTime(Duration::seconds(60)); + assert_eq!(expiry_time(&bytes), Done(&[][..], expected)); + } } \ No newline at end of file From 735f7158aa9ca641df51616a7f12050acff83107 Mon Sep 17 00:00:00 2001 From: Sebastian Geisler Date: Thu, 8 Mar 2018 13:58:22 +0100 Subject: [PATCH 3/3] implement min cltv expiry parser --- src/ln/invoice/parsers.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/ln/invoice/parsers.rs b/src/ln/invoice/parsers.rs index 2eb8937d8d5..1774a17eba2 100644 --- a/src/ln/invoice/parsers.rs +++ b/src/ln/invoice/parsers.rs @@ -107,6 +107,15 @@ named!(expiry_time <&[u8], TaggedField>, ) ); +named!(min_final_cltv_expiry <&[u8], TaggedField>, + do_parse!( + tag!(&[24u8]) >> + data_length: data_length >> + mfce: call!(parse_u64, data_length) >> + (MinFinalCltvExpiry(mfce)) + ) +); + #[cfg(test)] mod test { // Reverse character set. Maps ASCII byte -> CHARSET index on [0,31] @@ -228,4 +237,19 @@ mod test { let expected = ExpiryTime(Duration::seconds(60)); assert_eq!(expiry_time(&bytes), Done(&[][..], expected)); } + + #[test] + fn test_min_final_cltv_expiry_parser() { + use super::TaggedField::MinFinalCltvExpiry; + use chrono::Duration; + use super::min_final_cltv_expiry; + use nom::IResult::Done; + + let bytes = "cqzpu".bytes().map( + |c| CHARSET_REV[c as usize] as u8 + ).collect::>(); + + let expected = MinFinalCltvExpiry(60); + assert_eq!(min_final_cltv_expiry(&bytes), Done(&[][..], expected)); + } } \ No newline at end of file