From 74cdd862db0686f74ccf2c89dc01ea7ff954fbd7 Mon Sep 17 00:00:00 2001 From: Orbital Date: Tue, 12 Mar 2024 21:09:31 -0500 Subject: [PATCH 1/8] offers: add timeout for invoice response --- src/lib.rs | 10 ++++++++-- src/lndk_offers.rs | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f5785327..8f76cd55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::{Mutex, Once}; use tokio::sync::mpsc::{Receiver, Sender}; -use tokio::time::{sleep, Duration}; +use tokio::time::{sleep, timeout, Duration}; use tonic_lnd::lnrpc::GetInfoRequest; use tonic_lnd::Client; use triggered::{Listener, Trigger}; @@ -240,7 +240,13 @@ impl OfferHandler { let offer_id = cfg.offer.clone().to_string(); let validated_amount = self.send_invoice_request(cfg, started).await?; - let invoice = self.wait_for_invoice().await; + let invoice = match timeout(Duration::from_secs(100), self.wait_for_invoice()).await { + Ok(invoice) => invoice, + Err(_) => { + error!("Did not receive invoice in 100 seconds."); + return Err(OfferError::InvoiceTimeout); + } + }; { let mut active_offers = self.active_offers.lock().unwrap(); active_offers.insert(offer_id.clone(), OfferState::InvoiceReceived); diff --git a/src/lndk_offers.rs b/src/lndk_offers.rs index aa105b8c..923924f3 100644 --- a/src/lndk_offers.rs +++ b/src/lndk_offers.rs @@ -59,6 +59,8 @@ pub enum OfferError { TrackFailure(Status), /// Failed to send payment. PaymentFailure, + /// Failed to receive an invoice back from offer creator before the timeout. + InvoiceTimeout, } impl Display for OfferError { @@ -82,6 +84,7 @@ impl Display for OfferError { OfferError::RouteFailure(e) => write!(f, "Error routing payment: {e:?}"), OfferError::TrackFailure(e) => write!(f, "Error tracking payment: {e:?}"), OfferError::PaymentFailure => write!(f, "Failed to send payment"), + OfferError::InvoiceTimeout => write!(f, "Did not receive invoice in 100 seconds."), } } } From 53fef9dbb40e20d7617dc4207bf6a585feb3ca05 Mon Sep 17 00:00:00 2001 From: Orbital Date: Thu, 25 Jan 2024 19:59:02 -0600 Subject: [PATCH 2/8] lnd: export network verifier for cli --- src/lnd.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lnd.rs b/src/lnd.rs index 67389d83..c548159e 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -162,7 +162,7 @@ impl<'a> NodeSigner for LndNodeSigner<'a> { #[derive(Debug)] /// Error when parsing provided configuration options. -pub(crate) enum NetworkParseError { +pub enum NetworkParseError { /// Invalid indicates an invalid network was provided. Invalid(String), } @@ -177,7 +177,7 @@ impl fmt::Display for NetworkParseError { } } -pub(crate) fn string_to_network(network_str: &str) -> Result { +pub fn string_to_network(network_str: &str) -> Result { match network_str { "mainnet" => Ok(Network::Bitcoin), "testnet" => Ok(Network::Testnet), From f946fa0b538522ccb8e1c9276c0ce57fbf0b7d8a Mon Sep 17 00:00:00 2001 From: Orbital Date: Thu, 25 Jan 2024 19:59:02 -0600 Subject: [PATCH 3/8] lnd: export get_lnd_client, features_support_onion_messages & network checker --- src/lnd.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lnd.rs b/src/lnd.rs index c548159e..d38cf024 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -26,7 +26,7 @@ const ONION_MESSAGES_REQUIRED: u32 = 38; pub(crate) const ONION_MESSAGES_OPTIONAL: u32 = 39; /// get_lnd_client connects to LND's grpc api using the config provided, blocking until a connection is established. -pub(crate) fn get_lnd_client(cfg: LndCfg) -> Result { +pub fn get_lnd_client(cfg: LndCfg) -> Result { block_on(tonic_lnd::connect(cfg.address, cfg.cert, cfg.macaroon)) } @@ -49,9 +49,7 @@ impl LndCfg { } /// features_support_onion_messages returns a boolean indicating whether a feature set supports onion messaging. -pub(crate) fn features_support_onion_messages( - features: &HashMap, -) -> bool { +pub fn features_support_onion_messages(features: &HashMap) -> bool { features.contains_key(&ONION_MESSAGES_OPTIONAL) || features.contains_key(&ONION_MESSAGES_REQUIRED) } From fe2943572bcce47f753d13f27923ccb1c135508c Mon Sep 17 00:00:00 2001 From: Orbital Date: Fri, 26 Jan 2024 16:25:07 -0600 Subject: [PATCH 4/8] lnd: convert network string to lowercase before processing --- src/lnd.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lnd.rs b/src/lnd.rs index d38cf024..0c18f447 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -176,6 +176,8 @@ impl fmt::Display for NetworkParseError { } pub fn string_to_network(network_str: &str) -> Result { + let network_lowercase = String::from(network_str).to_lowercase(); + let network_str = network_lowercase.as_str(); match network_str { "mainnet" => Ok(Network::Bitcoin), "testnet" => Ok(Network::Testnet), From bcb76c0e761944b5ee4109f47637970c955142cd Mon Sep 17 00:00:00 2001 From: Orbital Date: Fri, 26 Jan 2024 16:31:46 -0600 Subject: [PATCH 5/8] cli: add global arguments for connecting to lnd --- Cargo.toml | 2 +- src/cli.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9b4aa33d..763e730b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ spec = "config_spec.toml" [dependencies] async-trait = "0.1.66" bitcoin = { version = "0.30.2", features = ["rand"] } -clap = { version = "4.4.6", features = ["derive"] } +clap = { version = "4.4.6", features = ["derive", "string"] } futures = "0.3.26" home = "0.5.5" lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "06f9dd7", features = ["max_level_trace", "_test_utils"] } diff --git a/src/cli.rs b/src/cli.rs index 42d6c9f9..7ca725cb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,11 +1,54 @@ use clap::{Parser, Subcommand}; use lndk::lndk_offers::decode; +use std::ffi::OsString; + +fn get_cert_path_default() -> OsString { + home::home_dir() + .unwrap() + .as_path() + .join(".lnd") + .join("tls.cert") + .into_os_string() +} + +fn get_macaroon_path_default() -> OsString { + home::home_dir() + .unwrap() + .as_path() + .join(".lnd/data/chain/bitcoin/regtest/admin.macaroon") + .into_os_string() +} /// A cli for interacting with lndk. #[derive(Debug, Parser)] #[command(name = "lndk-cli")] #[command(about = "A cli for interacting with lndk", long_about = None)] struct Cli { + /// Global variables + #[arg( + short, + long, + global = true, + required = false, + default_value = "regtest" + )] + network: String, + + #[arg(short, long, global = true, required = false, default_value = get_cert_path_default())] + tls_cert: String, + + #[arg(short, long, global = true, required = false, default_value = get_macaroon_path_default())] + macaroon: String, + + #[arg( + short, + long, + global = true, + required = false, + default_value = "https://localhost:10009" + )] + address: String, + #[command(subcommand)] command: Commands, } From 0ffdeaace1a569cd3996a027b5aa0b0b5cfb8b72 Mon Sep 17 00:00:00 2001 From: Orbital Date: Sun, 28 Jan 2024 22:50:45 -0600 Subject: [PATCH 6/8] cli: Add cli command to pay offer --- src/cli.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 7ca725cb..dc730ec9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,12 @@ use clap::{Parser, Subcommand}; -use lndk::lndk_offers::decode; +use lndk::lnd::{get_lnd_client, string_to_network, LndCfg}; +use lndk::lndk_offers::{decode, get_destination}; +use lndk::{Cfg, LifecycleSignals, LndkOnionMessenger, OfferHandler, PayOfferParams}; +use log::LevelFilter; use std::ffi::OsString; +use tokio::select; +use tokio::sync::mpsc; +use tokio::sync::mpsc::{Receiver, Sender}; fn get_cert_path_default() -> OsString { home::home_dir() @@ -60,23 +66,103 @@ enum Commands { /// The offer string to decode. offer_string: String, }, + /// PayOffer pays a BOLT 12 offer, provided as a 'lno'-prefaced offer string. + PayOffer { + /// The offer string. + offer_string: String, + + /// Amount the user would like to pay. If this isn't set, we'll assume the user is paying + /// whatever the offer amount is. + #[arg(required = false)] + amount: Option, + }, } -fn main() { +#[tokio::main] +async fn main() -> Result<(), ()> { let args = Cli::parse(); match args.command { Commands::Decode { offer_string } => { println!("Decoding offer: {offer_string}."); match decode(offer_string) { - Ok(offer) => println!("Decoded offer: {:?}.", offer), + Ok(offer) => { + println!("Decoded offer: {:?}.", offer); + Ok(()) + } Err(e) => { println!( "ERROR please provide offer starting with lno. Provided offer is \ invalid, failed to decode with error: {:?}.", e - ) + ); + Err(()) } } } + Commands::PayOffer { + offer_string, + amount, + } => { + let offer = match decode(offer_string) { + Ok(offer) => offer, + Err(e) => { + println!( + "ERROR: please provide offer starting with lno. Provided offer is \ + invalid, failed to decode with error: {:?}.", + e + ); + return Err(()); + } + }; + + let destination = get_destination(&offer).await; + let network = string_to_network(&args.network).map_err(|e| { + println!("ERROR: invalid network string: {}", e); + })?; + let lnd_cfg = LndCfg::new(args.address, args.tls_cert.into(), args.macaroon.into()); + let client = get_lnd_client(lnd_cfg.clone()).map_err(|e| { + println!("ERROR: failed to connect to lnd: {}", e); + })?; + + let (shutdown, listener) = triggered::trigger(); + let (tx, rx): (Sender, Receiver) = mpsc::channel(1); + let signals = LifecycleSignals { + shutdown: shutdown.clone(), + listener, + started: tx, + }; + let lndk_cfg = Cfg { + lnd: lnd_cfg, + log_dir: None, + log_level: LevelFilter::Info, + signals, + }; + + let handler = OfferHandler::new(); + let messenger = LndkOnionMessenger::new(handler); + let pay_cfg = PayOfferParams { + offer: offer.clone(), + amount, + network, + client, + destination, + reply_path: None, + }; + select! { + _ = messenger.run(lndk_cfg) => { + println!("ERROR: lndk stopped running before pay offer finished."); + }, + res = messenger.offer_handler.pay_offer(pay_cfg, rx) => { + match res { + Ok(_) => println!("Successfully paid for offer!"), + Err(err) => println!("Error paying for offer: {err:?}"), + } + + shutdown.trigger(); + } + } + + Ok(()) + } } } From c656c4ab88074da26e9c78dc39799c0e5c32f2f6 Mon Sep 17 00:00:00 2001 From: Orbital Date: Sun, 28 Jan 2024 22:10:04 -0600 Subject: [PATCH 7/8] docs: Add instructions for paying an offer with the cli --- README.md | 8 ++++++++ docs/cli_commands.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 docs/cli_commands.md diff --git a/README.md b/README.md index 8313067b..c9277a53 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,14 @@ protocol.custom-nodeann=39 protocol.custom-init=39 ``` +#### Two options for LNDK + +Now that we have LND set up properly, there are two key things you can do with LNDK: +1) Forward onion messages. By increasing the number of Lightning nodes out there that can forward onion messages, this increases the anonymity set and helps to bootstrap BOLT 12 for more private payments. +2) Pay BOLT 12 offers, a more private standard for receiving payments over Lightning, which also allows for static invoices. + +To accomplish #1, follow the instructions below to get the LNDK binary up and running. For #2, you can [use the CLI](https://github.com/lndk-org/lndk/blob/master/docs/cli_commands.md). + #### Running LNDK Now we need to set up LNDK. To start: diff --git a/docs/cli_commands.md b/docs/cli_commands.md new file mode 100644 index 00000000..3acbdf62 --- /dev/null +++ b/docs/cli_commands.md @@ -0,0 +1,43 @@ +# LNDK-CLI + +For now, you can use `lndk-cli` separately from `lndk` meaning you don't need to have a `lndk` binary running for `lndk-cli` to work. + +## Installation + +To use the `lndk-cli` to pay a BOLT 12 offer, follow these instructions: +- To install lndk-cli: + `cargo install --bin=lndk-cli --path .` +- With the above command, `lndk-cli` will be installed to `~/.cargo/bin`. So make sure `~/.cargo/bin` is on your PATH so we can properly run `lndk-cli`. +- Run `lndk-cli -h` to make sure it's working. You'll see output similar to: + +``` +A cli for interacting with lndk + +Usage: lndk-cli [OPTIONS] + +Commands: + decode Decodes a bech32-encoded offer string into a BOLT 12 offer + pay-offer PayOffer pays a BOLT 12 offer, provided as a 'lno'-prefaced offer string + help Print this message or the help of the given subcommand(s) + +Options: + -n, --network Global variables [default: regtest] + -t, --tls-cert [default: /$HOME/.lnd/tls.cert] + -m, --macaroon [default: /$HOME/.lnd/data/chain/bitcoin/regtest/admin.macaroon] + -a, --address
[default: https://localhost:10009] + -h, --help Print help +``` + +## Commands + +Once `lndk-cli` is installed, you can use it to pay an offer. + +Since `lndk-cli` needs to connect to lnd, you'll need to provide lnd credentials to the binary. If your credentials are not in the default location, you'll need to specify them manually. + +If your credentials are in the default location, paying an offer looks like: + +`lndk-cli pay-offer ` + +If your credentials are not in the default location, an example command looks like: + +`lndk-cli -- --network=mainnet --tls-cert=/credentials/tls.cert --macaroon=/credentials/custom.macaroon --address=https://localhost:10019 pay-offer ` From 4d2d5131511d4f4806709978c63a5a1266cdc39b Mon Sep 17 00:00:00 2001 From: Orbital Date: Fri, 23 Feb 2024 16:49:36 -0600 Subject: [PATCH 8/8] docs: Add instructions for paying an Eclair offer --- docs/test_pay_offer.md | 70 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/test_pay_offer.md diff --git a/docs/test_pay_offer.md b/docs/test_pay_offer.md new file mode 100644 index 00000000..21603900 --- /dev/null +++ b/docs/test_pay_offer.md @@ -0,0 +1,70 @@ +## Eclair testing instructions + +These instructions assume you already have bitcoind and lnd nodes set up on regtest. + +The network we'll build will look like this: + +`LND --> Eclair --> Eclair2` + +Eclair2 will create an offer and we'll make sure lnd can pay the offer with the help of lndk. + +For Eclair to create an offer to pay, we need to install the tipjar plugin. Pull down this branch (https://github.com/ACINQ/eclair-plugins/pull/6/files) and build `eclair-plugins`: + +`mvn package -DskipTests` + +The resulting jar will be located at `eclair-plugins/bolt12-tip-jar/target/bolt12-tip-jar-0.9.1-SNAPSHOT.jar` + +Set up two eclair directories, for example at `~/eclair1` and `~/eclair2` with the following configuration within (at `eclair.conf`): + +``` +eclair { + chain=regtest + + server { + port=9995 + public-ips = [ "https://localhost" ] + } + + bitcoind { + rpcuser="PASSWORD" + rpcpassword="PASSWORD" + rpcport=18443 + zmqblock="tcp://127.0.0.1:28334" + zmqtx="tcp://127.0.0.1:29335" + } + + api { + enabled = true + port = 8181 + password = "testing2" + } + + features { + option_route_blinding = optional + } + + tip-jar { + description = "donation to eclair" + default-amount-msat = 100000000 // Amount to use if the invoice request does not specify an amount + max-final-expiry-delta = 1000 // How long (in blocks) the route to pay the invoice will be valid + } +} +``` + +Just: +- Set the correct bitcoind values. +- Make sure that `server.port` and `api.port` are different for each Eclair node. + +Install the dependencies for Eclair and install Eclair with `mvn package -DskipTests`. After unzipping the eclair bin (located in `target`), run two eclair nodes, each pointing to the appropriate datadirs we set above, and load in the tipjar plugin: + +`./eclair-node.sh /eclair-plugins/bolt12-tip-jar/target/bolt12-tip-jar-0.9.1-SNAPSHOT.jar -Declair.datadir="~/eclair1` + +`./eclair-node.sh ../../../../../eclair-plugins/bolt12-tip-jar/target/bolt12-tip-jar-0.9.1-SNAPSHOT.jar -Declair.datadir="~/eclair2` + +Using [Eclair's API](https://acinq.github.io/eclair), open two channels: 1) from LND to Eclair1, 2) from Eclair1 to Eclair2. Mine several blocks using bitcoind and make sure those channels are confirmed. + +Have Eclair2 create an offer with: + +`eclair-cli tipjarshowoffer` + +Then pay the offer. Run `lndk-cli pay-offer` using the instructions above.