Skip to content

WIP: Bolt 11 invoices #9

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ bitcoin = "0.11"
rust-crypto = "0.2"
rand = "0.4"
secp256k1 = "0.8"
bech32 = {git = "https://github.com/sgeisler/rust-bech32"} # https://github.com/clarkmoody/rust-bech32/pull/4
chrono = "0.4"
regex = "0.2"
bit-vec = "0.4.4"
nom = "3.2.1"
error-chain = "0.11.0"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, do you really need all these deps? Seems like a lot just to save a few lines on parsing code. See-also: dep guidelines in README.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

dep guidelines in README.

Thank you for pointing that out, I probably skipped right ahead to the todo's. Getting rid of nom and bit-vec will be easy (nom looked nice at the beginning, but isn't that well documented etc. and not really of much help, I just stuck with it because it was challenging and fun). I can probably live without error-chain/failure too. Loosing regex could be a bit annoying.


[dev-dependencies]
hex = "0.3.1"
11 changes: 11 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ extern crate bitcoin;
extern crate secp256k1;
extern crate rand;
extern crate crypto;
extern crate bech32;
extern crate chrono;
extern crate regex;
extern crate bit_vec;
#[macro_use]
extern crate nom;
#[macro_use]
extern crate error_chain;

#[cfg(test)]
extern crate hex;

pub mod chain;
pub mod ln;
Expand Down
187 changes: 187 additions & 0 deletions src/ln/invoice/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use std::str::FromStr;
use std::num::ParseIntError;

use bech32;
use bech32::Bech32;

use chrono::{DateTime, Utc, Duration};

use regex::Regex;

use secp256k1;
use secp256k1::key::PublicKey;
use secp256k1::{Signature, Secp256k1};

mod parsers;

/// An Invoice for a payment on the lightning network as defined in
/// [BOLT #11](https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#examples).

#[derive(Eq, PartialEq, Debug)]
pub struct Invoice {
/// The currency deferred from the 3rd and 4th character of the bech32 transaction
pub currency: Currency,

/// The amount to pay in pico-satoshis
pub amount: Option<u64>,

pub timestamp: DateTime<Utc>,

/// tagged fields of the payment request
pub tagged: Vec<TaggedField>,

pub signature: Signature,
}

#[derive(Eq, PartialEq, Debug)]
pub enum Currency {
Bitcoin,
BitcoinTestnet,
}

#[derive(Eq, PartialEq, Debug)]
pub enum TaggedField {
PaymentHash([u8; 32]),
Description(String),
PayeePubKey(PublicKey),
DescriptionHash([u8; 32]),
ExpiryTime(Duration),
MinFinalCltvExpiry(u64),
Fallback(Fallback),
Route {
pubkey: PublicKey,
short_channel_id: u64,
fee_base_msat: i32,
fee_proportional_millionths: i32,
cltv_expiry_delta: u16,
}
}

// TODO: better types instead onf byte arrays
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just use rust-bitcoin's Script object directly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Wouldn't that have quite bad ergonomics? It forces the user to build a complete script and our library checks if it is one of the three supported types, strips away all the boilerplate/OP_CODEs and extracts the hash that the user probably had to begin with. The other way around seems to be a bit cumbersome too: how do I generate the corresponding address from the script (even worse: a to_address() function will have to return a Result because afaik not every instance of Script is encodable as some type of address)?

That's just how I understood Script from the docs, maybe I missed something.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, oops, sorry, I was just thinking the deserialize -> internal struct direction, not struct -> serialized form direction. Probably time to clean up the rust-bitcoin address stuff so that it supports more types of addresses... @apoelstra ?

#[derive(Eq, PartialEq, Debug)]
pub enum Fallback {
SegWitScript {
version: u8,
script: Vec<u8>,
},
PubKeyHash([u8; 20]),
ScriptHash([u8; 20]),
}

impl Invoice {
// TODO: maybe rewrite using nom
fn parse_hrp(hrp: &str) -> Result<(Currency, Option<u64>)> {
let re = Regex::new(r"^ln([^0-9]*)([0-9]*)([munp]?)$").unwrap();
let parts = match re.captures(&hrp) {
Some(capture_group) => capture_group,
None => return Err(ErrorKind::MalformedHRP.into())
};

let currency = parts[0].parse::<Currency>()?;

let amount = if !parts[1].is_empty() {
Some(parts[1].parse::<u64>()?)
} else {
None
};

/// `get_multiplier(x)` will only return `None` if `x` is not "m", "u", "n" or "p", which
/// due to the above regex ensures that `get_multiplier(x)` iif `x == ""`, so it's ok to
/// convert a none to 1BTC aka 10^12pBTC.
let multiplier = parts[2].chars().next().and_then(|suffix| {
get_multiplier(&suffix)
}).unwrap_or(1_000_000_000_000);

Ok((currency, amount.map(|amount| amount * multiplier)))
}
}

impl FromStr for Invoice {
type Err = Error;

fn from_str(s: &str) -> Result<Self> {
let Bech32 {hrp, data} = s.parse()?;

let (currency, amount) = Invoice::parse_hrp(&hrp)?;

Ok(Invoice {
currency,
amount,
timestamp: Utc::now(),
tagged: vec![],
signature: Signature::from_der(&Secp256k1::new(), &[0; 65])?,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be from_compact not from_der (I believe this is busted elsewhere in the codebase right now...)

})
}
}

fn get_multiplier(multiplier: &char) -> Option<u64> {
match multiplier {
&'m' => Some(1_000_000_000),
&'u' => Some(1_000_000),
&'n' => Some(1_000),
&'p' => Some(1),
_ => None
}
}

impl Currency {
pub fn get_currency_prefix(&self) -> &'static str {
match self {
&Currency::Bitcoin => "bc",
&Currency::BitcoinTestnet => "tb",
}
}

pub fn from_prefix(prefix: &str) -> Result<Currency> {
match prefix {
"bc" => Ok(Currency::Bitcoin),
"tb" => Ok(Currency::BitcoinTestnet),
_ => Err(ErrorKind::BadCurrencyPrefix.into())
}
}
}

impl FromStr for Currency {
type Err = Error;

fn from_str(s: &str) -> Result<Self> {
Currency::from_prefix(s)
}
}

error_chain! {
foreign_links {
Bech32Error(bech32::Error);
ParseIntError(ParseIntError);
MalformedSignature(secp256k1::Error);
}

errors {
BadLnPrefix {
description("The invoice did not begin with 'ln'."),
display("The invoice did not begin with 'ln'."),
}

BadCurrencyPrefix {
description("unsupported currency"),
display("unsupported currency"),
}

MalformedHRP {
description("malformed human readable part"),
display("malformed human readable part"),
}
}
}

#[cfg(test)]
mod test {
#[test]
fn test_currency_code() {
use super::Currency;
assert_eq!("bc", Currency::Bitcoin.get_currency_prefix());
assert_eq!("tb", Currency::BitcoinTestnet.get_currency_prefix());
}

// TODO: add more tests once parsers are finished
}
Loading