From: Sebastian Date: Wed, 7 Apr 2021 16:48:01 +0000 (-0400) Subject: Pure import of lightning-invoice crate X-Git-Tag: v0.0.14~31^2~4 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=f00bb10a82c122102f712675e6e4523bb5678e2a;p=rust-lightning Pure import of lightning-invoice crate Original repo: https://github.com/rust-bitcoin/rust-lightning-invoice --- diff --git a/lightning-invoice/.gitignore b/lightning-invoice/.gitignore new file mode 100644 index 00000000..f7981508 --- /dev/null +++ b/lightning-invoice/.gitignore @@ -0,0 +1,3 @@ +target +**/*.rs.bk +Cargo.lock \ No newline at end of file diff --git a/lightning-invoice/.travis-kcov.sh b/lightning-invoice/.travis-kcov.sh new file mode 100644 index 00000000..235f9217 --- /dev/null +++ b/lightning-invoice/.travis-kcov.sh @@ -0,0 +1,14 @@ +shopt -s extglob + +rm -r target +cargo test +mkdir target/kcov target/kcov/unit target/kcov/integration target/kcov/merged +ls target +kcov --verify target/kcov/unit target/debug/lightning_invoice-!(*.d) +kcov --verify target/kcov/integration target/debug/ser_de-!(*.d) +kcov --include-pattern="$(pwd)/src" --merge target/kcov/merged target/kcov/unit target/kcov/integration +find . -type l | xargs -n 1 rm + +git add -f target/kcov +git commit -m "last kcov result" +git push -f https://sgeisler:$GITHUB_TOKEN@github.com/rust-bitcoin/rust-lightning-invoice.git HEAD:gh-pages \ No newline at end of file diff --git a/lightning-invoice/.travis.yml b/lightning-invoice/.travis.yml new file mode 100644 index 00000000..04380506 --- /dev/null +++ b/lightning-invoice/.travis.yml @@ -0,0 +1,31 @@ +language: rust +sudo: required +rust: + - nightly + - beta + - stable +cache: cargo + +jobs: + include: + - rust: 1.29.0 + script: + - cargo generate-lockfile --verbose + - cargo update -p cc --precise "1.0.41" --verbose + - cargo build + - cargo test + - stage: fuzz + before_install: + - sudo apt-get -qq update + - sudo apt-get install -y binutils-dev libunwind8-dev + rust: stable + script: cd fuzz && cargo test --verbose && ./travis-fuzz.sh + - stage: coverage + if: type = cron || type = push + before_install: + - sudo apt-get -qq update + - sudo apt-get install cmake g++ pkg-config jq libcurl4-openssl-dev libelf-dev libdw-dev binutils-dev libiberty-dev + - cargo install -f cargo-kcov + - for i in {0..10}; do echo "retry $i"; (cargo kcov --print-install-kcov-sh | sh) && break; done + rust: stable + script: bash .travis-kcov.sh \ No newline at end of file diff --git a/lightning-invoice/Cargo.toml b/lightning-invoice/Cargo.toml new file mode 100644 index 00000000..e19c7ac3 --- /dev/null +++ b/lightning-invoice/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "lightning-invoice" +description = "Data structures to parse and serialize BOLT11 lightning invoices" +version = "0.4.0" +authors = ["Sebastian Geisler "] +license = "Apache-2.0" +documentation = "https://docs.rs/lightning-invoice/" +repository = "https://github.com/rust-bitcoin/rust-lightning-invoice" +keywords = [ "lightning", "bitcoin", "invoice", "BOLT11" ] +readme = "README.md" + +[dependencies] +bech32 = "0.7" +secp256k1 = { version = "0.20", features = ["recovery"] } +num-traits = "0.2.8" +bitcoin_hashes = "0.9.4" + +[lib] +crate-type = ["cdylib", "rlib"] + diff --git a/lightning-invoice/LICENSE b/lightning-invoice/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/lightning-invoice/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lightning-invoice/README.md b/lightning-invoice/README.md new file mode 100644 index 00000000..9fb85080 --- /dev/null +++ b/lightning-invoice/README.md @@ -0,0 +1,17 @@ +# lightning-invoice +[![Build Status](https://travis-ci.org/rust-bitcoin/rust-lightning-invoice.svg?branch=master)](https://travis-ci.org/rust-bitcoin/rust-lightning-invoice) +[![Coverage Report](https://img.shields.io/badge/dynamic/json.svg?label=Coverage&url=https%3A%2F%2Frust-bitcoin.github.io%2Frust-lightning-invoice%2Ftarget%2Fkcov%2Fmerged%2Fkcov-merged%2Fcoverage.json&query=%24.percent_covered&colorB=blue&suffix=%25)](https://rust-bitcoin.github.io/rust-lightning-invoice/target/kcov/merged/) +[![Crates.io Release](https://img.shields.io/badge/crates.io-v0.4.0-orange.svg?longCache=true)](https://crates.io/crates/lightning-invoice) +[![Docs.rs](https://docs.rs/lightning-invoice/badge.svg)](https://docs.rs/lightning-invoice/) + +This repo provides data structures for BOLT 11 lightning invoices and +functions to parse and serialize these from and to bech32. + +**Please be sure to run the test suite since we need to check assumptions +regarding `SystemTime`'s bounds on your platform. You can also call `check_platform` +on startup or in your test suite to do so.** + +## Contributing +* same coding style standard as [rust-bitcoin/rust-lightning](https://github.com/rust-bitcoin/rust-lightning) +* use tabs and spaces (appropriately) +* no unnecessary dependencies diff --git a/lightning-invoice/fuzz/.gitignore b/lightning-invoice/fuzz/.gitignore new file mode 100644 index 00000000..38a90086 --- /dev/null +++ b/lightning-invoice/fuzz/.gitignore @@ -0,0 +1,2 @@ +target +hfuzz_* diff --git a/lightning-invoice/fuzz/Cargo.toml b/lightning-invoice/fuzz/Cargo.toml new file mode 100644 index 00000000..68a0d4e0 --- /dev/null +++ b/lightning-invoice/fuzz/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "lightning-invoice-fuzz" +version = "0.0.1" +authors = ["Automatically generated"] +publish = false + +[package.metadata] +cargo-fuzz = true + +[features] +afl_fuzz = ["afl"] +honggfuzz_fuzz = ["honggfuzz"] + +[dependencies] +honggfuzz = { version = "0.5", optional = true } +afl = { version = "0.4", optional = true } +lightning-invoice = { path = ".."} +bech32 = "0.7" + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "serde_data_part" +path = "fuzz_targets/serde_data_part.rs" diff --git a/lightning-invoice/fuzz/fuzz_targets/serde_data_part.rs b/lightning-invoice/fuzz/fuzz_targets/serde_data_part.rs new file mode 100644 index 00000000..406f9676 --- /dev/null +++ b/lightning-invoice/fuzz/fuzz_targets/serde_data_part.rs @@ -0,0 +1,69 @@ +extern crate lightning_invoice; +extern crate bech32; + +use lightning_invoice::RawDataPart; +use bech32::{FromBase32, ToBase32, u5}; + +fn do_test(data: &[u8]) { + let bech32 = data.iter().map(|x| u5::try_from_u8(x % 32).unwrap()).collect::>(); + let invoice = match RawDataPart::from_base32(&bech32) { + Ok(invoice) => invoice, + Err(_) => return, + }; + + // Our encoding is not worse than the input + assert!(invoice.to_base32().len() <= bech32.len()); + + // Our serialization is loss-less + assert_eq!( + RawDataPart::from_base32(&invoice.to_base32()).expect("faild parsing out own encoding"), + invoice + ); +} + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + do_test(&data); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + do_test(data); + }); + } +} + +#[cfg(test)] +mod tests { + fn extend_vec_from_hex(hex: &str, out: &mut Vec) { + let mut b = 0; + for (idx, c) in hex.as_bytes().iter().filter(|&&c| c != b'\n').enumerate() { + b <<= 4; + match *c { + b'A'...b'F' => b |= c - b'A' + 10, + b'a'...b'f' => b |= c - b'a' + 10, + b'0'...b'9' => b |= c - b'0', + _ => panic!("Bad hex"), + } + if (idx & 1) == 1 { + out.push(b); + b = 0; + } + } + } + + #[test] + fn duplicate_crash() { + let mut a = Vec::new(); + extend_vec_from_hex("000000", &mut a); + super::do_test(&a); + } +} diff --git a/lightning-invoice/fuzz/travis-fuzz.sh b/lightning-invoice/fuzz/travis-fuzz.sh new file mode 100755 index 00000000..ae85ea91 --- /dev/null +++ b/lightning-invoice/fuzz/travis-fuzz.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e +cargo install --force honggfuzz +for TARGET in fuzz_targets/*; do + FILENAME=$(basename $TARGET) + FILE="${FILENAME%.*}" + if [ -d hfuzz_input/$FILE ]; then + HFUZZ_INPUT_ARGS="-f hfuzz_input/$FILE/input" + fi + HFUZZ_BUILD_ARGS="--features honggfuzz_fuzz" HFUZZ_RUN_ARGS="-N1000000 --exit_upon_crash -v $HFUZZ_INPUT_ARGS" cargo hfuzz run $FILE + + if [ -f hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT ]; then + cat hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT + for CASE in hfuzz_workspace/$FILE/SIG*; do + cat $CASE | xxd -p + done + exit 1 + fi +done diff --git a/lightning-invoice/src/de.rs b/lightning-invoice/src/de.rs new file mode 100644 index 00000000..d6cb9207 --- /dev/null +++ b/lightning-invoice/src/de.rs @@ -0,0 +1,1076 @@ +use std::error; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::num::ParseIntError; +use std::str; +use std::str::FromStr; + +use bech32; +use bech32::{u5, FromBase32}; + +use bitcoin_hashes::Hash; +use bitcoin_hashes::sha256; + +use num_traits::{CheckedAdd, CheckedMul}; + +use secp256k1; +use secp256k1::recovery::{RecoveryId, RecoverableSignature}; +use secp256k1::key::PublicKey; + +use super::*; + +use self::hrp_sm::parse_hrp; + +/// State machine to parse the hrp +mod hrp_sm { + use std::ops::Range; + + #[derive(PartialEq, Eq, Debug)] + enum States { + Start, + ParseL, + ParseN, + ParseCurrencyPrefix, + ParseAmountNumber, + ParseAmountSiPrefix, + } + + impl States { + fn next_state(&self, read_symbol: char) -> Result { + match *self { + States::Start => { + if read_symbol == 'l' { + Ok(States::ParseL) + } else { + Err(super::ParseError::MalformedHRP) + } + } + States::ParseL => { + if read_symbol == 'n' { + Ok(States::ParseN) + } else { + Err(super::ParseError::MalformedHRP) + } + }, + States::ParseN => { + if !read_symbol.is_numeric() { + Ok(States::ParseCurrencyPrefix) + } else { + Ok(States::ParseAmountNumber) + } + }, + States::ParseCurrencyPrefix => { + if !read_symbol.is_numeric() { + Ok(States::ParseCurrencyPrefix) + } else { + Ok(States::ParseAmountNumber) + } + }, + States::ParseAmountNumber => { + if read_symbol.is_numeric() { + Ok(States::ParseAmountNumber) + } else if ['m', 'u', 'n', 'p'].contains(&read_symbol) { + Ok(States::ParseAmountSiPrefix) + } else { + Err(super::ParseError::MalformedHRP) + } + }, + States::ParseAmountSiPrefix => Err(super::ParseError::MalformedHRP), + } + } + + fn is_final(&self) -> bool { + !(*self == States::ParseL || *self == States::ParseN) + } + } + + + struct StateMachine { + state: States, + position: usize, + currency_prefix: Option>, + amount_number: Option>, + amount_si_prefix: Option>, + } + + impl StateMachine { + fn new() -> StateMachine { + StateMachine { + state: States::Start, + position: 0, + currency_prefix: None, + amount_number: None, + amount_si_prefix: None, + } + } + + fn update_range(range: &mut Option>, position: usize) { + let new_range = match *range { + None => Range {start: position, end: position + 1}, + Some(ref r) => Range {start: r.start, end: r.end + 1}, + }; + *range = Some(new_range); + } + + fn step(&mut self, c: char) -> Result<(), super::ParseError> { + let next_state = self.state.next_state(c)?; + match next_state { + States::ParseCurrencyPrefix => { + StateMachine::update_range(&mut self.currency_prefix, self.position) + } + States::ParseAmountNumber => { + StateMachine::update_range(&mut self.amount_number, self.position) + }, + States::ParseAmountSiPrefix => { + StateMachine::update_range(&mut self.amount_si_prefix, self.position) + }, + _ => {} + } + + self.position += 1; + self.state = next_state; + Ok(()) + } + + fn is_final(&self) -> bool { + self.state.is_final() + } + + fn currency_prefix(&self) -> &Option> { + &self.currency_prefix + } + + fn amount_number(&self) -> &Option> { + &self.amount_number + } + + fn amount_si_prefix(&self) -> &Option> { + &self.amount_si_prefix + } + } + + pub fn parse_hrp(input: &str) -> Result<(&str, &str, &str), super::ParseError> { + let mut sm = StateMachine::new(); + for c in input.chars() { + sm.step(c)?; + } + + if !sm.is_final() { + return Err(super::ParseError::MalformedHRP); + } + + let currency = sm.currency_prefix().clone() + .map(|r| &input[r]).unwrap_or(""); + let amount = sm.amount_number().clone() + .map(|r| &input[r]).unwrap_or(""); + let si = sm.amount_si_prefix().clone() + .map(|r| &input[r]).unwrap_or(""); + + Ok((currency, amount, si)) + } +} + + +impl FromStr for super::Currency { + type Err = ParseError; + + fn from_str(currency_prefix: &str) -> Result { + match currency_prefix { + "bc" => Ok(Currency::Bitcoin), + "tb" => Ok(Currency::BitcoinTestnet), + "bcrt" => Ok(Currency::Regtest), + "sb" => Ok(Currency::Simnet), + _ => Err(ParseError::UnknownCurrency) + } + } +} + +impl FromStr for SiPrefix { + type Err = ParseError; + + fn from_str(currency_prefix: &str) -> Result { + use SiPrefix::*; + match currency_prefix { + "m" => Ok(Milli), + "u" => Ok(Micro), + "n" => Ok(Nano), + "p" => Ok(Pico), + _ => Err(ParseError::UnknownSiPrefix) + } + } +} + +/// ``` +/// use lightning_invoice::Invoice; +/// +/// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\ +/// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\ +/// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\ +/// ky03ylcqca784w"; +/// +/// assert!(invoice.parse::().is_ok()); +/// ``` +impl FromStr for Invoice { + type Err = ParseOrSemanticError; + + fn from_str(s: &str) -> Result::Err> { + let signed = s.parse::()?; + Ok(Invoice::from_signed(signed)?) + } +} + +/// ``` +/// use lightning_invoice::*; +/// +/// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\ +/// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\ +/// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\ +/// ky03ylcqca784w"; +/// +/// let parsed_1 = invoice.parse::(); +/// +/// let parsed_2 = match invoice.parse::() { +/// Ok(signed) => match Invoice::from_signed(signed) { +/// Ok(invoice) => Ok(invoice), +/// Err(e) => Err(ParseOrSemanticError::SemanticError(e)), +/// }, +/// Err(e) => Err(ParseOrSemanticError::ParseError(e)), +/// }; +/// +/// assert!(parsed_1.is_ok()); +/// assert_eq!(parsed_1, parsed_2); +/// ``` +impl FromStr for SignedRawInvoice { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let (hrp, data) = bech32::decode(s)?; + + if data.len() < 104 { + return Err(ParseError::TooShortDataPart); + } + + let raw_hrp: RawHrp = hrp.parse()?; + let data_part = RawDataPart::from_base32(&data[..data.len()-104])?; + + Ok(SignedRawInvoice { + raw_invoice: RawInvoice { + hrp: raw_hrp, + data: data_part, + }, + hash: RawInvoice::hash_from_parts( + hrp.as_bytes(), + &data[..data.len()-104] + ), + signature: Signature::from_base32(&data[data.len()-104..])?, + }) + } +} + +impl FromStr for RawHrp { + type Err = ParseError; + + fn from_str(hrp: &str) -> Result::Err> { + let parts = parse_hrp(hrp)?; + + let currency = parts.0.parse::()?; + + let amount = if !parts.1.is_empty() { + Some(parts.1.parse::()?) + } else { + None + }; + + let si_prefix: Option = if parts.2.is_empty() { + None + } else { + let si: SiPrefix = parts.2.parse()?; + if let Some(amt) = amount { + if amt.checked_mul(si.multiplier()).is_none() { + return Err(ParseError::IntegerOverflowError); + } + } + Some(si) + }; + + Ok(RawHrp { + currency: currency, + raw_amount: amount, + si_prefix: si_prefix, + }) + } +} + +impl FromBase32 for RawDataPart { + type Err = ParseError; + + fn from_base32(data: &[u5]) -> Result { + if data.len() < 7 { // timestamp length + return Err(ParseError::TooShortDataPart); + } + + let timestamp = PositiveTimestamp::from_base32(&data[0..7])?; + let tagged = parse_tagged_parts(&data[7..])?; + + Ok(RawDataPart { + timestamp: timestamp, + tagged_fields: tagged, + }) + } +} + +impl FromBase32 for PositiveTimestamp { + type Err = ParseError; + + fn from_base32(b32: &[u5]) -> Result { + if b32.len() != 7 { + return Err(ParseError::InvalidSliceLength("PositiveTimestamp::from_base32()".into())); + } + let timestamp: u64 = parse_int_be(b32, 32) + .expect("7*5bit < 64bit, no overflow possible"); + match PositiveTimestamp::from_unix_timestamp(timestamp) { + Ok(t) => Ok(t), + Err(CreationError::TimestampOutOfBounds) => Err(ParseError::TimestampOverflow), + Err(_) => unreachable!(), + } + } +} + +impl FromBase32 for Signature { + type Err = ParseError; + fn from_base32(signature: &[u5]) -> Result { + if signature.len() != 104 { + return Err(ParseError::InvalidSliceLength("Signature::from_base32()".into())); + } + let recoverable_signature_bytes = Vec::::from_base32(signature)?; + let signature = &recoverable_signature_bytes[0..64]; + let recovery_id = RecoveryId::from_i32(recoverable_signature_bytes[64] as i32)?; + + Ok(Signature(RecoverableSignature::from_compact( + signature, + recovery_id + )?)) + } +} + +fn parse_int_be(digits: &[U], base: T) -> Option + where T: CheckedAdd + CheckedMul + From + Default, + U: Into + Copy +{ + digits.iter().fold(Some(Default::default()), |acc, b| + acc + .and_then(|x| x.checked_mul(&base)) + .and_then(|x| x.checked_add(&(Into::::into(*b)).into())) + ) +} + +fn parse_tagged_parts(data: &[u5]) -> Result, ParseError> { + let mut parts = Vec::::new(); + let mut data = data; + + while !data.is_empty() { + if data.len() < 3 { + return Err(ParseError::UnexpectedEndOfTaggedFields); + } + + // Ignore tag at data[0], it will be handled in the TaggedField parsers and + // parse the length to find the end of the tagged field's data + let len = parse_int_be(&data[1..3], 32).expect("can't overflow"); + let last_element = 3 + len; + + if data.len() < last_element { + return Err(ParseError::UnexpectedEndOfTaggedFields); + } + + // Get the tagged field's data slice + let field = &data[0..last_element]; + + // Set data slice to remaining data + data = &data[last_element..]; + + match TaggedField::from_base32(field) { + Ok(field) => { + parts.push(RawTaggedField::KnownSemantics(field)) + }, + Err(ParseError::Skip) => { + parts.push(RawTaggedField::UnknownSemantics(field.into())) + }, + Err(e) => {return Err(e)} + } + } + Ok(parts) +} + +impl FromBase32 for TaggedField { + type Err = ParseError; + + fn from_base32(field: &[u5]) -> Result { + if field.len() < 3 { + return Err(ParseError::UnexpectedEndOfTaggedFields); + } + + let tag = field[0]; + let field_data = &field[3..]; + + match tag.to_u8() { + constants::TAG_PAYMENT_HASH => + Ok(TaggedField::PaymentHash(Sha256::from_base32(field_data)?)), + constants::TAG_DESCRIPTION => + Ok(TaggedField::Description(Description::from_base32(field_data)?)), + constants::TAG_PAYEE_PUB_KEY => + Ok(TaggedField::PayeePubKey(PayeePubKey::from_base32(field_data)?)), + constants::TAG_DESCRIPTION_HASH => + Ok(TaggedField::DescriptionHash(Sha256::from_base32(field_data)?)), + constants::TAG_EXPIRY_TIME => + Ok(TaggedField::ExpiryTime(ExpiryTime::from_base32(field_data)?)), + constants::TAG_MIN_FINAL_CLTV_EXPIRY => + Ok(TaggedField::MinFinalCltvExpiry(MinFinalCltvExpiry::from_base32(field_data)?)), + constants::TAG_FALLBACK => + Ok(TaggedField::Fallback(Fallback::from_base32(field_data)?)), + constants::TAG_ROUTE => + Ok(TaggedField::Route(Route::from_base32(field_data)?)), + constants::TAG_PAYMENT_SECRET => + Ok(TaggedField::PaymentSecret(PaymentSecret::from_base32(field_data)?)), + _ => { + // "A reader MUST skip over unknown fields" + Err(ParseError::Skip) + } + } + } +} + +impl FromBase32 for Sha256 { + type Err = ParseError; + + fn from_base32(field_data: &[u5]) -> Result { + if field_data.len() != 52 { + // "A reader MUST skip over […] a p, [or] h […] field that does not have data_length 52 […]." + Err(ParseError::Skip) + } else { + Ok(Sha256(sha256::Hash::from_slice(&Vec::::from_base32(field_data)?) + .expect("length was checked before (52 u5 -> 32 u8)"))) + } + } +} + +impl FromBase32 for Description { + type Err = ParseError; + + fn from_base32(field_data: &[u5]) -> Result { + let bytes = Vec::::from_base32(field_data)?; + let description = String::from(str::from_utf8(&bytes)?); + Ok(Description::new(description).expect( + "Max len is 639=floor(1023*5/8) since the len field is only 10bits long" + )) + } +} + +impl FromBase32 for PayeePubKey { + type Err = ParseError; + + fn from_base32(field_data: &[u5]) -> Result { + if field_data.len() != 53 { + // "A reader MUST skip over […] a n […] field that does not have data_length 53 […]." + Err(ParseError::Skip) + } else { + let data_bytes = Vec::::from_base32(field_data)?; + let pub_key = PublicKey::from_slice(&data_bytes)?; + Ok(pub_key.into()) + } + } +} + +impl FromBase32 for PaymentSecret { + type Err = ParseError; + + fn from_base32(field_data: &[u5]) -> Result { + if field_data.len() != 52 { + Err(ParseError::Skip) + } else { + let data_bytes = Vec::::from_base32(field_data)?; + let mut payment_secret = [0; 32]; + payment_secret.copy_from_slice(&data_bytes); + Ok(PaymentSecret(payment_secret)) + } + } +} + +impl FromBase32 for ExpiryTime { + type Err = ParseError; + + fn from_base32(field_data: &[u5]) -> Result { + match parse_int_be::(field_data, 32) + .and_then(|t| ExpiryTime::from_seconds(t).ok()) // ok, since the only error is out of bounds + { + Some(t) => Ok(t), + None => Err(ParseError::IntegerOverflowError), + } + } +} + +impl FromBase32 for MinFinalCltvExpiry { + type Err = ParseError; + + fn from_base32(field_data: &[u5]) -> Result { + let expiry = parse_int_be::(field_data, 32); + if let Some(expiry) = expiry { + Ok(MinFinalCltvExpiry(expiry)) + } else { + Err(ParseError::IntegerOverflowError) + } + } +} + +impl FromBase32 for Fallback { + type Err = ParseError; + + fn from_base32(field_data: &[u5]) -> Result { + if field_data.len() < 1 { + return Err(ParseError::UnexpectedEndOfTaggedFields); + } + + let version = field_data[0]; + let bytes = Vec::::from_base32(&field_data[1..])?; + + match version.to_u8() { + 0..=16 => { + if bytes.len() < 2 || bytes.len() > 40 { + return Err(ParseError::InvalidSegWitProgramLength); + } + + Ok(Fallback::SegWitProgram { + version: version, + program: bytes + }) + }, + 17 => { + if bytes.len() != 20 { + return Err(ParseError::InvalidPubKeyHashLength); + } + //TODO: refactor once const generics are available + let mut pkh = [0u8; 20]; + pkh.copy_from_slice(&bytes); + Ok(Fallback::PubKeyHash(pkh)) + } + 18 => { + if bytes.len() != 20 { + return Err(ParseError::InvalidScriptHashLength); + } + let mut sh = [0u8; 20]; + sh.copy_from_slice(&bytes); + Ok(Fallback::ScriptHash(sh)) + } + _ => Err(ParseError::Skip) + } + } +} + +impl FromBase32 for Route { + type Err = ParseError; + + fn from_base32(field_data: &[u5]) -> Result { + let bytes = Vec::::from_base32(field_data)?; + + if bytes.len() % 51 != 0 { + return Err(ParseError::UnexpectedEndOfTaggedFields); + } + + let mut route_hops = Vec::::new(); + + let mut bytes = bytes.as_slice(); + while !bytes.is_empty() { + let hop_bytes = &bytes[0..51]; + bytes = &bytes[51..]; + + let mut channel_id: [u8; 8] = Default::default(); + channel_id.copy_from_slice(&hop_bytes[33..41]); + + let hop = RouteHop { + pubkey: PublicKey::from_slice(&hop_bytes[0..33])?, + short_channel_id: channel_id, + fee_base_msat: parse_int_be(&hop_bytes[41..45], 256).expect("slice too big?"), + fee_proportional_millionths: parse_int_be(&hop_bytes[45..49], 256).expect("slice too big?"), + cltv_expiry_delta: parse_int_be(&hop_bytes[49..51], 256).expect("slice too big?") + }; + + route_hops.push(hop); + } + + Ok(Route(route_hops)) + } +} + +/// Errors that indicate what is wrong with the invoice. They have some granularity for debug +/// reasons, but should generally result in an "invalid BOLT11 invoice" message for the user. +#[allow(missing_docs)] +#[derive(PartialEq, Debug, Clone)] +pub enum ParseError { + Bech32Error(bech32::Error), + ParseAmountError(ParseIntError), + MalformedSignature(secp256k1::Error), + BadPrefix, + UnknownCurrency, + UnknownSiPrefix, + MalformedHRP, + TooShortDataPart, + UnexpectedEndOfTaggedFields, + DescriptionDecodeError(str::Utf8Error), + PaddingError, + IntegerOverflowError, + InvalidSegWitProgramLength, + InvalidPubKeyHashLength, + InvalidScriptHashLength, + InvalidRecoveryId, + InvalidSliceLength(String), + + /// Not an error, but used internally to signal that a part of the invoice should be ignored + /// according to BOLT11 + Skip, + TimestampOverflow, +} + +/// Indicates that something went wrong while parsing or validating the invoice. Parsing errors +/// should be mostly seen as opaque and are only there for debugging reasons. Semantic errors +/// like wrong signatures, missing fields etc. could mean that someone tampered with the invoice. +#[derive(PartialEq, Debug, Clone)] +pub enum ParseOrSemanticError { + /// The invoice couldn't be decoded + ParseError(ParseError), + + /// The invoice could be decoded but violates the BOLT11 standard + SemanticError(::SemanticError), +} + +impl Display for ParseError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match *self { + // TODO: find a way to combine the first three arms (e as error::Error?) + ParseError::Bech32Error(ref e) => { + write!(f, "Invalid bech32: {}", e) + } + ParseError::ParseAmountError(ref e) => { + write!(f, "Invalid amount in hrp ({})", e) + } + ParseError::MalformedSignature(ref e) => { + write!(f, "Invalid secp256k1 signature: {}", e) + } + ParseError::DescriptionDecodeError(ref e) => { + write!(f, "Description is not a valid utf-8 string: {}", e) + } + ParseError::InvalidSliceLength(ref function) => { + write!(f, "Slice in function {} had the wrong length", function) + } + ParseError::BadPrefix => f.write_str("did not begin with 'ln'"), + ParseError::UnknownCurrency => f.write_str("currency code unknown"), + ParseError::UnknownSiPrefix => f.write_str("unknown SI prefix"), + ParseError::MalformedHRP => f.write_str("malformed human readable part"), + ParseError::TooShortDataPart => { + f.write_str("data part too short (should be at least 111 bech32 chars long)") + }, + ParseError::UnexpectedEndOfTaggedFields => { + f.write_str("tagged fields part ended unexpectedly") + }, + ParseError::PaddingError => f.write_str("some data field had bad padding"), + ParseError::IntegerOverflowError => { + f.write_str("parsed integer doesn't fit into receiving type") + }, + ParseError::InvalidSegWitProgramLength => { + f.write_str("fallback SegWit program is too long or too short") + }, + ParseError::InvalidPubKeyHashLength => { + f.write_str("fallback public key hash has a length unequal 20 bytes") + }, + ParseError::InvalidScriptHashLength => { + f.write_str("fallback script hash has a length unequal 32 bytes") + }, + ParseError::InvalidRecoveryId => { + f.write_str("recovery id is out of range (should be in [0,3])") + }, + ParseError::Skip => { + f.write_str("the tagged field has to be skipped because of an unexpected, but allowed property") + }, + ParseError::TimestampOverflow => { + f.write_str("the invoice's timestamp could not be represented as SystemTime") + }, + } + } +} + +impl Display for ParseOrSemanticError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ParseOrSemanticError::ParseError(err) => err.fmt(f), + ParseOrSemanticError::SemanticError(err) => err.fmt(f), + } + } +} + +impl error::Error for ParseError {} + +impl error::Error for ParseOrSemanticError {} + +macro_rules! from_error { + ($my_error:expr, $extern_error:ty) => { + impl From<$extern_error> for ParseError { + fn from(e: $extern_error) -> Self { + $my_error(e) + } + } + } +} + +from_error!(ParseError::MalformedSignature, secp256k1::Error); +from_error!(ParseError::ParseAmountError, ParseIntError); +from_error!(ParseError::DescriptionDecodeError, str::Utf8Error); + +impl From for ParseError { + fn from(e: bech32::Error) -> Self { + match e { + bech32::Error::InvalidPadding => ParseError::PaddingError, + _ => ParseError::Bech32Error(e) + } + } +} + +impl From for ParseOrSemanticError { + fn from(e: ParseError) -> Self { + ParseOrSemanticError::ParseError(e) + } +} + +impl From<::SemanticError> for ParseOrSemanticError { + fn from(e: SemanticError) -> Self { + ParseOrSemanticError::SemanticError(e) + } +} + +#[cfg(test)] +mod test { + use de::ParseError; + use secp256k1::PublicKey; + use bech32::u5; + use bitcoin_hashes::hex::FromHex; + use bitcoin_hashes::sha256; + + 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 + ]; + + fn from_bech32(bytes_5b: &[u8]) -> Vec { + bytes_5b + .iter() + .map(|c| u5::try_from_u8(CHARSET_REV[*c as usize] as u8).unwrap()) + .collect() + } + + #[test] + fn test_parse_currency_prefix() { + use Currency; + + assert_eq!("bc".parse::(), Ok(Currency::Bitcoin)); + assert_eq!("tb".parse::(), Ok(Currency::BitcoinTestnet)); + assert_eq!("bcrt".parse::(), Ok(Currency::Regtest)); + assert_eq!("sb".parse::(), Ok(Currency::Simnet)); + assert_eq!("something_else".parse::(), Err(ParseError::UnknownCurrency)) + } + + #[test] + fn test_parse_int_from_bytes_be() { + use de::parse_int_be; + + assert_eq!(parse_int_be::(&[1, 2, 3, 4], 256), Some(16909060)); + assert_eq!(parse_int_be::(&[1, 3], 32), Some(35)); + assert_eq!(parse_int_be::(&[255, 255, 255, 255], 256), Some(4294967295)); + assert_eq!(parse_int_be::(&[1, 0, 0, 0, 0], 256), None); + } + + #[test] + fn test_parse_sha256_hash() { + use Sha256; + use bech32::FromBase32; + + let input = from_bech32( + "qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq".as_bytes() + ); + + let hash = sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap(); + let expected = Ok(Sha256(hash)); + + assert_eq!(Sha256::from_base32(&input), expected); + + // make sure hashes of unknown length get skipped + let input_unexpected_length = from_bech32( + "qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypyq".as_bytes() + ); + assert_eq!(Sha256::from_base32(&input_unexpected_length), Err(ParseError::Skip)); + } + + #[test] + fn test_parse_description() { + use ::Description; + use bech32::FromBase32; + + let input = from_bech32("xysxxatsyp3k7enxv4js".as_bytes()); + let expected = Ok(Description::new("1 cup coffee".to_owned()).unwrap()); + assert_eq!(Description::from_base32(&input), expected); + } + + #[test] + fn test_parse_payee_pub_key() { + use ::PayeePubKey; + use bech32::FromBase32; + + let input = from_bech32("q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66".as_bytes()); + let pk_bytes = [ + 0x03, 0xe7, 0x15, 0x6a, 0xe3, 0x3b, 0x0a, 0x20, 0x8d, 0x07, 0x44, 0x19, 0x91, 0x63, + 0x17, 0x7e, 0x90, 0x9e, 0x80, 0x17, 0x6e, 0x55, 0xd9, 0x7a, 0x2f, 0x22, 0x1e, 0xde, + 0x0f, 0x93, 0x4d, 0xd9, 0xad + ]; + let expected = Ok(PayeePubKey( + PublicKey::from_slice(&pk_bytes[..]).unwrap() + )); + + assert_eq!(PayeePubKey::from_base32(&input), expected); + + // expects 33 bytes + let input_unexpected_length = from_bech32( + "q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhvq".as_bytes() + ); + assert_eq!(PayeePubKey::from_base32(&input_unexpected_length), Err(ParseError::Skip)); + } + + #[test] + fn test_parse_expiry_time() { + use ::ExpiryTime; + use bech32::FromBase32; + + let input = from_bech32("pu".as_bytes()); + let expected = Ok(ExpiryTime::from_seconds(60).unwrap()); + assert_eq!(ExpiryTime::from_base32(&input), expected); + + let input_too_large = from_bech32("sqqqqqqqqqqqq".as_bytes()); + assert_eq!(ExpiryTime::from_base32(&input_too_large), Err(ParseError::IntegerOverflowError)); + } + + #[test] + fn test_parse_min_final_cltv_expiry() { + use ::MinFinalCltvExpiry; + use bech32::FromBase32; + + let input = from_bech32("pr".as_bytes()); + let expected = Ok(MinFinalCltvExpiry(35)); + + assert_eq!(MinFinalCltvExpiry::from_base32(&input), expected); + } + + #[test] + fn test_parse_fallback() { + use Fallback; + use bech32::FromBase32; + + let cases = vec![ + ( + from_bech32("3x9et2e20v6pu37c5d9vax37wxq72un98".as_bytes()), + Ok(Fallback::PubKeyHash([ + 0x31, 0x72, 0xb5, 0x65, 0x4f, 0x66, 0x83, 0xc8, 0xfb, 0x14, 0x69, 0x59, 0xd3, + 0x47, 0xce, 0x30, 0x3c, 0xae, 0x4c, 0xa7 + ])) + ), + ( + from_bech32("j3a24vwu6r8ejrss3axul8rxldph2q7z9".as_bytes()), + Ok(Fallback::ScriptHash([ + 0x8f, 0x55, 0x56, 0x3b, 0x9a, 0x19, 0xf3, 0x21, 0xc2, 0x11, 0xe9, 0xb9, 0xf3, + 0x8c, 0xdf, 0x68, 0x6e, 0xa0, 0x78, 0x45 + ])) + ), + ( + from_bech32("qw508d6qejxtdg4y5r3zarvary0c5xw7k".as_bytes()), + Ok(Fallback::SegWitProgram { + version: u5::try_from_u8(0).unwrap(), + program: Vec::from(&[ + 0x75u8, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, 0x94, 0x1c, 0x45, + 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6 + ][..]) + }) + ), + ( + vec![u5::try_from_u8(21).unwrap(); 41], + Err(ParseError::Skip) + ), + ( + vec![], + Err(ParseError::UnexpectedEndOfTaggedFields) + ), + ( + vec![u5::try_from_u8(1).unwrap(); 81], + Err(ParseError::InvalidSegWitProgramLength) + ), + ( + vec![u5::try_from_u8(17).unwrap(); 1], + Err(ParseError::InvalidPubKeyHashLength) + ), + ( + vec![u5::try_from_u8(18).unwrap(); 1], + Err(ParseError::InvalidScriptHashLength) + ) + ]; + + for (input, expected) in cases.into_iter() { + assert_eq!(Fallback::from_base32(&input), expected); + } + } + + #[test] + fn test_parse_route() { + use RouteHop; + use ::Route; + use bech32::FromBase32; + + let input = from_bech32( + "q20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqa\ + fqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq".as_bytes() + ); + + let mut expected = Vec::::new(); + expected.push(RouteHop { + pubkey: PublicKey::from_slice( + &[ + 0x02u8, 0x9e, 0x03, 0xa9, 0x01, 0xb8, 0x55, 0x34, 0xff, 0x1e, 0x92, 0xc4, 0x3c, + 0x74, 0x43, 0x1f, 0x7c, 0xe7, 0x20, 0x46, 0x06, 0x0f, 0xcf, 0x7a, 0x95, 0xc3, + 0x7e, 0x14, 0x8f, 0x78, 0xc7, 0x72, 0x55 + ][..] + ).unwrap(), + short_channel_id: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], + fee_base_msat: 1, + fee_proportional_millionths: 20, + cltv_expiry_delta: 3 + }); + expected.push(RouteHop { + pubkey: PublicKey::from_slice( + &[ + 0x03u8, 0x9e, 0x03, 0xa9, 0x01, 0xb8, 0x55, 0x34, 0xff, 0x1e, 0x92, 0xc4, 0x3c, + 0x74, 0x43, 0x1f, 0x7c, 0xe7, 0x20, 0x46, 0x06, 0x0f, 0xcf, 0x7a, 0x95, 0xc3, + 0x7e, 0x14, 0x8f, 0x78, 0xc7, 0x72, 0x55 + ][..] + ).unwrap(), + short_channel_id: [0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a], + fee_base_msat: 2, + fee_proportional_millionths: 30, + cltv_expiry_delta: 4 + }); + + assert_eq!(Route::from_base32(&input), Ok(Route(expected))); + + assert_eq!( + Route::from_base32(&[u5::try_from_u8(0).unwrap(); 40][..]), + Err(ParseError::UnexpectedEndOfTaggedFields) + ); + } + + #[test] + fn test_payment_secret_deserialization() { + use bech32::CheckBase32; + use secp256k1::recovery::{RecoveryId, RecoverableSignature}; + use TaggedField::*; + use {SiPrefix, SignedRawInvoice, Signature, RawInvoice, RawTaggedField, RawHrp, RawDataPart, + Currency, Sha256, PositiveTimestamp}; + + assert_eq!( // BOLT 11 payment secret invoice. The unknown fields are invoice features. + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu".parse(), + Ok(SignedRawInvoice { + raw_invoice: RawInvoice { + hrp: RawHrp { + currency: Currency::Bitcoin, + raw_amount: Some(25), + si_prefix: Some(SiPrefix::Milli) + }, + data: RawDataPart { + timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(), + tagged_fields: vec ! [ + PaymentHash(Sha256(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).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())], + } + }, + 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, + 0xf8, 0x7e, 0x44, 0x54, 0x55, 0xb9], + signature: Signature(RecoverableSignature::from_compact( + &[0xd7, 0x90, 0x4c, 0xc4, 0xb7, 0x4a, 0x22, 0x26, 0x9c, 0x68, 0xc1, 0xdf, 0x68, + 0xa9, 0x6c, 0x21, 0x4d, 0x65, 0x1b, 0x93, 0x76, 0xe9, 0xf1, 0x64, 0xd3, 0x60, + 0x4d, 0xa4, 0xb7, 0xde, 0xcc, 0xce, 0x0e, 0x82, 0xaa, 0xab, 0x4c, 0x85, 0xd3, + 0x58, 0xea, 0x14, 0xd0, 0xae, 0x34, 0x2d, 0xa3, 0x08, 0x12, 0xf9, 0x5d, 0x97, + 0x60, 0x82, 0xea, 0xac, 0x81, 0x39, 0x11, 0xda, 0xe0, 0x1a, 0xf3, 0xc1], + RecoveryId::from_i32(1).unwrap() + ).unwrap()), + }) + ) + } + + #[test] + fn test_raw_signed_invoice_deserialization() { + use TaggedField::*; + use secp256k1::recovery::{RecoveryId, RecoverableSignature}; + use {SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart, Currency, Sha256, + PositiveTimestamp}; + + assert_eq!( + "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmw\ + wd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9\ + ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w".parse(), + Ok(SignedRawInvoice { + raw_invoice: RawInvoice { + hrp: RawHrp { + currency: Currency::Bitcoin, + raw_amount: None, + si_prefix: None, + }, + data: RawDataPart { + timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(), + tagged_fields: vec ! [ + PaymentHash(Sha256(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap())).into(), + Description( + ::Description::new( + "Please consider supporting this project".to_owned() + ).unwrap() + ).into(), + ], + }, + }, + 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 + ], + signature: Signature(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()), + } + ) + ) + } +} diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs new file mode 100644 index 00000000..62e5bf16 --- /dev/null +++ b/lightning-invoice/src/lib.rs @@ -0,0 +1,1581 @@ +#![deny(missing_docs)] +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] + +#![cfg_attr(feature = "strict", deny(warnings))] + +//! This crate provides data structures to represent +//! [lightning BOLT11](https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md) +//! invoices and functions to create, encode and decode these. If you just want to use the standard +//! en-/decoding functionality this should get you started: +//! +//! * For parsing use `str::parse::(&self)` (see the docs of `impl FromStr for Invoice`) +//! * For constructing invoices use the `InvoiceBuilder` +//! * For serializing invoices use the `Display`/`ToString` traits + +extern crate bech32; +extern crate bitcoin_hashes; +extern crate num_traits; +extern crate secp256k1; + +use bech32::u5; +use bitcoin_hashes::Hash; +use bitcoin_hashes::sha256; + +use secp256k1::key::PublicKey; +use secp256k1::{Message, Secp256k1}; +use secp256k1::recovery::RecoverableSignature; +use std::ops::Deref; + +use std::iter::FilterMap; +use std::slice::Iter; +use std::time::{SystemTime, Duration, UNIX_EPOCH}; +use std::fmt::{Display, Formatter, self}; + +mod de; +mod ser; +mod tb; + +pub use de::{ParseError, ParseOrSemanticError}; + +// TODO: fix before 2037 (see rust PR #55527) +/// Defines the maximum UNIX timestamp that can be represented as `SystemTime`. This is checked by +/// one of the unit tests, please run them. +const SYSTEM_TIME_MAX_UNIX_TIMESTAMP: u64 = std::i32::MAX as u64; + +/// Allow the expiry time to be up to one year. Since this reduces the range of possible timestamps +/// it should be rather low as long as we still have to support 32bit time representations +const MAX_EXPIRY_TIME: u64 = 60 * 60 * 24 * 356; + +/// This function is used as a static assert for the size of `SystemTime`. If the crate fails to +/// compile due to it this indicates that your system uses unexpected bounds for `SystemTime`. You +/// can remove this functions and run the test `test_system_time_bounds_assumptions`. In any case, +/// please open an issue. If all tests pass you should be able to use this library safely by just +/// removing this function till we patch it accordingly. +fn __system_time_size_check() { + // Use 2 * sizeof(u64) as expected size since the expected underlying implementation is storing + // a `Duration` since `SystemTime::UNIX_EPOCH`. + unsafe { std::mem::transmute::(UNIX_EPOCH); } +} + + +/// **Call this function on startup to ensure that all assumptions about the platform are valid.** +/// +/// Unfortunately we have to make assumptions about the upper bounds of the `SystemTime` type on +/// your platform which we can't fully verify at compile time and which isn't part of it's contract. +/// To our best knowledge our assumptions hold for all platforms officially supported by rust, but +/// since this check is fast we recommend to do it anyway. +/// +/// If this function fails this is considered a bug. Please open an issue describing your +/// platform and stating your current system time. +/// +/// # Panics +/// If the check fails this function panics. By calling this function on startup you ensure that +/// this wont happen at an arbitrary later point in time. +pub fn check_platform() { + // The upper and lower bounds of `SystemTime` are not part of its public contract and are + // platform specific. That's why we have to test if our assumptions regarding these bounds + // hold on the target platform. + // + // If this test fails on your platform, please don't use the library and open an issue + // instead so we can resolve the situation. Currently this library is tested on: + // * Linux (64bit) + let fail_date = UNIX_EPOCH + Duration::from_secs(SYSTEM_TIME_MAX_UNIX_TIMESTAMP); + let year = Duration::from_secs(60 * 60 * 24 * 365); + + // Make sure that the library will keep working for another year + assert!(fail_date.duration_since(SystemTime::now()).unwrap() > year); + + let max_ts = PositiveTimestamp::from_unix_timestamp( + SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME + ).unwrap(); + let max_exp = ::ExpiryTime::from_seconds(MAX_EXPIRY_TIME).unwrap(); + + assert_eq!( + (*max_ts.as_time() + *max_exp.as_duration()).duration_since(UNIX_EPOCH).unwrap().as_secs(), + SYSTEM_TIME_MAX_UNIX_TIMESTAMP + ); +} + + +/// Builder for `Invoice`s. It's the most convenient and advised way to use this library. It ensures +/// that only a semantically and syntactically correct Invoice can be built using it. +/// +/// ``` +/// extern crate secp256k1; +/// extern crate lightning_invoice; +/// extern crate bitcoin_hashes; +/// +/// use bitcoin_hashes::Hash; +/// use bitcoin_hashes::sha256; +/// +/// use secp256k1::Secp256k1; +/// use secp256k1::key::SecretKey; +/// +/// use lightning_invoice::{Currency, InvoiceBuilder}; +/// +/// # fn main() { +/// let private_key = SecretKey::from_slice( +/// &[ +/// 0xe1, 0x26, 0xf6, 0x8f, 0x7e, 0xaf, 0xcc, 0x8b, 0x74, 0xf5, 0x4d, 0x26, 0x9f, +/// 0xe2, 0x06, 0xbe, 0x71, 0x50, 0x00, 0xf9, 0x4d, 0xac, 0x06, 0x7d, 0x1c, 0x04, +/// 0xa8, 0xca, 0x3b, 0x2d, 0xb7, 0x34 +/// ][..] +/// ).unwrap(); +/// +/// let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).unwrap(); +/// +/// let invoice = InvoiceBuilder::new(Currency::Bitcoin) +/// .description("Coins pls!".into()) +/// .payment_hash(payment_hash) +/// .current_timestamp() +/// .build_signed(|hash| { +/// Secp256k1::new().sign_recoverable(hash, &private_key) +/// }) +/// .unwrap(); +/// +/// assert!(invoice.to_string().starts_with("lnbc1")); +/// # } +/// ``` +/// +/// # Type parameters +/// The two parameters `D` and `H` signal if the builder already contains the correct amount of the +/// given field: +/// * `D`: exactly one `Description` or `DescriptionHash` +/// * `H`: exactly one `PaymentHash` +/// * `T`: the timestamp is set +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct InvoiceBuilder { + currency: Currency, + amount: Option, + si_prefix: Option, + timestamp: Option, + tagged_fields: Vec, + error: Option, + + phantom_d: std::marker::PhantomData, + phantom_h: std::marker::PhantomData, + phantom_t: std::marker::PhantomData, +} + +/// Represents a syntactically and semantically correct lightning BOLT11 invoice. +/// +/// There are three ways to construct an `Invoice`: +/// 1. using `InvoiceBuilder` +/// 2. using `Invoice::from_signed(SignedRawInvoice)` +/// 3. using `str::parse::(&str)` +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct Invoice { + signed_invoice: SignedRawInvoice, +} + +/// Represents the description of an invoice which has to be either a directly included string or +/// a hash of a description provided out of band. +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum InvoiceDescription<'f> { + /// Reference to the directly supplied description in the invoice + Direct(&'f Description), + + /// Reference to the description's hash included in the invoice + Hash(&'f Sha256), +} + +/// Represents a signed `RawInvoice` with cached hash. The signature is not checked and may be +/// invalid. +/// +/// # Invariants +/// The hash has to be either from the deserialized invoice or from the serialized `raw_invoice`. +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct SignedRawInvoice { + /// The rawInvoice that the signature belongs to + raw_invoice: RawInvoice, + + /// Hash of the `RawInvoice` that will be used to check the signature. + /// + /// * if the `SignedRawInvoice` was deserialized the hash is of from the original encoded form, + /// since it's not guaranteed that encoding it again will lead to the same result since integers + /// could have been encoded with leading zeroes etc. + /// * if the `SignedRawInvoice` was constructed manually the hash will be the calculated hash + /// from the `RawInvoice` + hash: [u8; 32], + + /// signature of the payment request + signature: Signature, +} + +/// Represents an syntactically correct Invoice for a payment on the lightning network, +/// but without the signature information. +/// De- and encoding should not lead to information loss but may lead to different hashes. +/// +/// For methods without docs see the corresponding methods in `Invoice`. +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct RawInvoice { + /// human readable part + pub hrp: RawHrp, + + /// data part + pub data: RawDataPart, +} + +/// Data of the `RawInvoice` that is encoded in the human readable part +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct RawHrp { + /// The currency deferred from the 3rd and 4th character of the bech32 transaction + pub currency: Currency, + + /// The amount that, multiplied by the SI prefix, has to be payed + pub raw_amount: Option, + + /// SI prefix that gets multiplied with the `raw_amount` + pub si_prefix: Option, +} + +/// Data of the `RawInvoice` that is encoded in the data part +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct RawDataPart { + /// generation time of the invoice + pub timestamp: PositiveTimestamp, + + /// tagged fields of the payment request + pub tagged_fields: Vec, +} + +/// A timestamp that refers to a date after 1 January 1970 which means its representation as UNIX +/// timestamp is positive. +/// +/// # Invariants +/// The UNIX timestamp representing the stored time has to be positive and small enough so that +/// a `EpiryTime` can be added to it without an overflow. +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct PositiveTimestamp(SystemTime); + +/// SI prefixes for the human readable part +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +pub enum SiPrefix { + /// 10^-3 + Milli, + /// 10^-6 + Micro, + /// 10^-9 + Nano, + /// 10^-12 + Pico, +} + +impl SiPrefix { + /// Returns the multiplier to go from a BTC value to picoBTC implied by this SiPrefix. + /// This is effectively 10^12 * the prefix multiplier + pub fn multiplier(&self) -> u64 { + match *self { + SiPrefix::Milli => 1_000_000_000, + SiPrefix::Micro => 1_000_000, + SiPrefix::Nano => 1_000, + SiPrefix::Pico => 1, + } + } + + /// Returns all enum variants of `SiPrefix` sorted in descending order of their associated + /// multiplier. + pub fn values_desc() -> &'static [SiPrefix] { + use SiPrefix::*; + static VALUES: [SiPrefix; 4] = [Milli, Micro, Nano, Pico]; + &VALUES + } +} + +/// Enum representing the crypto currencies (or networks) supported by this library +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum Currency { + /// Bitcoin mainnet + Bitcoin, + + /// Bitcoin testnet + BitcoinTestnet, + + /// Bitcoin regtest + Regtest, + + /// Bitcoin simnet/signet + Simnet, +} + +/// Tagged field which may have an unknown tag +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum RawTaggedField { + /// Parsed tagged field with known tag + KnownSemantics(TaggedField), + /// tagged field which was not parsed due to an unknown tag or undefined field semantics + UnknownSemantics(Vec), +} + +/// Tagged field with known tag +/// +/// For descriptions of the enum values please refer to the enclosed type's docs. +#[allow(missing_docs)] +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum TaggedField { + PaymentHash(Sha256), + Description(Description), + PayeePubKey(PayeePubKey), + DescriptionHash(Sha256), + ExpiryTime(ExpiryTime), + MinFinalCltvExpiry(MinFinalCltvExpiry), + Fallback(Fallback), + Route(Route), + PaymentSecret(PaymentSecret), +} + +/// SHA-256 hash +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct Sha256(pub sha256::Hash); + +/// Description string +/// +/// # Invariants +/// The description can be at most 639 __bytes__ long +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct Description(String); + +/// Payee public key +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct PayeePubKey(pub PublicKey); + +/// 256-bit payment secret +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct PaymentSecret(pub [u8; 32]); + +/// Positive duration that defines when (relatively to the timestamp) in the future the invoice +/// expires +/// +/// # Invariants +/// The number of seconds this expiry time represents has to be in the range +/// `0...(SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME)` to avoid overflows when adding it to a +/// timestamp +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct ExpiryTime(Duration); + +/// `min_final_cltv_expiry` to use for the last HTLC in the route +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct MinFinalCltvExpiry(pub u64); + +// TODO: better types instead onf byte arrays +/// Fallback address in case no LN payment is possible +#[allow(missing_docs)] +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum Fallback { + SegWitProgram { + version: u5, + program: Vec, + }, + PubKeyHash([u8; 20]), + ScriptHash([u8; 20]), +} + +/// Recoverable signature +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct Signature(pub RecoverableSignature); + +/// Private routing information +/// +/// # Invariants +/// The encoded route has to be <1024 5bit characters long (<=639 bytes or <=12 hops) +/// +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct Route(Vec); + +/// Node on a private route +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct RouteHop { + /// Node's public key + pub pubkey: PublicKey, + + /// Which channel of this node we would be using + pub short_channel_id: [u8; 8], + + /// Fee charged by this node per transaction + pub fee_base_msat: u32, + + /// Fee charged by this node proportional to the amount routed + pub fee_proportional_millionths: u32, + + /// Delta substracted by this node from incoming cltv_expiry value + pub cltv_expiry_delta: u16, +} + +/// Tag constants as specified in BOLT11 +#[allow(missing_docs)] +pub mod constants { + pub const TAG_PAYMENT_HASH: u8 = 1; + pub const TAG_DESCRIPTION: u8 = 13; + pub const TAG_PAYEE_PUB_KEY: u8 = 19; + pub const TAG_DESCRIPTION_HASH: u8 = 23; + pub const TAG_EXPIRY_TIME: u8 = 6; + pub const TAG_MIN_FINAL_CLTV_EXPIRY: u8 = 24; + pub const TAG_FALLBACK: u8 = 9; + pub const TAG_ROUTE: u8 = 3; + pub const TAG_PAYMENT_SECRET: u8 = 16; +} + +impl InvoiceBuilder { + /// Construct new, empty `InvoiceBuilder`. All necessary fields have to be filled first before + /// `InvoiceBuilder::build(self)` becomes available. + pub fn new(currrency: Currency) -> Self { + InvoiceBuilder { + currency: currrency, + amount: None, + si_prefix: None, + timestamp: None, + tagged_fields: Vec::new(), + error: None, + + phantom_d: std::marker::PhantomData, + phantom_h: std::marker::PhantomData, + phantom_t: std::marker::PhantomData, + } + } +} + +impl InvoiceBuilder { + /// Helper function to set the completeness flags. + fn set_flags(self) -> InvoiceBuilder { + InvoiceBuilder:: { + currency: self.currency, + amount: self.amount, + si_prefix: self.si_prefix, + timestamp: self.timestamp, + tagged_fields: self.tagged_fields, + error: self.error, + + phantom_d: std::marker::PhantomData, + phantom_h: std::marker::PhantomData, + phantom_t: std::marker::PhantomData, + } + } + + /// Sets the amount in pico BTC. The optimal SI prefix is choosen automatically. + pub fn amount_pico_btc(mut self, amount: u64) -> Self { + let biggest_possible_si_prefix = SiPrefix::values_desc() + .iter() + .find(|prefix| amount % prefix.multiplier() == 0) + .expect("Pico should always match"); + self.amount = Some(amount / biggest_possible_si_prefix.multiplier()); + self.si_prefix = Some(*biggest_possible_si_prefix); + self + } + + /// Sets the payee's public key. + pub fn payee_pub_key(mut self, pub_key: PublicKey) -> Self { + self.tagged_fields.push(TaggedField::PayeePubKey(PayeePubKey(pub_key))); + self + } + + /// Sets the payment secret + pub fn payment_secret(mut self, payment_secret: PaymentSecret) -> Self { + self.tagged_fields.push(TaggedField::PaymentSecret(payment_secret)); + self + } + + /// Sets the expiry time + pub fn expiry_time(mut self, expiry_time: Duration) -> Self { + match ExpiryTime::from_duration(expiry_time) { + Ok(t) => self.tagged_fields.push(TaggedField::ExpiryTime(t)), + Err(e) => self.error = Some(e), + }; + self + } + + /// Sets `min_final_cltv_expiry`. + pub fn min_final_cltv_expiry(mut self, min_final_cltv_expiry: u64) -> Self { + self.tagged_fields.push(TaggedField::MinFinalCltvExpiry(MinFinalCltvExpiry(min_final_cltv_expiry))); + self + } + + /// Adds a fallback address. + pub fn fallback(mut self, fallback: Fallback) -> Self { + self.tagged_fields.push(TaggedField::Fallback(fallback)); + self + } + + /// Adds a private route. + pub fn route(mut self, route: Vec) -> Self { + match Route::new(route) { + Ok(r) => self.tagged_fields.push(TaggedField::Route(r)), + Err(e) => self.error = Some(e), + } + self + } +} + +impl InvoiceBuilder { + /// Builds a `RawInvoice` if no `CreationError` occurred while construction any of the fields. + pub fn build_raw(self) -> Result { + + // If an error occurred at any time before, return it now + if let Some(e) = self.error { + return Err(e); + } + + let hrp = RawHrp { + currency: self.currency, + raw_amount: self.amount, + si_prefix: self.si_prefix, + }; + + let timestamp = self.timestamp.expect("ensured to be Some(t) by type T"); + + let tagged_fields = self.tagged_fields.into_iter().map(|tf| { + RawTaggedField::KnownSemantics(tf) + }).collect::>(); + + let data = RawDataPart { + timestamp: timestamp, + tagged_fields: tagged_fields, + }; + + Ok(RawInvoice { + hrp: hrp, + data: data, + }) + } +} + +impl InvoiceBuilder { + /// Set the description. This function is only available if no description (hash) was set. + pub fn description(mut self, description: String) -> InvoiceBuilder { + match Description::new(description) { + Ok(d) => self.tagged_fields.push(TaggedField::Description(d)), + Err(e) => self.error = Some(e), + } + self.set_flags() + } + + /// Set the description hash. This function is only available if no description (hash) was set. + pub fn description_hash(mut self, description_hash: sha256::Hash) -> InvoiceBuilder { + self.tagged_fields.push(TaggedField::DescriptionHash(Sha256(description_hash))); + self.set_flags() + } +} + +impl InvoiceBuilder { + /// Set the payment hash. This function is only available if no payment hash was set. + pub fn payment_hash(mut self, hash: sha256::Hash) -> InvoiceBuilder { + self.tagged_fields.push(TaggedField::PaymentHash(Sha256(hash))); + self.set_flags() + } +} + +impl InvoiceBuilder { + /// Sets the timestamp. + pub fn timestamp(mut self, time: SystemTime) -> InvoiceBuilder { + match PositiveTimestamp::from_system_time(time) { + Ok(t) => self.timestamp = Some(t), + Err(e) => self.error = Some(e), + } + + self.set_flags() + } + + /// Sets the timestamp to the current UNIX timestamp. + pub fn current_timestamp(mut self) -> InvoiceBuilder { + let now = PositiveTimestamp::from_system_time(SystemTime::now()); + self.timestamp = Some(now.expect("for the foreseeable future this shouldn't happen")); + self.set_flags() + } +} + +impl InvoiceBuilder { + /// Builds and signs an invoice using the supplied `sign_function`. This function MAY NOT fail + /// and MUST produce a recoverable signature valid for the given hash and if applicable also for + /// the included payee public key. + pub fn build_signed(self, sign_function: F) -> Result + where F: FnOnce(&Message) -> RecoverableSignature + { + let invoice = self.try_build_signed::<_, ()>(|hash| { + Ok(sign_function(hash)) + }); + + match invoice { + Ok(i) => Ok(i), + Err(SignOrCreationError::CreationError(e)) => Err(e), + Err(SignOrCreationError::SignError(())) => unreachable!(), + } + } + + /// Builds and signs an invoice using the supplied `sign_function`. This function MAY fail with + /// an error of type `E` and MUST produce a recoverable signature valid for the given hash and + /// if applicable also for the included payee public key. + pub fn try_build_signed(self, sign_function: F) -> Result> + where F: FnOnce(&Message) -> Result + { + let raw = match self.build_raw() { + Ok(r) => r, + Err(e) => return Err(SignOrCreationError::CreationError(e)), + }; + + let signed = match raw.sign(sign_function) { + Ok(s) => s, + Err(e) => return Err(SignOrCreationError::SignError(e)), + }; + + let invoice = Invoice { + signed_invoice: signed, + }; + + invoice.check_field_counts().expect("should be ensured by type signature of builder"); + + Ok(invoice) + } +} + + +impl SignedRawInvoice { + /// Disassembles the `SignedRawInvoice` into its three parts: + /// 1. raw invoice + /// 2. hash of the raw invoice + /// 3. signature + pub fn into_parts(self) -> (RawInvoice, [u8; 32], Signature) { + (self.raw_invoice, self.hash, self.signature) + } + + /// The `RawInvoice` which was signed. + pub fn raw_invoice(&self) -> &RawInvoice { + &self.raw_invoice + } + + /// The hash of the `RawInvoice` that was signed. + pub fn hash(&self) -> &[u8; 32] { + &self.hash + } + + /// Signature for the invoice. + pub fn signature(&self) -> &Signature { + &self.signature + } + + /// Recovers the public key used for signing the invoice from the recoverable signature. + pub fn recover_payee_pub_key(&self) -> Result { + let hash = Message::from_slice(&self.hash[..]) + .expect("Hash is 32 bytes long, same as MESSAGE_SIZE"); + + Ok(PayeePubKey(Secp256k1::new().recover( + &hash, + &self.signature + )?)) + } + + /// Checks if the signature is valid for the included payee public key or if none exists if it's + /// valid for the recovered signature (which should always be true?). + pub fn check_signature(&self) -> bool { + let included_pub_key = self.raw_invoice.payee_pub_key(); + + let mut recovered_pub_key = Option::None; + if recovered_pub_key.is_none() { + let recovered = match self.recover_payee_pub_key() { + Ok(pk) => pk, + Err(_) => return false, + }; + recovered_pub_key = Some(recovered); + } + + let pub_key = included_pub_key.or_else(|| recovered_pub_key.as_ref()) + .expect("One is always present"); + + let hash = Message::from_slice(&self.hash[..]) + .expect("Hash is 32 bytes long, same as MESSAGE_SIZE"); + + let secp_context = Secp256k1::new(); + let verification_result = secp_context.verify( + &hash, + &self.signature.to_standard(), + pub_key + ); + + match verification_result { + Ok(()) => true, + Err(_) => false, + } + } +} + +/// Finds the first element of an enum stream of a given variant and extracts one member of the +/// variant. If no element was found `None` gets returned. +/// +/// The following example would extract the first +/// ``` +/// use Enum::* +/// +/// enum Enum { +/// A(u8), +/// B(u16) +/// } +/// +/// let elements = vec![A(1), A(2), B(3), A(4)] +/// +/// assert_eq!(find_extract!(elements.iter(), Enum::B(ref x), x), Some(3u16)) +/// ``` +macro_rules! find_extract { + ($iter:expr, $enm:pat, $enm_var:ident) => { + $iter.filter_map(|tf| match *tf { + $enm => Some($enm_var), + _ => None, + }).next() + }; +} + +#[allow(missing_docs)] +impl RawInvoice { + /// Hash the HRP as bytes and signatureless data part. + fn hash_from_parts(hrp_bytes: &[u8], data_without_signature: &[u5]) -> [u8; 32] { + use bech32::FromBase32; + + let mut preimage = Vec::::from(hrp_bytes); + + 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(u5::try_from_u8(0).unwrap()); + + // if overhang is in (1..3) we need to add u5(0) padding two times + if overhang < 3 { + data_part.push(u5::try_from_u8(0).unwrap()); + } + } + + preimage.extend_from_slice(&Vec::::from_base32(&data_part) + .expect("No padding error may occur due to appended zero above.")); + + let mut hash: [u8; 32] = Default::default(); + hash.copy_from_slice(&sha256::Hash::hash(&preimage)[..]); + hash + } + + /// Calculate the hash of the encoded `RawInvoice` + pub fn hash(&self) -> [u8; 32] { + use bech32::ToBase32; + + RawInvoice::hash_from_parts( + self.hrp.to_string().as_bytes(), + &self.data.to_base32() + ) + } + + /// Signs the invoice using the supplied `sign_function`. This function MAY fail with an error + /// of type `E`. Since the signature of a `SignedRawInvoice` is not required to be valid there + /// are no constraints regarding the validity of the produced signature. + pub fn sign(self, sign_method: F) -> Result + where F: FnOnce(&Message) -> Result + { + let raw_hash = self.hash(); + let hash = Message::from_slice(&raw_hash[..]) + .expect("Hash is 32 bytes long, same as MESSAGE_SIZE"); + let signature = sign_method(&hash)?; + + Ok(SignedRawInvoice { + raw_invoice: self, + hash: raw_hash, + signature: Signature(signature), + }) + } + + /// Returns an iterator over all tagged fields with known semantics. + pub fn known_tagged_fields(&self) + -> FilterMap, fn(&RawTaggedField) -> Option<&TaggedField>> + { + // For 1.14.0 compatibility: closures' types can't be written an fn()->() in the + // function's type signature. + // TODO: refactor once impl Trait is available + fn match_raw(raw: &RawTaggedField) -> Option<&TaggedField> { + match *raw { + RawTaggedField::KnownSemantics(ref tf) => Some(tf), + _ => None, + } + } + + self.data.tagged_fields.iter().filter_map(match_raw ) + } + + pub fn payment_hash(&self) -> Option<&Sha256> { + find_extract!(self.known_tagged_fields(), TaggedField::PaymentHash(ref x), x) + } + + pub fn description(&self) -> Option<&Description> { + find_extract!(self.known_tagged_fields(), TaggedField::Description(ref x), x) + } + + pub fn payee_pub_key(&self) -> Option<&PayeePubKey> { + find_extract!(self.known_tagged_fields(), TaggedField::PayeePubKey(ref x), x) + } + + pub fn description_hash(&self) -> Option<&Sha256> { + find_extract!(self.known_tagged_fields(), TaggedField::DescriptionHash(ref x), x) + } + + pub fn expiry_time(&self) -> Option<&ExpiryTime> { + find_extract!(self.known_tagged_fields(), TaggedField::ExpiryTime(ref x), x) + } + + pub fn min_final_cltv_expiry(&self) -> Option<&MinFinalCltvExpiry> { + find_extract!(self.known_tagged_fields(), TaggedField::MinFinalCltvExpiry(ref x), x) + } + + pub fn payment_secret(&self) -> Option<&PaymentSecret> { + find_extract!(self.known_tagged_fields(), TaggedField::PaymentSecret(ref x), x) + } + + pub fn fallbacks(&self) -> Vec<&Fallback> { + self.known_tagged_fields().filter_map(|tf| match tf { + &TaggedField::Fallback(ref f) => Some(f), + _ => None, + }).collect::>() + } + + pub fn routes(&self) -> Vec<&Route> { + self.known_tagged_fields().filter_map(|tf| match tf { + &TaggedField::Route(ref r) => Some(r), + _ => None, + }).collect::>() + } + + pub fn amount_pico_btc(&self) -> Option { + self.hrp.raw_amount.map(|v| { + v * self.hrp.si_prefix.as_ref().map_or(1_000_000_000_000, |si| { si.multiplier() }) + }) + } + + pub fn currency(&self) -> Currency { + self.hrp.currency.clone() + } +} + +impl PositiveTimestamp { + /// Create a new `PositiveTimestamp` from a unix timestamp in the Range + /// `0...SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME`, otherwise return a + /// `CreationError::TimestampOutOfBounds`. + pub fn from_unix_timestamp(unix_seconds: u64) -> Result { + if unix_seconds > SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME { + Err(CreationError::TimestampOutOfBounds) + } else { + Ok(PositiveTimestamp(UNIX_EPOCH + Duration::from_secs(unix_seconds))) + } + } + + /// Create a new `PositiveTimestamp` from a `SystemTime` with a corresponding unix timestamp in + /// the Range `0...SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME`, otherwise return a + /// `CreationError::TimestampOutOfBounds`. + pub fn from_system_time(time: SystemTime) -> Result { + if time + .duration_since(UNIX_EPOCH) + .map(|t| t.as_secs() <= SYSTEM_TIME_MAX_UNIX_TIMESTAMP - MAX_EXPIRY_TIME) + .unwrap_or(true) + { + Ok(PositiveTimestamp(time)) + } else { + Err(CreationError::TimestampOutOfBounds) + } + } + + /// Returns the UNIX timestamp representing the stored time + pub fn as_unix_timestamp(&self) -> u64 { + self.0.duration_since(UNIX_EPOCH) + .expect("ensured by type contract/constructors") + .as_secs() + } + + /// Returns a reference to the internal `SystemTime` time representation + pub fn as_time(&self) -> &SystemTime { + &self.0 + } +} + +impl Into for PositiveTimestamp { + fn into(self) -> SystemTime { + self.0 + } +} + +impl Deref for PositiveTimestamp { + type Target = SystemTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Invoice { + /// Transform the `Invoice` into it's unchecked version + pub fn into_signed_raw(self) -> SignedRawInvoice { + self.signed_invoice + } + + /// Check that all mandatory fields are present + fn check_field_counts(&self) -> Result<(), SemanticError> { + // "A writer MUST include exactly one p field […]." + let payment_hash_cnt = self.tagged_fields().filter(|&tf| match *tf { + TaggedField::PaymentHash(_) => true, + _ => false, + }).count(); + if payment_hash_cnt < 1 { + return Err(SemanticError::NoPaymentHash); + } else if payment_hash_cnt > 1 { + return Err(SemanticError::MultiplePaymentHashes); + } + + // "A writer MUST include either exactly one d or exactly one h field." + let description_cnt = self.tagged_fields().filter(|&tf| match *tf { + TaggedField::Description(_) | TaggedField::DescriptionHash(_) => true, + _ => false, + }).count(); + if description_cnt < 1 { + return Err(SemanticError::NoDescription); + } else if description_cnt > 1 { + return Err(SemanticError::MultipleDescriptions); + } + + Ok(()) + } + + /// Check that the invoice is signed correctly and that key recovery works + pub fn check_signature(&self) -> Result<(), SemanticError> { + match self.signed_invoice.recover_payee_pub_key() { + Err(secp256k1::Error::InvalidRecoveryId) => + return Err(SemanticError::InvalidRecoveryId), + Err(_) => panic!("no other error may occur"), + Ok(_) => {}, + } + + if !self.signed_invoice.check_signature() { + return Err(SemanticError::InvalidSignature); + } + + Ok(()) + } + + /// Constructs an `Invoice` from a `SignedInvoice` by checking all its invariants. + /// ``` + /// use lightning_invoice::*; + /// + /// let invoice = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp\ + /// l2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d7\ + /// 3gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ec\ + /// ky03ylcqca784w"; + /// + /// let signed = invoice.parse::().unwrap(); + /// + /// assert!(Invoice::from_signed(signed).is_ok()); + /// ``` + pub fn from_signed(signed_invoice: SignedRawInvoice) -> Result { + let invoice = Invoice { + signed_invoice: signed_invoice, + }; + invoice.check_field_counts()?; + invoice.check_signature()?; + + Ok(invoice) + } + + /// Returns the `Invoice`'s timestamp (should equal it's creation time) + pub fn timestamp(&self) -> &SystemTime { + self.signed_invoice.raw_invoice().data.timestamp.as_time() + } + + /// Returns an iterator over all tagged fields of this Invoice. + pub fn tagged_fields(&self) + -> FilterMap, fn(&RawTaggedField) -> Option<&TaggedField>> { + self.signed_invoice.raw_invoice().known_tagged_fields() + } + + /// Returns the hash to which we will receive the preimage on completion of the payment + pub fn payment_hash(&self) -> &sha256::Hash { + &self.signed_invoice.payment_hash().expect("checked by constructor").0 + } + + /// Return the description or a hash of it for longer ones + pub fn description(&self) -> InvoiceDescription { + if let Some(ref direct) = self.signed_invoice.description() { + return InvoiceDescription::Direct(direct); + } else if let Some(ref hash) = self.signed_invoice.description_hash() { + return InvoiceDescription::Hash(hash); + } + unreachable!("ensured by constructor"); + } + + /// Get the payee's public key if one was included in the invoice + pub fn payee_pub_key(&self) -> Option<&PublicKey> { + 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() + } + + /// 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 + } + + /// Returns the invoice's expiry time if present + pub fn expiry_time(&self) -> Duration { + self.signed_invoice.expiry_time() + .map(|x| x.0) + .unwrap_or(Duration::from_secs(3600)) + } + + /// Returns the invoice's `min_cltv_expiry` time if present + pub fn min_final_cltv_expiry(&self) -> Option<&u64> { + self.signed_invoice.min_final_cltv_expiry().map(|x| &x.0) + } + + /// Returns a list of all fallback addresses + pub fn fallbacks(&self) -> Vec<&Fallback> { + self.signed_invoice.fallbacks() + } + + /// Returns a list of all routes included in the invoice + pub fn routes(&self) -> Vec<&Route> { + self.signed_invoice.routes() + } + + /// Returns the currency for which the invoice was issued + pub fn currency(&self) -> Currency { + self.signed_invoice.currency() + } + + /// Returns the amount if specified in the invoice as pico . + pub fn amount_pico_btc(&self) -> Option { + self.signed_invoice.amount_pico_btc() + } +} + +impl From for RawTaggedField { + fn from(tf: TaggedField) -> Self { + RawTaggedField::KnownSemantics(tf) + } +} + +impl TaggedField { + /// Numeric representation of the field's tag + pub fn tag(&self) -> u5 { + let tag = match *self { + TaggedField::PaymentHash(_) => constants::TAG_PAYMENT_HASH, + TaggedField::Description(_) => constants::TAG_DESCRIPTION, + TaggedField::PayeePubKey(_) => constants::TAG_PAYEE_PUB_KEY, + TaggedField::DescriptionHash(_) => constants::TAG_DESCRIPTION_HASH, + TaggedField::ExpiryTime(_) => constants::TAG_EXPIRY_TIME, + TaggedField::MinFinalCltvExpiry(_) => constants::TAG_MIN_FINAL_CLTV_EXPIRY, + TaggedField::Fallback(_) => constants::TAG_FALLBACK, + TaggedField::Route(_) => constants::TAG_ROUTE, + TaggedField::PaymentSecret(_) => constants::TAG_PAYMENT_SECRET, + }; + + u5::try_from_u8(tag).expect("all tags defined are <32") + } +} + +impl Description { + + /// Creates a new `Description` if `description` is at most 1023 __bytes__ long, + /// returns `CreationError::DescriptionTooLong` otherwise + /// + /// Please note that single characters may use more than one byte due to UTF8 encoding. + pub fn new(description: String) -> Result { + if description.len() > 639 { + Err(CreationError::DescriptionTooLong) + } else { + Ok(Description(description)) + } + } + + /// Returns the underlying description `String` + pub fn into_inner(self) -> String { + self.0 + } +} + +impl Into for Description { + fn into(self) -> String { + self.into_inner() + } +} + +impl Deref for Description { + type Target = str; + + fn deref(&self) -> &str { + &self.0 + } +} + +impl From for PayeePubKey { + fn from(pk: PublicKey) -> Self { + PayeePubKey(pk) + } +} + +impl Deref for PayeePubKey { + type Target = PublicKey; + + fn deref(&self) -> &PublicKey { + &self.0 + } +} + +impl ExpiryTime { + /// Construct an `ExpiryTime` from seconds. If there exists a `PositiveTimestamp` which would + /// overflow on adding the `EpiryTime` to it then this function will return a + /// `CreationError::ExpiryTimeOutOfBounds`. + pub fn from_seconds(seconds: u64) -> Result { + if seconds <= MAX_EXPIRY_TIME { + Ok(ExpiryTime(Duration::from_secs(seconds))) + } else { + Err(CreationError::ExpiryTimeOutOfBounds) + } + } + + /// Construct an `ExpiryTime` from a `Duration`. If there exists a `PositiveTimestamp` which + /// would overflow on adding the `EpiryTime` to it then this function will return a + /// `CreationError::ExpiryTimeOutOfBounds`. + pub fn from_duration(duration: Duration) -> Result { + if duration.as_secs() <= MAX_EXPIRY_TIME { + Ok(ExpiryTime(duration)) + } else { + Err(CreationError::ExpiryTimeOutOfBounds) + } + } + + /// Returns the expiry time in seconds + pub fn as_seconds(&self) -> u64 { + self.0.as_secs() + } + + /// Returns a reference to the underlying `Duration` (=expiry time) + pub fn as_duration(&self) -> &Duration { + &self.0 + } +} + +impl Route { + /// Create a new (partial) route from a list of hops + pub fn new(hops: Vec) -> Result { + if hops.len() <= 12 { + Ok(Route(hops)) + } else { + Err(CreationError::RouteTooLong) + } + } + + /// Returrn the underlying vector of hops + pub fn into_inner(self) -> Vec { + self.0 + } +} + +impl Into> for Route { + fn into(self) -> Vec { + self.into_inner() + } +} + +impl Deref for Route { + type Target = Vec; + + fn deref(&self) -> &Vec { + &self.0 + } +} + +impl Deref for Signature { + type Target = RecoverableSignature; + + fn deref(&self) -> &RecoverableSignature { + &self.0 + } +} + +impl Deref for SignedRawInvoice { + type Target = RawInvoice; + + fn deref(&self) -> &RawInvoice { + &self.raw_invoice + } +} + +/// Errors that may occur when constructing a new `RawInvoice` or `Invoice` +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum CreationError { + /// The supplied description string was longer than 639 __bytes__ (see [`Description::new(…)`](./struct.Description.html#method.new)) + DescriptionTooLong, + + /// The specified route has too many hops and can't be encoded + RouteTooLong, + + /// The unix timestamp of the supplied date is <0 or can't be represented as `SystemTime` + TimestampOutOfBounds, + + /// The supplied expiry time could cause an overflow if added to a `PositiveTimestamp` + ExpiryTimeOutOfBounds, +} + +impl Display for CreationError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + CreationError::DescriptionTooLong => f.write_str("The supplied description string was longer than 639 bytes"), + CreationError::RouteTooLong => f.write_str("The specified route has too many hops and can't be encoded"), + CreationError::TimestampOutOfBounds => f.write_str("The unix timestamp of the supplied date is <0 or can't be represented as `SystemTime`"), + CreationError::ExpiryTimeOutOfBounds => f.write_str("The supplied expiry time could cause an overflow if added to a `PositiveTimestamp`"), + } + } +} + +impl std::error::Error for CreationError { } + +/// Errors that may occur when converting a `RawInvoice` to an `Invoice`. They relate to the +/// requirements sections in BOLT #11 +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum SemanticError { + /// The invoice is missing the mandatory payment hash + NoPaymentHash, + + /// The invoice has multiple payment hashes which isn't allowed + MultiplePaymentHashes, + + /// No description or description hash are part of the invoice + NoDescription, + + /// The invoice contains multiple descriptions and/or description hashes which isn't allowed + MultipleDescriptions, + + /// The recovery id doesn't fit the signature/pub key + InvalidRecoveryId, + + /// The invoice's signature is invalid + InvalidSignature, +} + +impl Display for SemanticError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + SemanticError::NoPaymentHash => f.write_str("The invoice is missing the mandatory payment hash"), + SemanticError::MultiplePaymentHashes => f.write_str("The invoice has multiple payment hashes which isn't allowed"), + SemanticError::NoDescription => f.write_str("No description or description hash are part of the invoice"), + SemanticError::MultipleDescriptions => f.write_str("The invoice contains multiple descriptions and/or description hashes which isn't allowed"), + SemanticError::InvalidRecoveryId => f.write_str("The recovery id doesn't fit the signature/pub key"), + SemanticError::InvalidSignature => f.write_str("The invoice's signature is invalid"), + } + } +} + +impl std::error::Error for SemanticError { } + +/// When signing using a fallible method either an user-supplied `SignError` or a `CreationError` +/// may occur. +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum SignOrCreationError { + /// An error occurred during signing + SignError(S), + + /// An error occurred while building the transaction + CreationError(CreationError), +} + +impl Display for SignOrCreationError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + SignOrCreationError::SignError(_) => f.write_str("An error occurred during signing"), + SignOrCreationError::CreationError(err) => err.fmt(f), + } + } +} + +#[cfg(test)] +mod test { + use bitcoin_hashes::hex::FromHex; + use bitcoin_hashes::sha256; + + #[test] + fn test_system_time_bounds_assumptions() { + ::check_platform(); + + assert_eq!( + ::PositiveTimestamp::from_unix_timestamp(::SYSTEM_TIME_MAX_UNIX_TIMESTAMP + 1), + Err(::CreationError::TimestampOutOfBounds) + ); + + assert_eq!( + ::ExpiryTime::from_seconds(::MAX_EXPIRY_TIME + 1), + Err(::CreationError::ExpiryTimeOutOfBounds) + ); + } + + #[test] + fn test_calc_invoice_hash() { + use ::{RawInvoice, RawHrp, RawDataPart, Currency, PositiveTimestamp}; + use ::TaggedField::*; + + let invoice = RawInvoice { + hrp: RawHrp { + currency: Currency::Bitcoin, + raw_amount: None, + si_prefix: None, + }, + data: RawDataPart { + timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(), + tagged_fields: vec![ + PaymentHash(::Sha256(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap())).into(), + Description(::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.hash(), expected_hash) + } + + #[test] + fn test_check_signature() { + use TaggedField::*; + use secp256k1::Secp256k1; + use secp256k1::recovery::{RecoveryId, RecoverableSignature}; + use secp256k1::key::{SecretKey, PublicKey}; + use {SignedRawInvoice, Signature, RawInvoice, RawHrp, RawDataPart, Currency, Sha256, + PositiveTimestamp}; + + let invoice = SignedRawInvoice { + raw_invoice: RawInvoice { + hrp: RawHrp { + currency: Currency::Bitcoin, + raw_amount: None, + si_prefix: None, + }, + data: RawDataPart { + timestamp: PositiveTimestamp::from_unix_timestamp(1496314658).unwrap(), + tagged_fields: vec ! [ + PaymentHash(Sha256(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap())).into(), + Description( + ::Description::new( + "Please consider supporting this project".to_owned() + ).unwrap() + ).into(), + ], + }, + }, + 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 + ], + signature: Signature(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!(invoice.check_signature()); + + let private_key = SecretKey::from_slice( + &[ + 0xe1, 0x26, 0xf6, 0x8f, 0x7e, 0xaf, 0xcc, 0x8b, 0x74, 0xf5, 0x4d, 0x26, 0x9f, 0xe2, + 0x06, 0xbe, 0x71, 0x50, 0x00, 0xf9, 0x4d, 0xac, 0x06, 0x7d, 0x1c, 0x04, 0xa8, 0xca, + 0x3b, 0x2d, 0xb7, 0x34 + ][..] + ).unwrap(); + let public_key = PublicKey::from_secret_key(&Secp256k1::new(), &private_key); + + assert_eq!(invoice.recover_payee_pub_key(), Ok(::PayeePubKey(public_key))); + + let (raw_invoice, _, _) = invoice.into_parts(); + let new_signed = raw_invoice.sign::<_, ()>(|hash| { + Ok(Secp256k1::new().sign_recoverable(hash, &private_key)) + }).unwrap(); + + assert!(new_signed.check_signature()); + } + + #[test] + fn test_builder_amount() { + use ::*; + + let builder = InvoiceBuilder::new(Currency::Bitcoin) + .description("Test".into()) + .payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap()) + .current_timestamp(); + + let invoice = builder.clone() + .amount_pico_btc(15000) + .build_raw() + .unwrap(); + + assert_eq!(invoice.hrp.si_prefix, Some(SiPrefix::Nano)); + assert_eq!(invoice.hrp.raw_amount, Some(15)); + + + let invoice = builder.clone() + .amount_pico_btc(1500) + .build_raw() + .unwrap(); + + assert_eq!(invoice.hrp.si_prefix, Some(SiPrefix::Pico)); + assert_eq!(invoice.hrp.raw_amount, Some(1500)); + } + + #[test] + fn test_builder_fail() { + use ::*; + use std::iter::FromIterator; + use secp256k1::key::PublicKey; + + let builder = InvoiceBuilder::new(Currency::Bitcoin) + .payment_hash(sha256::Hash::from_slice(&[0;32][..]).unwrap()) + .current_timestamp(); + + let too_long_string = String::from_iter( + (0..1024).map(|_| '?') + ); + + let long_desc_res = builder.clone() + .description(too_long_string) + .build_raw(); + assert_eq!(long_desc_res, Err(CreationError::DescriptionTooLong)); + + let route_hop = RouteHop { + pubkey: PublicKey::from_slice( + &[ + 0x03, 0x9e, 0x03, 0xa9, 0x01, 0xb8, 0x55, 0x34, 0xff, 0x1e, 0x92, 0xc4, + 0x3c, 0x74, 0x43, 0x1f, 0x7c, 0xe7, 0x20, 0x46, 0x06, 0x0f, 0xcf, 0x7a, + 0x95, 0xc3, 0x7e, 0x14, 0x8f, 0x78, 0xc7, 0x72, 0x55 + ][..] + ).unwrap(), + short_channel_id: [0; 8], + fee_base_msat: 0, + fee_proportional_millionths: 0, + cltv_expiry_delta: 0, + }; + let too_long_route = vec![route_hop; 13]; + let long_route_res = builder.clone() + .description("Test".into()) + .route(too_long_route) + .build_raw(); + assert_eq!(long_route_res, Err(CreationError::RouteTooLong)); + + let sign_error_res = builder.clone() + .description("Test".into()) + .try_build_signed(|_| { + Err("ImaginaryError") + }); + assert_eq!(sign_error_res, Err(SignOrCreationError::SignError("ImaginaryError"))); + } + + #[test] + fn test_builder_ok() { + use ::*; + use secp256k1::Secp256k1; + use secp256k1::key::{SecretKey, PublicKey}; + use std::time::{UNIX_EPOCH, Duration}; + + let secp_ctx = Secp256k1::new(); + + let private_key = SecretKey::from_slice( + &[ + 0xe1, 0x26, 0xf6, 0x8f, 0x7e, 0xaf, 0xcc, 0x8b, 0x74, 0xf5, 0x4d, 0x26, 0x9f, 0xe2, + 0x06, 0xbe, 0x71, 0x50, 0x00, 0xf9, 0x4d, 0xac, 0x06, 0x7d, 0x1c, 0x04, 0xa8, 0xca, + 0x3b, 0x2d, 0xb7, 0x34 + ][..] + ).unwrap(); + let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key); + + let route_1 = vec![ + RouteHop { + pubkey: public_key.clone(), + short_channel_id: [123; 8], + fee_base_msat: 2, + fee_proportional_millionths: 1, + cltv_expiry_delta: 145, + }, + RouteHop { + pubkey: public_key.clone(), + short_channel_id: [42; 8], + fee_base_msat: 3, + fee_proportional_millionths: 2, + cltv_expiry_delta: 146, + } + ]; + + let route_2 = vec![ + RouteHop { + pubkey: public_key.clone(), + short_channel_id: [0; 8], + fee_base_msat: 4, + fee_proportional_millionths: 3, + cltv_expiry_delta: 147, + }, + RouteHop { + pubkey: public_key.clone(), + short_channel_id: [1; 8], + fee_base_msat: 5, + fee_proportional_millionths: 4, + cltv_expiry_delta: 148, + } + ]; + + let builder = InvoiceBuilder::new(Currency::BitcoinTestnet) + .amount_pico_btc(123) + .timestamp(UNIX_EPOCH + Duration::from_secs(1234567)) + .payee_pub_key(public_key.clone()) + .expiry_time(Duration::from_secs(54321)) + .min_final_cltv_expiry(144) + .min_final_cltv_expiry(143) + .fallback(Fallback::PubKeyHash([0;20])) + .route(route_1.clone()) + .route(route_2.clone()) + .description_hash(sha256::Hash::from_slice(&[3;32][..]).unwrap()) + .payment_hash(sha256::Hash::from_slice(&[21;32][..]).unwrap()); + + let invoice = builder.clone().build_signed(|hash| { + secp_ctx.sign_recoverable(hash, &private_key) + }).unwrap(); + + assert!(invoice.check_signature().is_ok()); + assert_eq!(invoice.tagged_fields().count(), 9); + + assert_eq!(invoice.amount_pico_btc(), Some(123)); + assert_eq!(invoice.currency(), Currency::BitcoinTestnet); + assert_eq!( + invoice.timestamp().duration_since(UNIX_EPOCH).unwrap().as_secs(), + 1234567 + ); + assert_eq!(invoice.payee_pub_key(), Some(&public_key)); + assert_eq!(invoice.expiry_time(), Duration::from_secs(54321)); + assert_eq!(invoice.min_final_cltv_expiry(), Some(&144)); + assert_eq!(invoice.fallbacks(), vec![&Fallback::PubKeyHash([0;20])]); + assert_eq!(invoice.routes(), vec![&Route(route_1), &Route(route_2)]); + assert_eq!( + invoice.description(), + InvoiceDescription::Hash(&Sha256(sha256::Hash::from_slice(&[3;32][..]).unwrap())) + ); + assert_eq!(invoice.payment_hash(), &sha256::Hash::from_slice(&[21;32][..]).unwrap()); + + let raw_invoice = builder.build_raw().unwrap(); + assert_eq!(raw_invoice, *invoice.into_signed_raw().raw_invoice()) + } +} diff --git a/lightning-invoice/src/ser.rs b/lightning-invoice/src/ser.rs new file mode 100644 index 00000000..2b4332f8 --- /dev/null +++ b/lightning-invoice/src/ser.rs @@ -0,0 +1,514 @@ +use std::fmt; +use std::fmt::{Display, Formatter}; +use bech32::{ToBase32, u5, WriteBase32, Base32Len}; + +use ::*; + +/// Converts a stream of bytes written to it to base32. On finalization the according padding will +/// be applied. That means the results of writing two data blocks with one or two `BytesToBase32` +/// converters will differ. +struct BytesToBase32<'a, W: WriteBase32 + 'a> { + /// Target for writing the resulting `u5`s resulting from the written bytes + writer: &'a mut W, + /// Holds all unwritten bits left over from last round. The bits are stored beginning from + /// the most significant bit. E.g. if buffer_bits=3, then the byte with bits a, b and c will + /// look as follows: [a, b, c, 0, 0, 0, 0, 0] + buffer: u8, + /// Amount of bits left over from last round, stored in buffer. + buffer_bits: u8, +} + +impl<'a, W: WriteBase32> BytesToBase32<'a, W> { + /// Create a new bytes-to-base32 converter with `writer` as a sink for the resulting base32 + /// data. + pub fn new(writer: &'a mut W) -> BytesToBase32<'a, W> { + BytesToBase32 { + writer, + buffer: 0, + buffer_bits: 0, + } + } + + /// Add more bytes to the current conversion unit + pub fn append(&mut self, bytes: &[u8]) -> Result<(), W::Err> { + for b in bytes { + self.append_u8(*b)?; + } + Ok(()) + } + + pub fn append_u8(&mut self, byte: u8) -> Result<(), W::Err> { + // Write first u5 if we have to write two u5s this round. That only happens if the + // buffer holds too many bits, so we don't have to combine buffer bits with new bits + // from this rounds byte. + if self.buffer_bits >= 5 { + self.writer.write_u5( + u5::try_from_u8((self.buffer & 0b11111000) >> 3 ).expect("<32") + )?; + self.buffer = self.buffer << 5; + self.buffer_bits -= 5; + } + + // Combine all bits from buffer with enough bits from this rounds byte so that they fill + // a u5. Save reamining bits from byte to buffer. + let from_buffer = self.buffer >> 3; + let from_byte = byte >> (3 + self.buffer_bits); // buffer_bits <= 4 + + self.writer.write_u5(u5::try_from_u8(from_buffer | from_byte).expect("<32"))?; + self.buffer = byte << (5 - self.buffer_bits); + self.buffer_bits = 3 + self.buffer_bits; + + Ok(()) + } + + pub fn finalize(mut self) -> Result<(), W::Err> { + self.inner_finalize()?; + std::mem::forget(self); + Ok(()) + } + + fn inner_finalize(&mut self) -> Result<(), W::Err>{ + // There can be at most two u5s left in the buffer after processing all bytes, write them. + if self.buffer_bits >= 5 { + self.writer.write_u5( + u5::try_from_u8((self.buffer & 0b11111000) >> 3).expect("<32") + )?; + self.buffer = self.buffer << 5; + self.buffer_bits -= 5; + } + + if self.buffer_bits != 0 { + self.writer.write_u5(u5::try_from_u8(self.buffer >> 3).expect("<32"))?; + } + + Ok(()) + } +} + +impl<'a, W: WriteBase32> Drop for BytesToBase32<'a, W> { + fn drop(&mut self) { + self.inner_finalize() + .expect("Unhandled error when finalizing conversion on drop. User finalize to handle.") + } +} + +/// Calculates the base32 encoded size of a byte slice +fn bytes_size_to_base32_size(byte_size: usize) -> usize { + let bits = byte_size * 8; + if bits % 5 == 0 { + // without padding bits + bits / 5 + } else { + // with padding bits + bits / 5 + 1 + } +} + +impl Display for Invoice { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + self.signed_invoice.fmt(f) + } +} + +impl Display for SignedRawInvoice { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + let hrp = self.raw_invoice.hrp.to_string(); + let mut data = self.raw_invoice.data.to_base32(); + data.extend_from_slice(&self.signature.to_base32()); + + bech32::encode_to_fmt(f, &hrp, data).expect("HRP is valid")?; + + Ok(()) + } +} + +impl Display for RawHrp { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + let amount = match self.raw_amount { + Some(ref amt) => amt.to_string(), + None => String::new(), + }; + + let si_prefix = match self.si_prefix { + Some(ref si) => si.to_string(), + None => String::new(), + }; + + write!( + f, + "ln{}{}{}", + self.currency, + amount, + si_prefix + ) + } +} + +impl Display for Currency { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + let currency_code = match *self { + Currency::Bitcoin => "bc", + Currency::BitcoinTestnet => "tb", + Currency::Regtest => "bcrt", + Currency::Simnet => "sb", + }; + write!(f, "{}", currency_code) + } +} + +impl Display for SiPrefix { + fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", + match *self { + SiPrefix::Milli => "m", + SiPrefix::Micro => "u", + SiPrefix::Nano => "n", + SiPrefix::Pico => "p", + } + ) + } +} + +fn encode_int_be_base32(int: u64) -> Vec { + let base = 32u64; + + let mut out_vec = Vec::::new(); + + let mut rem_int = int; + while rem_int != 0 { + out_vec.push(u5::try_from_u8((rem_int % base) as u8).expect("always <32")); + rem_int /= base; + } + + out_vec.reverse(); + out_vec +} + +fn encoded_int_be_base32_size(int: u64) -> usize { + for pos in (0..13).rev() { + if int & (0x1f << (5 * pos)) != 0 { + return (pos + 1) as usize; + } + } + 0usize +} + +fn encode_int_be_base256>(int: T) -> Vec { + let base = 256u64; + + let mut out_vec = Vec::::new(); + + let mut rem_int: u64 = int.into(); + while rem_int != 0 { + out_vec.push((rem_int % base) as u8); + rem_int /= base; + } + + out_vec.reverse(); + out_vec +} + +/// Appends the default value of `T` to the front of the `in_vec` till it reaches the length +/// `target_length`. If `in_vec` already is too lang `None` is returned. +fn try_stretch(mut in_vec: Vec, target_len: usize) -> Option> + where T: Default + Copy +{ + if in_vec.len() > target_len { + None + } else if in_vec.len() == target_len { + Some(in_vec) + } else { + let mut out_vec = Vec::::with_capacity(target_len); + out_vec.append(&mut vec![T::default(); target_len - in_vec.len()]); + out_vec.append(&mut in_vec); + Some(out_vec) + } +} + +impl ToBase32 for RawDataPart { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + // encode timestamp + self.timestamp.write_base32(writer)?; + + // encode tagged fields + for tagged_field in self.tagged_fields.iter() { + tagged_field.write_base32(writer)?; + } + + Ok(()) + } +} + +impl ToBase32 for PositiveTimestamp { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + // FIXME: use writer for int encoding + writer.write( + &try_stretch(encode_int_be_base32(self.as_unix_timestamp()), 7) + .expect("Can't be longer due than 7 u5s due to timestamp bounds") + ) + } +} + +impl ToBase32 for RawTaggedField { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + match *self { + RawTaggedField::UnknownSemantics(ref content) => { + writer.write(content) + }, + RawTaggedField::KnownSemantics(ref tagged_field) => { + tagged_field.write_base32(writer) + } + } + } +} + +impl ToBase32 for Sha256 { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + (&self.0[..]).write_base32(writer) + } +} +impl Base32Len for Sha256 { + fn base32_len(&self) -> usize { + (&self.0[..]).base32_len() + } +} + +impl ToBase32 for Description { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + self.as_bytes().write_base32(writer) + } +} + +impl Base32Len for Description { + fn base32_len(&self) -> usize { + self.0.as_bytes().base32_len() + } +} + +impl ToBase32 for PayeePubKey { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + (&self.serialize()[..]).write_base32(writer) + } +} + +impl Base32Len for PayeePubKey { + fn base32_len(&self) -> usize { + bytes_size_to_base32_size(secp256k1::constants::PUBLIC_KEY_SIZE) + } +} + +impl ToBase32 for PaymentSecret { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + (&self.0[..]).write_base32(writer) + } +} + +impl Base32Len for PaymentSecret { + fn base32_len(&self) -> usize { + bytes_size_to_base32_size(32) + } +} + +impl ToBase32 for ExpiryTime { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + writer.write(&encode_int_be_base32(self.as_seconds())) + } +} + +impl Base32Len for ExpiryTime { + fn base32_len(&self) -> usize { + encoded_int_be_base32_size(self.0.as_secs()) + } +} + +impl ToBase32 for MinFinalCltvExpiry { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + writer.write(&encode_int_be_base32(self.0)) + } +} + +impl Base32Len for MinFinalCltvExpiry { + fn base32_len(&self) -> usize { + encoded_int_be_base32_size(self.0) + } +} + +impl ToBase32 for Fallback { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + match *self { + Fallback::SegWitProgram {version: v, program: ref p} => { + writer.write_u5(v)?; + p.write_base32(writer) + }, + Fallback::PubKeyHash(ref hash) => { + writer.write_u5(u5::try_from_u8(17).expect("17 < 32"))?; + (&hash[..]).write_base32(writer) + }, + Fallback::ScriptHash(ref hash) => { + writer.write_u5(u5::try_from_u8(18).expect("18 < 32"))?; + (&hash[..]).write_base32(writer) + } + } + } +} + +impl Base32Len for Fallback { + fn base32_len(&self) -> usize { + match *self { + Fallback::SegWitProgram {program: ref p, ..} => { + bytes_size_to_base32_size(p.len()) + 1 + }, + Fallback::PubKeyHash(_) | Fallback::ScriptHash(_) => { + 33 + }, + } + } +} + +impl ToBase32 for Route { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + let mut converter = BytesToBase32::new(writer); + + for hop in self.iter() { + converter.append(&hop.pubkey.serialize()[..])?; + converter.append(&hop.short_channel_id[..])?; + + let fee_base_msat = try_stretch( + encode_int_be_base256(hop.fee_base_msat), + 4 + ).expect("sizeof(u32) == 4"); + converter.append(&fee_base_msat)?; + + let fee_proportional_millionths = try_stretch( + encode_int_be_base256(hop.fee_proportional_millionths), + 4 + ).expect("sizeof(u32) == 4"); + converter.append(&fee_proportional_millionths)?; + + let cltv_expiry_delta = try_stretch( + encode_int_be_base256(hop.cltv_expiry_delta), + 2 + ).expect("sizeof(u16) == 2"); + converter.append(&cltv_expiry_delta)?; + } + + converter.finalize()?; + Ok(()) + } +} + +impl Base32Len for Route { + fn base32_len(&self) -> usize { + bytes_size_to_base32_size(self.0.len() * 51) + } +} + +impl ToBase32 for TaggedField { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + /// Writes a tagged field: tag, length and data. `tag` should be in `0..32` otherwise the + /// function will panic. + fn write_tagged_field(writer: &mut W, tag: u8, payload: &P) -> Result<(), W::Err> + where W: WriteBase32, + P: ToBase32 + Base32Len, + { + let len = payload.base32_len(); + assert!(len < 1024, "Every tagged field data can be at most 1023 bytes long."); + + writer.write_u5(u5::try_from_u8(tag).expect("invalid tag, not in 0..32"))?; + writer.write(&try_stretch( + encode_int_be_base32(len as u64), + 2 + ).expect("Can't be longer than 2, see assert above."))?; + payload.write_base32(writer) + } + + match *self { + TaggedField::PaymentHash(ref hash) => { + write_tagged_field(writer, constants::TAG_PAYMENT_HASH, hash) + }, + TaggedField::Description(ref description) => { + write_tagged_field(writer, constants::TAG_DESCRIPTION, description) + }, + TaggedField::PayeePubKey(ref pub_key) => { + write_tagged_field(writer, constants::TAG_PAYEE_PUB_KEY, pub_key) + }, + TaggedField::DescriptionHash(ref hash) => { + write_tagged_field(writer, constants::TAG_DESCRIPTION_HASH, hash) + }, + TaggedField::ExpiryTime(ref duration) => { + write_tagged_field(writer, constants::TAG_EXPIRY_TIME, duration) + }, + TaggedField::MinFinalCltvExpiry(ref expiry) => { + write_tagged_field(writer, constants::TAG_MIN_FINAL_CLTV_EXPIRY, expiry) + }, + TaggedField::Fallback(ref fallback_address) => { + write_tagged_field(writer, constants::TAG_FALLBACK, fallback_address) + }, + TaggedField::Route(ref route_hops) => { + write_tagged_field(writer, constants::TAG_ROUTE, route_hops) + }, + TaggedField::PaymentSecret(ref payment_secret) => { + write_tagged_field(writer, constants::TAG_PAYMENT_SECRET, payment_secret) + }, + + } + } +} + +impl ToBase32 for Signature { + fn write_base32(&self, writer: &mut W) -> Result<(), ::Err> { + let mut converter = BytesToBase32::new(writer); + let (recovery_id, signature) = self.0.serialize_compact(); + converter.append(&signature[..])?; + converter.append_u8(recovery_id.to_i32() as u8)?; + converter.finalize() + } +} + +#[cfg(test)] +mod test { + use bech32::CheckBase32; + + #[test] + fn test_currency_code() { + use Currency; + + assert_eq!("bc", Currency::Bitcoin.to_string()); + assert_eq!("tb", Currency::BitcoinTestnet.to_string()); + assert_eq!("bcrt", Currency::Regtest.to_string()); + assert_eq!("sb", Currency::Simnet.to_string()); + } + + #[test] + fn test_raw_hrp() { + use ::{Currency, RawHrp, SiPrefix}; + + let hrp = RawHrp { + currency: Currency::Bitcoin, + raw_amount: Some(100), + si_prefix: Some(SiPrefix::Micro), + }; + + assert_eq!(hrp.to_string(), "lnbc100u"); + } + + #[test] + fn test_encode_int_be_base32() { + use ser::encode_int_be_base32; + + let input: u64 = 33764; + let expected_out = CheckBase32::check_base32(&[1, 0, 31, 4]).unwrap(); + + assert_eq!(expected_out, encode_int_be_base32(input)); + } + + #[test] + fn test_encode_int_be_base256() { + use ser::encode_int_be_base256; + + let input: u64 = 16842530; + let expected_out = vec![1, 0, 255, 34]; + + assert_eq!(expected_out, encode_int_be_base256(input)); + } +} diff --git a/lightning-invoice/src/tb.rs b/lightning-invoice/src/tb.rs new file mode 100644 index 00000000..dde8a53f --- /dev/null +++ b/lightning-invoice/src/tb.rs @@ -0,0 +1,10 @@ +pub trait Bool {} + +#[derive(Copy, Clone)] +pub struct True {} + +#[derive(Copy, Clone)] +pub struct False {} + +impl Bool for True {} +impl Bool for False {} \ No newline at end of file diff --git a/lightning-invoice/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs new file mode 100644 index 00000000..403f8f1f --- /dev/null +++ b/lightning-invoice/tests/ser_de.rs @@ -0,0 +1,148 @@ +extern crate bitcoin_hashes; +extern crate lightning_invoice; +extern crate secp256k1; + +use bitcoin_hashes::hex::FromHex; +use bitcoin_hashes::sha256; +use lightning_invoice::*; +use secp256k1::Secp256k1; +use secp256k1::key::SecretKey; +use secp256k1::recovery::{RecoverableSignature, RecoveryId}; +use std::time::{Duration, UNIX_EPOCH}; + +// TODO: add more of the examples from BOLT11 and generate ones causing SemanticErrors + +fn get_test_tuples() -> Vec<(String, SignedRawInvoice, Option)> { + vec![ + ( + "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmw\ + wd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9\ + ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .description("Please consider supporting this project".to_owned()) + .build_raw() + .unwrap() + .sign(|_| { + 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(), + None + ), + ( + "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3\ + k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch\ + 9zw97j25emudupq63nyw24cg27h2rspfj9srp".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_pico_btc(2500000000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .description("1 cup coffee".to_owned()) + .expiry_time(Duration::from_secs(60)) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + & [ + 0xe8, 0x96, 0x39, 0xba, 0x68, 0x14, 0xe3, 0x66, 0x89, 0xd4, 0xb9, 0x1b, + 0xf1, 0x25, 0xf1, 0x03, 0x51, 0xb5, 0x5d, 0xa0, 0x57, 0xb0, 0x06, 0x47, + 0xa8, 0xda, 0xba, 0xeb, 0x8a, 0x90, 0xc9, 0x5f, 0x16, 0x0f, 0x9d, 0x5a, + 0x6e, 0x0f, 0x79, 0xd1, 0xfc, 0x2b, 0x96, 0x42, 0x38, 0xb9, 0x44, 0xe2, + 0xfa, 0x4a, 0xa6, 0x77, 0xc6, 0xf0, 0x20, 0xd4, 0x66, 0x47, 0x2a, 0xb8, + 0x42, 0xbd, 0x75, 0x0e + ], + RecoveryId::from_i32(1).unwrap() + ) + }).unwrap(), + None + ), + ( + "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qq\ + dhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7k\ + hhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .amount_pico_btc(20000000000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .description_hash(sha256::Hash::from_hex( + "3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1" + ).unwrap()) + .build_raw() + .unwrap() + .sign(|_| { + RecoverableSignature::from_compact( + & [ + 0xc6, 0x34, 0x86, 0xe8, 0x1f, 0x8c, 0x87, 0x8a, 0x10, 0x5b, 0xc9, 0xd9, + 0x59, 0xaf, 0x19, 0x73, 0x85, 0x4c, 0x4d, 0xc5, 0x52, 0xc4, 0xf0, 0xe0, + 0xe0, 0xc7, 0x38, 0x96, 0x03, 0xd6, 0xbd, 0xc6, 0x77, 0x07, 0xbf, 0x6b, + 0xe9, 0x92, 0xa8, 0xce, 0x7b, 0xf5, 0x00, 0x16, 0xbb, 0x41, 0xd8, 0xa9, + 0xb5, 0x35, 0x86, 0x52, 0xc4, 0x96, 0x04, 0x45, 0xa1, 0x70, 0xd0, 0x49, + 0xce, 0xd4, 0x55, 0x8c + ], + RecoveryId::from_i32(0).unwrap() + ) + }).unwrap(), + None + ), + ( + "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp59g4z52329g4z52329g4z52329g4z52329g4z52329g4z52329g4q9gkzyrw8zhfxmrcxsx7hj40yejq6lkvn75l9yjmapjv94haz8x8jy2tvmgex8rnyqkj825csd2t64fu0p4ctad2cf4tgy5gh2fns6ygp6pnc3y".to_owned(), + InvoiceBuilder::new(Currency::Bitcoin) + .payment_hash(sha256::Hash::from_hex( + "0001020304050607080900010203040506070809000102030405060708090102" + ).unwrap()) + .description("coffee beans".to_string()) + .amount_pico_btc(20000000000) + .timestamp(UNIX_EPOCH + Duration::from_secs(1496314658)) + .payment_secret(PaymentSecret([42; 32])) + .build_signed(|msg_hash| { + let privkey = SecretKey::from_slice(&[41; 32]).unwrap(); + let secp_ctx = Secp256k1::new(); + secp_ctx.sign_recoverable(msg_hash, &privkey) + }) + .unwrap() + .into_signed_raw(), + None + ) + ] +} + + +#[test] +fn serialize() { + for (serialized, deserialized, _) in get_test_tuples() { + assert_eq!(deserialized.to_string(), serialized); + } +} + +#[test] +fn deserialize() { + for (serialized, deserialized, maybe_error) in get_test_tuples() { + let parsed = serialized.parse::().unwrap(); + + assert_eq!(parsed, deserialized); + + let validated = Invoice::from_signed(parsed); + + if let Some(error) = maybe_error { + assert_eq!(Err(error), validated); + } else { + assert!(validated.is_ok()); + } + } +}