From e487ce3c4a96732bbf754342f579beb61e6aaefb Mon Sep 17 00:00:00 2001 From: jbesraa Date: Thu, 4 Apr 2024 19:31:32 +0300 Subject: [PATCH] split pkgs and test payjoin channel payment --- .gitignore | 1 + Cargo.toml | 1 + lightning-payjoin/Cargo.toml | 15 ++ lightning-payjoin/src/lib.rs | 139 ++++++++++++++ src/builder.rs | 16 +- src/channel_scheduler.rs | 200 ++++++++++++++++++++ src/event.rs | 18 +- src/lib.rs | 69 +++---- src/payjoin_handler.rs | 107 +++++++++++ src/pj_new_crate.rs | 284 ----------------------------- src/pjoin.rs | 122 ------------- src/tx_broadcaster.rs | 20 +- src/wallet.rs | 230 ++++++++++------------- tests/integration_tests_payjoin.rs | 67 ++++--- 14 files changed, 671 insertions(+), 618 deletions(-) create mode 100644 lightning-payjoin/Cargo.toml create mode 100644 lightning-payjoin/src/lib.rs create mode 100644 src/channel_scheduler.rs create mode 100644 src/payjoin_handler.rs delete mode 100644 src/pj_new_crate.rs delete mode 100644 src/pjoin.rs diff --git a/.gitignore b/.gitignore index de30b070c..471109d4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ +/lightning-payjoin/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html diff --git a/Cargo.toml b/Cargo.toml index ef0029cac..a9dadc246 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async"] } lightning-liquidity = { path = "../lightning-liquidity", features = ["std"] } +lightning-payjoin = { path = "./lightning-payjoin" } bdk = { version = "0.29.0", default-features = false, features = ["std", "async-interface", "use-esplora-async", "sqlite-bundled", "keys-bip39"]} reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] } diff --git a/lightning-payjoin/Cargo.toml b/lightning-payjoin/Cargo.toml new file mode 100644 index 000000000..89be9295e --- /dev/null +++ b/lightning-payjoin/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "lightning-payjoin" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "sync" ] } +http-body-util = "0.1.0" +hyper = {version = "1.2.0", features = ["http1", "server"]} +bytes = "1.5.0" +hyper-util = {version = "0.1.3", features = ["tokio"] } +payjoin = { version = "0.13.0", features = ["receive", "send"] } +bitcoin = "0.30.2" diff --git a/lightning-payjoin/src/lib.rs b/lightning-payjoin/src/lib.rs new file mode 100644 index 000000000..e3fbaca8c --- /dev/null +++ b/lightning-payjoin/src/lib.rs @@ -0,0 +1,139 @@ +use http_body_util::Full; +use hyper::body::Incoming; +use hyper::header::HeaderValue; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{HeaderMap, Request}; +use hyper_util::rt::TokioIo; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::net::TcpStream; +use tokio::sync::Mutex; +use tokio::task::JoinError; + +use self::utils::http_response; + +pub trait PayjoinLNReceiver { + fn convert_payjoin_request_to_funding_tx( + &self, request: Request, + ) -> impl std::future::Future>> + std::marker::Send; +} + +#[derive(Clone)] +pub struct PayjoinService { + receiver_handler: P, +} + +impl

PayjoinService

+where + P: PayjoinLNReceiver + Send + Sync + 'static + Clone, +{ + pub fn new(receiver_handler: P) -> Self { + Self { receiver_handler } + } + pub async fn serve_incoming_payjoin_requests(&self, stream: TcpStream) -> Result<(), JoinError> { + let io = TokioIo::new(stream); + let payjoin_lightning = Arc::new(Mutex::new(PayjoinService::new(self.receiver_handler.clone()))); + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .serve_connection( + io, + service_fn(move |http_request| { + Self::http_router(http_request, payjoin_lightning.clone()) + }), + ) + .await + { + println!("Error serving connection: {:?}", err); + } + }) + .await + } + async fn convert_payjoin_request_to_funding_tx( + &self, request: Request, + ) -> Result> { + self.receiver_handler.convert_payjoin_request_to_funding_tx(request).await + } + async fn http_router( + http_request: Request, payjoin_lightning: Arc>>, + ) -> Result>, hyper::Error> { + match (http_request.method(), http_request.uri().path()) { + (&hyper::Method::POST, "/payjoin") => { + let payjoin_lightning = payjoin_lightning.lock().await; + let payjoin_proposal = + payjoin_lightning.convert_payjoin_request_to_funding_tx(http_request).await.unwrap(); + return http_response(payjoin_proposal); + }, + _ => http_response("404".into()), + } + } +} + +pub mod utils { + use bitcoin::{absolute::LockTime, base64, psbt::Psbt, ScriptBuf}; + use http_body_util::Full; + use hyper::{header::HeaderValue, HeaderMap}; + + pub fn body_to_psbt(headers: HeaderMap, mut body: impl std::io::Read) -> Psbt { + let content_length = + headers.get("content-length").unwrap().to_str().unwrap().parse::().unwrap(); + let mut buf = vec![0; content_length as usize]; // 4_000_000 * 4 / 3 fits in u32 + body.read_exact(&mut buf).unwrap(); + let base64 = base64::decode(&buf).unwrap(); + let psbt = Psbt::deserialize(&base64).unwrap(); + psbt + } + + pub fn from_original_psbt_to_funding_psbt( + output_script: ScriptBuf, channel_value_sat: u64, mut psbt: Psbt, locktime: LockTime, + is_mine: impl Fn(&ScriptBuf) -> Result>, + ) -> Psbt { + let multisig_script = output_script; + psbt.unsigned_tx.lock_time = locktime; + psbt.unsigned_tx + .output + .push(bitcoin::TxOut { value: channel_value_sat, script_pubkey: multisig_script.clone() }); + psbt.unsigned_tx.output.retain(|output| { + let is_mine = is_mine(&output.script_pubkey).unwrap(); + !is_mine || output.script_pubkey == multisig_script + }); + let psbt = Psbt::from_unsigned_tx(psbt.unsigned_tx).unwrap(); + psbt + } + + pub fn amount_directed_to_us_sat( + psbt: Psbt, is_mine: impl Fn(&ScriptBuf) -> Result>, + ) -> u64 { + let mut ret = 0; + psbt.unsigned_tx.output.iter().for_each(|output| { + let is_mine = is_mine(&output.script_pubkey).unwrap(); + if is_mine { + ret += output.value; + } + }); + ret + } + pub fn http_response( + s: String, + ) -> Result>, hyper::Error> { + Ok(hyper::Response::builder().body(Full::new(bytes::Bytes::from(s))).unwrap()) + } +} + +struct RequestHeaders(HashMap); + +impl payjoin::receive::Headers for RequestHeaders { + fn get_header(&self, key: &str) -> Option<&str> { + self.0.get(key).map(|e| e.as_str()) + } +} + +impl From> for RequestHeaders { + fn from(req: HeaderMap) -> Self { + let mut h = HashMap::new(); + for (k, v) in req.iter() { + h.insert(k.to_string(), v.to_str().unwrap().to_string()); + } + RequestHeaders(h) + } +} diff --git a/src/builder.rs b/src/builder.rs index ee93389ff..a252f3958 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,3 +1,4 @@ +use crate::channel_scheduler::ChannelScheduler; use crate::config::{ Config, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, DEFAULT_ESPLORA_SERVER_URL, WALLET_KEYS_SEED_LEN, @@ -10,10 +11,9 @@ use crate::io::sqlite_store::SqliteStore; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, FilesystemLogger, Logger}; use crate::message_handler::NodeCustomMessageHandler; +use crate::payjoin_handler::PayjoinChannelManager; use crate::payment_store::PaymentStore; use crate::peer_store::PeerStore; -use crate::pj_new_crate::ChannelScheduler; -use crate::pjoin::LDKPayjoinExecuter; use crate::sweep::OutputSweeper; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ @@ -570,7 +570,7 @@ fn build_with_store_internal( let tx_broadcaster = Arc::new(TransactionBroadcaster::new( tx_sync.client().clone(), Arc::clone(&logger), - Arc::clone(&channel_scheduler) + Arc::clone(&channel_scheduler), )); let fee_estimator = Arc::new(OnchainFeeEstimator::new( tx_sync.client().clone(), @@ -589,7 +589,7 @@ fn build_with_store_internal( let tx_broadcaster = Arc::new(TransactionBroadcaster::new( tx_sync.client().clone(), Arc::clone(&logger), - Arc::clone(&channel_scheduler) + Arc::clone(&channel_scheduler), )); let fee_estimator = Arc::new(OnchainFeeEstimator::new( tx_sync.client().clone(), @@ -951,14 +951,12 @@ fn build_with_store_internal( }; let (stop_sender, _) = tokio::sync::watch::channel(()); - let payjoin_executer = LDKPayjoinExecuter::new( + let payjoin_channels_handler = PayjoinChannelManager::new( Arc::clone(&wallet), - Arc::clone(&logger), - Arc::clone(&peer_manager), Arc::clone(&channel_manager), - Arc::clone(&channel_scheduler) + Arc::clone(&channel_scheduler), ); - let payjoin = Arc::new(LDKPayjoin::new(payjoin_executer)); + let payjoin = Arc::new(LDKPayjoin::new(payjoin_channels_handler)); let is_listening = Arc::new(AtomicBool::new(false)); let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); diff --git a/src/channel_scheduler.rs b/src/channel_scheduler.rs new file mode 100644 index 000000000..86b886f70 --- /dev/null +++ b/src/channel_scheduler.rs @@ -0,0 +1,200 @@ +use bitcoin::{absolute::LockTime, secp256k1::PublicKey, ScriptBuf, Transaction, Txid}; +use lightning::ln::{msgs::SocketAddress, ChannelId}; + +#[derive(Clone)] +pub struct ChannelScheduler { + pub channels: Vec, +} + +impl ChannelScheduler { + pub(crate) fn new() -> Self { + Self { channels: vec![] } + } + pub(crate) fn schedule(&mut self, channel: ScheduledChannel) { + self.channels.push(channel); + } + pub(crate) fn channel(&self, user_channel_id: u128) -> Option { + self.channels.iter().find(|channel| channel.user_channel_id == user_channel_id).cloned() + } + pub(crate) fn add_funding_tx(&mut self, user_channel_id: u128, funding_tx: Transaction) { + if let Some(channel) = + self.channels.iter_mut().find(|channel| channel.user_channel_id == user_channel_id) + { + channel.funding_tx = Some(funding_tx); + } + } + pub(crate) fn add_funding_tx_params( + &mut self, user_channel_id: u128, funding_tx_params: FundingTxParams, + ) { + if let Some(channel) = + self.channels.iter_mut().find(|channel| channel.user_channel_id == user_channel_id) + { + channel.funding_tx_params = Some(funding_tx_params); + } + } + pub(crate) fn mark_as_funding_signed(&mut self, txid: Txid) -> bool { + if let Some(channel) = self.channels.iter_mut().find(|channel| { + let funding_tx = match &channel.funding_tx { + Some(tx) => tx, + None => return false, + }; + return funding_tx.txid() == txid; + }) { + channel.is_funding_signed = true; + true + } else { + false + } + } + pub(crate) fn is_funding_signed(&self, txid: Txid) -> bool { + if let Some(channel) = self.channels.iter().find(|channel| { + let funding_tx = match &channel.funding_tx { + Some(tx) => tx, + None => return false, + }; + funding_tx.txid() == txid + }) { + channel.is_funding_signed + } else { + false + } + } + pub(crate) fn get_next_channel(&self, channel_amount: u64) -> Option { + let channels = self + .channels + .iter() + .filter(|channel| channel.channel_value_satoshi == channel_amount) + .cloned(); + channels.min_by_key(|channel| channel.created_at) + } +} + +#[derive(Clone, Debug)] +pub struct FundingTxParams { + output_script: ScriptBuf, + locktime: LockTime, + temporary_channel_id: ChannelId, +} + +impl FundingTxParams { + pub fn new( + output_script: ScriptBuf, locktime: LockTime, temporary_channel_id: ChannelId, + ) -> Self { + Self { output_script, locktime, temporary_channel_id } + } +} + +#[derive(Clone, Debug)] +pub struct ScheduledChannel { + channel_value_satoshi: u64, + push_msat: Option, + user_channel_id: u128, + announce_channel: bool, + node_id: PublicKey, + address: SocketAddress, + funding_tx_params: Option, + funding_tx: Option, + is_funding_signed: bool, + created_at: u64, +} + +impl ScheduledChannel { + pub fn new( + channel_value_satoshi: u64, push_msat: Option, announce_channel: bool, + node_id: PublicKey, user_channel_id: u128, funding_tx_params: Option, + funding_tx: Option, address: SocketAddress, + ) -> Self { + Self { + channel_value_satoshi, + push_msat, + user_channel_id, + announce_channel, + node_id, + funding_tx_params, + funding_tx, + is_funding_signed: false, + address, + created_at: tokio::time::Instant::now().elapsed().as_secs(), + } + } + pub fn set_funding_tx(&mut self, funding_tx: Transaction) { + self.funding_tx = Some(funding_tx); + } + pub fn set_is_funding_signed(&mut self, is_funding_signed: bool) { + self.is_funding_signed = is_funding_signed; + } + pub fn channel_value_satoshi(&self) -> u64 { + self.channel_value_satoshi + } + pub fn user_channel_id(&self) -> u128 { + self.user_channel_id + } + pub fn node_id(&self) -> PublicKey { + self.node_id + } + pub fn announce_channel(&self) -> bool { + self.announce_channel + } + pub fn address(&self) -> SocketAddress { + self.address.clone() + } + pub fn push_msat(&self) -> Option { + self.push_msat + } + pub fn locktime(&self) -> Option { + match &self.funding_tx_params { + Some(params) => Some(params.locktime), + None => None, + } + } + pub fn output_script(&self) -> Option { + match &self.funding_tx_params { + Some(params) => Some(params.output_script.clone()), + None => None, + } + } + pub fn temporary_channel_id(&self) -> Option { + match &self.funding_tx_params { + Some(params) => Some(params.temporary_channel_id), + None => None, + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use bitcoin::secp256k1::{self, Secp256k1}; + use rand::Rng; + + #[tokio::test] + #[ignore] + async fn test_channel_scheduler() { + let create_pubkey = || -> PublicKey { + let secp = Secp256k1::new(); + PublicKey::from_secret_key(&secp, &secp256k1::SecretKey::from_slice(&[1; 32]).unwrap()) + }; + let channel_value_satoshi = 100; + let push_msat = None; + let announce_channel = false; + let node_id = create_pubkey(); + let user_channel_id: u128 = rand::thread_rng().gen::(); + let test_address = SocketAddress::from_str("http://test.com:8085"); + let channel = ScheduledChannel::new( + channel_value_satoshi, + push_msat, + announce_channel, + node_id, + user_channel_id, + None, + None, + test_address.unwrap(), + ); + let mut channel_scheduler = ChannelScheduler::new(); + channel_scheduler.schedule(channel.clone()); + let channels = channel_scheduler.channels; + assert_eq!(channels.len(), 1); + } +} diff --git a/src/event.rs b/src/event.rs index fd4fad334..59aebe129 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,4 +1,4 @@ -use crate::pj_new_crate::{ChannelScheduler, FundingTxParams}; +use crate::channel_scheduler::{ChannelScheduler, FundingTxParams}; use crate::types::{Sweeper, Wallet}; use crate::{ hex_utils, ChannelManager, Config, Error, NetworkGraph, PeerInfo, PeerStore, UserChannelId, @@ -319,7 +319,8 @@ where channel_manager: Arc>, output_sweeper: Arc>, network_graph: Arc, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>, - logger: L, config: Arc, channel_scheduler: Arc>, + logger: L, config: Arc, + channel_scheduler: Arc>, ) -> Self { Self { event_queue, @@ -346,7 +347,6 @@ where user_channel_id, .. } => { - dbg!("Entered FundingGenerationReady event handler"); // Construct the raw transaction with the output that is paid the amount of the // channel. let confirmation_target = ConfirmationTarget::NonAnchorChannelFee; @@ -355,21 +355,13 @@ where let cur_height = self.channel_manager.current_best_block().height(); let locktime = LockTime::from_height(cur_height).unwrap_or(LockTime::ZERO); - // payjoin scenario let mut channel_scheduler = self.channel_scheduler.lock().await; if channel_scheduler.channel(user_channel_id).is_some() { - dbg!("Entered payjoin channel scheduler scenario"); - let funding_tx_params = FundingTxParams::new( - output_script.clone().into_bytes(), - confirmation_target, - locktime, - temporary_channel_id - ); + let funding_tx_params = + FundingTxParams::new(output_script.clone(), locktime, temporary_channel_id); channel_scheduler.add_funding_tx_params(user_channel_id, funding_tx_params); - dbg!("payjoin channel scheduler scenario completed"); return {}; } - dbg!("Didnt enter payjoin channel scheduler scenario"); // Sign the final funding transaction and broadcast it. match self.wallet.create_funding_transaction( diff --git a/src/lib.rs b/src/lib.rs index 491e9ad85..6e7e6618a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,7 +89,6 @@ mod logger; mod message_handler; mod payment_store; mod peer_store; -mod pj_new_crate; mod sweep; mod tx_broadcaster; mod types; @@ -97,8 +96,8 @@ mod types; mod uniffi_types; mod wallet; -use crate::pj_new_crate::ScheduledChannel; -use crate::pjoin::LDKPayjoin; +use crate::channel_scheduler::{ChannelScheduler, ScheduledChannel}; +use crate::payjoin_handler::LDKPayjoin; pub use bitcoin; pub use lightning; pub use lightning_invoice; @@ -109,10 +108,10 @@ pub use error::Error as NodeError; use error::Error; pub use event::Event; -use pj_new_crate::ChannelScheduler; -pub use types::{BestBlock, ChannelConfig}; use payjoin::Uri; -mod pjoin; +pub use types::{BestBlock, ChannelConfig}; +mod channel_scheduler; +mod payjoin_handler; pub use io::utils::generate_entropy_mnemonic; @@ -741,31 +740,33 @@ impl Node { /// Request a new channel to be opened with a remote peer. pub async fn schedule_payjoin_channel( - &self, - channel_amount_sats: u64, - push_msat: Option, - announce_channel: bool, - node_id: PublicKey, - address: SocketAddress, + &self, channel_amount_sats: u64, push_msat: Option, announce_channel: bool, + node_id: PublicKey, address: SocketAddress, ) -> Result { let user_channel_id: u128 = rand::thread_rng().gen::(); - let channel = - ScheduledChannel::new(channel_amount_sats, push_msat, announce_channel, node_id, user_channel_id, None, None); - self.channel_scheduler.lock().await.schedule(channel); - let announce_channel = true; - self.connect_open_channel_payjoin( - node_id, - address, + let channel = ScheduledChannel::new( channel_amount_sats, + push_msat, + announce_channel, + node_id, + user_channel_id, None, None, - announce_channel, - user_channel_id - )?; // this should be stopped after `ACCEPT_CHANNEL` + address, + ); + self.channel_scheduler.lock().await.schedule(channel.clone()); + self.connect_open_scheduled_channel(channel)?; let bip21 = self.payjoin_bip21(channel_amount_sats); bip21 } + // node_id, + // address, + // channel_amount_sats, + // None, + // None, + // announce_channel, + // user_channel_id /// Generate a BIP21 URI for a payjoin request. fn payjoin_bip21(&self, amount_sats: u64) -> Result { let address = self.wallet.get_new_address()?; @@ -992,10 +993,8 @@ impl Node { } /// included `user_channel_id` in inputs - pub fn connect_open_channel_payjoin( - &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, - push_to_counterparty_msat: Option, channel_config: Option>, - announce_channel: bool, user_channel_id: u128, + pub fn connect_open_scheduled_channel( + &self, scheduled_channel: ScheduledChannel, ) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { @@ -1003,6 +1002,14 @@ impl Node { } let runtime = rt_lock.as_ref().unwrap(); + let announce_channel = scheduled_channel.announce_channel(); + let address = scheduled_channel.address(); + let node_id = scheduled_channel.node_id(); + let user_channel_id = scheduled_channel.user_channel_id(); + let channel_amount_sats = scheduled_channel.channel_value_satoshi(); + let push_to_counterparty_msat = scheduled_channel.push_msat(); + + // We dont check our balance because the this should an external funded channel. // let cur_balance = self.wallet.get_balance()?; // if cur_balance.get_spendable() < channel_amount_sats { // log_error!(self.logger, "Unable to create channel due to insufficient funds."); @@ -1024,14 +1031,13 @@ impl Node { }) })?; - let channel_config = (*(channel_config.unwrap_or_default())).clone().into(); let user_config = UserConfig { channel_handshake_limits: Default::default(), channel_handshake_config: ChannelHandshakeConfig { announced_channel: announce_channel, ..Default::default() }, - channel_config, + // channel_config, ..Default::default() }; @@ -1302,6 +1308,7 @@ impl Node { Ok(payment_hash) }, Err(e) => { + dbg!("Failed to send payment: {:?}", &e); log_error!(self.logger, "Failed to send payment: {:?}", e); match e { channelmanager::RetryableSendFailure::DuplicatePayment => { @@ -1411,6 +1418,7 @@ impl Node { }, Err(e) => { log_error!(self.logger, "Failed to send payment: {:?}", e); + dbg!("Failed to send payment: {:?}", &e); match e { channelmanager::RetryableSendFailure::DuplicatePayment => { @@ -2053,14 +2061,13 @@ async fn do_connect_peer( } } - // 1. user schedule channel // 1.1 qrcode created to scan // 2. user scan qrcode -// 2.1 node receives payjoin request +// 2.1 node receives payjoin request // 2.2 http endpoint loops for x amount of time looking for PayjoinProposal // 3. node scans if any scheduled channels waiting -// 3.1 node creates the requested channel +// 3.1 node creates the requested channel // 4. node wait for payjoin channel open requests in FundingGenerationReady state // 4.1 node creates funding tx with payjoin incoming transaction // 4.2 save in channel scheduler diff --git a/src/payjoin_handler.rs b/src/payjoin_handler.rs new file mode 100644 index 000000000..628992146 --- /dev/null +++ b/src/payjoin_handler.rs @@ -0,0 +1,107 @@ +use crate::channel_scheduler::ChannelScheduler; +use crate::types::{ChannelManager, Wallet}; +use bitcoin::ScriptBuf; +use http_body_util::BodyExt; +use hyper::body::Incoming; +use hyper::Request; +use lightning::util::persist::KVStore; +use lightning_payjoin::{PayjoinLNReceiver, PayjoinService}; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub struct PayjoinChannelManager { + channel_manager: Arc>, + channel_scheduler: Arc>, + wallet: Arc, +} + +impl Clone for PayjoinChannelManager +where + K: KVStore + Sync + Send + 'static, +{ + fn clone(&self) -> Self { + Self { + channel_manager: self.channel_manager.clone(), + wallet: self.wallet.clone(), + channel_scheduler: self.channel_scheduler.clone(), + } + } +} + +impl PayjoinChannelManager { + pub fn new( + wallet: Arc, channel_manager: Arc>, + channel_scheduler: Arc>, + ) -> Self { + Self { wallet, channel_manager, channel_scheduler } + } +} + +impl PayjoinLNReceiver for PayjoinChannelManager { + async fn convert_payjoin_request_to_funding_tx( + &self, request: Request, + ) -> Result> { + let headers = request.headers().clone(); + let _url = request.uri().path().to_string(); + let body = request.into_body().collect().await?; + let body = String::from_utf8(body.to_bytes().to_vec()).unwrap(); + let psbt = lightning_payjoin::utils::body_to_psbt(headers.clone(), body.as_bytes()); + let is_output_mine = |script: &ScriptBuf| self.wallet.is_mine(script).map_err(|e| e.into()); + let amount_to_us = + lightning_payjoin::utils::amount_directed_to_us_sat(psbt.clone(), is_output_mine); + let channel = self.channel_scheduler.lock().await.get_next_channel(amount_to_us).unwrap(); + let locktime = channel.locktime().unwrap(); + let psbt = lightning_payjoin::utils::body_to_psbt(headers.clone(), body.as_bytes()); + let psbt = lightning_payjoin::utils::from_original_psbt_to_funding_psbt( + channel.output_script().unwrap(), + channel.channel_value_satoshi(), + psbt, + locktime, + |script| self.wallet.is_mine(script).map_err(|e| e.into()), + ); + let temporary_channel_id = channel.temporary_channel_id().unwrap(); + let funding_tx = psbt.clone().extract_tx(); + self.channel_scheduler + .lock() + .await + .add_funding_tx(channel.user_channel_id(), funding_tx.clone()); + let counterparty_node_id = channel.node_id(); + let _ = self + .channel_manager + .funding_transaction_generated( + &temporary_channel_id, + &counterparty_node_id, + funding_tx.clone(), + ) + .unwrap(); + let res = tokio::time::timeout(tokio::time::Duration::from_secs(3), async move { + let txid = funding_tx.clone().txid(); + loop { + if self.channel_scheduler.lock().await.is_funding_signed(txid) { + break; + } + } + }) + .await; + if res.is_err() { + panic!("Funding tx not signed"); + // broadcast original tx + } + Ok(psbt.to_string()) + } +} + +pub struct LDKPayjoin { + inner: Arc>>>, +} + +impl LDKPayjoin { + pub fn new(handler: PayjoinChannelManager) -> Self { + let handler = PayjoinService::new(handler); + Self { inner: Arc::new(Mutex::new(handler)) } + } + + pub async fn serve(&self, stream: tokio::net::TcpStream) -> Result<(), tokio::task::JoinError> { + self.inner.lock().await.serve_incoming_payjoin_requests(stream).await + } +} diff --git a/src/pj_new_crate.rs b/src/pj_new_crate.rs deleted file mode 100644 index 577cc3d72..000000000 --- a/src/pj_new_crate.rs +++ /dev/null @@ -1,284 +0,0 @@ -/// Payjoin is a protocol for improving the privacy of Bitcoin transactions. It allows the -/// receiver of a Bitcoin payment to add inputs to the transaction, making it look like a regular -/// payment. This makes it harder for blockchain analysis to determine which transaction outputs -/// are change and which are payments. -/// -/// In Lightning Network, the payjoin protocol can be used to receive payment to your bitcoin -/// wallet and fund a channel at the same transaction. This can save on-chain fees. -/// -/// This module provides `PayjoinScheduler` and a `PayjoinExecuter` trait that can be used to -/// implement the payjoin protocol in a Lightning Network node. -use bitcoin::secp256k1::PublicKey; -use bitcoin::{Transaction, Txid}; -use http_body_util::Full; -use hyper::body::Incoming; -use hyper::server::conn::http1; -use hyper::service::service_fn; -use hyper::Request; -use hyper_util::rt::TokioIo; -use lightning::chain::chaininterface::ConfirmationTarget; -use lightning::ln::ChannelId; -use std::sync::Arc; -use tokio::net::TcpStream; -use tokio::sync::Mutex; -use tokio::task::JoinError; -use bitcoin::blockdata::locktime::absolute::LockTime; - -/// `PayjoinExecuter` is a trait that defines an interface for executing payjoin requests in -/// Lightning Network environment where it tries to create a channel with a predefined channel -/// details and use the incoming payjoin payment to fund the channel. -/// -/// `PayjoinExecuter` is used by `PayjoinScheduler` to execute on the incoming payjoin requests and -/// schedulded channels. -pub trait PayjoinExecuter { - /// The `request_to_psbt` method is called when a payjoin request is received. The method should - /// return a PSBT that is the result of the negotiation with a counterparty node after they - /// responded with FundingSigned message. - fn request_to_psbt( - &self, request: Request, - ) -> impl std::future::Future>> + std::marker::Send; -} - -#[derive(Clone, Debug)] -pub struct FundingTxParams { - pub output_script: Vec, - pub confirmation_target: ConfirmationTarget, - pub locktime: LockTime, - pub temporary_channel_id: ChannelId, -} - -impl FundingTxParams { - pub fn new(output_script: Vec, confirmation_target: ConfirmationTarget, locktime: LockTime, temporary_channel_id: ChannelId) -> Self { - Self { - output_script, - confirmation_target, - locktime, - temporary_channel_id, - } - } -} - -/// A scheduled channel is a channel that is scheduled to be created with a counterparty node. The -/// channel is opened when a payjoin request is received and the channel is funded with the -/// incoming payment. -#[derive(Clone, Debug)] -pub struct ScheduledChannel { - pub channel_value_satoshi: u64, - push_msat: Option, - user_channel_id: u128, - announce_channel: bool, - pub node_id: PublicKey, - funding_tx_params: Option, - pub funding_tx: Option, - pub is_funding_signed: bool, -} - -impl ScheduledChannel { - /// Create a new `ScheduledChannel` with the given channel details. - pub fn new( - channel_value_satoshi: u64, push_msat: Option, announce_channel: bool, - node_id: PublicKey, user_channel_id: u128, funding_tx_params: Option, funding_tx: Option - ) -> Self { - Self { channel_value_satoshi, push_msat, user_channel_id, announce_channel, node_id, funding_tx_params, funding_tx, is_funding_signed: false } - } - /// docs - pub fn set_funding_tx(&mut self, funding_tx: Transaction) { - self.funding_tx = Some(funding_tx); - } - /// docs - pub fn set_is_funding_signed(&mut self, is_funding_signed: bool) { - self.is_funding_signed = is_funding_signed; - } - /// docs - pub fn funding_tx_params(&self) -> Option { - self.funding_tx_params.clone() - } - /// Get the channel amount in satoshis. - /// - /// The channel amount is the amount that is used to fund the channel when it is created. - pub fn channel_value_satoshi(&self) -> u64 { - self.channel_value_satoshi - } - /// Get the push amount in millisatoshis. - /// - /// The push amount is the amount that is pushed to the counterparty node when the channel is - /// created. - pub fn push_msat(&self) -> Option { - self.push_msat - } - /// Get the user channel id. - pub fn user_channel_id(&self) -> u128 { - self.user_channel_id - } - /// Get the announce channel flag. - /// - /// The announce channel flag is used to determine if the channel should be announced to the - /// network when it is created. - pub fn announce_channel(&self) -> bool { - self.announce_channel - } - /// Get the node id of the counterparty node. - /// - /// The node id is the public key of the counterparty node that is used to create the channel. - pub fn node_id(&self) -> PublicKey { - self.node_id - } -} - -/// `PayjoinScheduler` is a scheduler that handles the incoming payjoin requests and the channels -/// that are to be created with the counterparty node. -/// -/// It manages a list of `ScheduledChannel` and executes on the incoming payjoin requests using the -/// `PayjoinExecuter` trait. -#[derive(Clone)] -pub struct PayjoinScheduler( - P, -); - -impl

PayjoinScheduler

-where - P: PayjoinExecuter + Send + Sync + 'static + Clone, -{ - /// Create a new `PayjoinScheduler` with the given channels and executer. - pub fn new(executer: P) -> Self { - Self(executer) - } - - /// Execute on the incoming payjoin request. - pub async fn request_to_psbt( - &self, request: Request, - ) -> Result> { - self.0.request_to_psbt(request).await - } - - /// Serve an incoming payjoin request. - /// - /// The incoming payjoin request is served using the given `TcpStream`. - /// The payjoin request is handled by the payjoin_handler function. - /// - /// The `PayjoinScheduler` is shared across multiple threads using the `Arc` and `Mutex` types. - /// And is accessible from the payjoin_handler function. - pub async fn serve(&self, stream: TcpStream) -> Result<(), JoinError> { - let io = TokioIo::new(stream); - let executer = self.0.clone(); - let payjoin_scheduler = Arc::new(Mutex::new(PayjoinScheduler::new(executer))); - tokio::task::spawn(async move { - if let Err(err) = http1::Builder::new() - .serve_connection( - io, - service_fn(move |http_request| { - payjoin_handler(http_request, payjoin_scheduler.clone()) - }), - ) - .await - { - println!("Error serving connection: {:?}", err); - } - }) - .await - } -} - -async fn payjoin_handler( - http_request: Request, pj_scheduler: Arc>>, -) -> Result>, hyper::Error> { - let make_http_response = - |s: String| -> Result>, hyper::Error> { - Ok(hyper::Response::builder().body(Full::new(bytes::Bytes::from(s))).unwrap()) - }; - match (http_request.method(), http_request.uri().path()) { - (&hyper::Method::POST, "/payjoin") => { - let scheduler = pj_scheduler.lock().await; - let res = scheduler.request_to_psbt(http_request).await.unwrap(); - return make_http_response(res); - }, - _ => make_http_response("404".into()), - } -} - -#[derive(Clone)] -pub struct ChannelScheduler{ - pub channels: Vec -} - -impl ChannelScheduler { - pub(crate) fn new() -> Self { - Self{ channels: vec![] } - } - pub(crate) fn schedule(&mut self, channel: ScheduledChannel) { - self.channels.push(channel); - } - pub(crate) fn channel(&self, user_channel_id: u128) -> Option { - self.channels.iter().find(|channel| channel.user_channel_id == user_channel_id).cloned() - } - pub(crate) fn pop(&self) -> Option { - self.channels.last().cloned() - } - pub(crate) fn add_funding_tx_params(&mut self, user_channel_id: u128, funding_tx_params: FundingTxParams) { - if let Some(channel) = self.channels.iter_mut().find(|channel| channel.user_channel_id == user_channel_id) { - channel.funding_tx_params = Some(funding_tx_params); - } - } - - pub(crate) fn mark_as_funding_signed(&mut self, txid: Txid) -> bool { - if let Some(channel) = self.channels.iter_mut().find(|channel| channel.funding_tx.as_ref().unwrap().txid() == txid) { - dbg!("Marking channel as funding signed {:?}", &channel); - channel.is_funding_signed = true; - true - } else { - false - } - } -} - -impl std::ops::Deref for ChannelScheduler { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.channels - } -} - -impl std::ops::DerefMut for ChannelScheduler { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.channels - } -} - - -#[cfg(test)] -mod tests { - use super::*; - use bitcoin::secp256k1::{self, Secp256k1}; - use rand::Rng; - - #[derive(Clone)] - struct PayjoinExecuterImpl; - - impl PayjoinExecuter for PayjoinExecuterImpl { - async fn request_to_psbt( - &self, _request: Request, - ) -> Result> { - Ok(String::new()) - } - } - - #[tokio::test] - #[ignore] - async fn test_channel_scheduler() { - let create_pubkey = || -> PublicKey { - let secp = Secp256k1::new(); - PublicKey::from_secret_key(&secp, &secp256k1::SecretKey::from_slice(&[1; 32]).unwrap()) - }; - let channel_value_satoshi = 100; - let push_msat = None; - let announce_channel = false; - let node_id = create_pubkey(); - let user_channel_id: u128 = rand::thread_rng().gen::(); - let channel = - ScheduledChannel::new(channel_value_satoshi, push_msat, announce_channel, node_id, user_channel_id, None, None); - let mut channel_scheduler = ChannelScheduler::new(); - channel_scheduler.schedule(channel.clone()); - let channels = channel_scheduler.channels; - assert_eq!(channels.len(), 1); - } -} diff --git a/src/pjoin.rs b/src/pjoin.rs deleted file mode 100644 index 48e9ac2d6..000000000 --- a/src/pjoin.rs +++ /dev/null @@ -1,122 +0,0 @@ -/// This is an implementation of the payjoin protocol using `pj_new_crate` and `payjoin` crates. -/// -/// Payjoin is used in the context of channel opening, allowing a node to fund a channel using -/// funds from an incoming payjoin request. -use hyper::{header::HeaderValue, HeaderMap, Request}; -use lightning::util::persist::KVStore; -use tokio::sync::Mutex; -use hyper::body::Incoming; -use http_body_util::BodyExt; - -use crate::{ - logger::FilesystemLogger, - pj_new_crate::{ChannelScheduler, PayjoinExecuter, PayjoinScheduler}, - types::{ChannelManager, PeerManager, Wallet}, -}; -use std::{collections::HashMap, sync::Arc}; - -pub struct LDKPayjoinExecuter { - channel_manager: Arc>, - logger: Arc, - peer_manager: Arc>, - channel_scheduler: Arc>, - wallet: Arc, -} - -impl Clone for LDKPayjoinExecuter -where - K: KVStore + Sync + Send + 'static, -{ - fn clone(&self) -> Self { - Self { - channel_manager: self.channel_manager.clone(), - logger: self.logger.clone(), - peer_manager: self.peer_manager.clone(), - wallet: self.wallet.clone(), - channel_scheduler: self.channel_scheduler.clone(), - } - } -} - -impl LDKPayjoinExecuter { - pub fn new( - wallet: Arc, logger: Arc, peer_manager: Arc>, - channel_manager: Arc>, channel_scheduler: Arc>, - ) -> Self { - Self { wallet, logger, peer_manager, channel_manager, channel_scheduler } - } -} - -struct RequestHeaders(HashMap); - -impl payjoin::receive::Headers for RequestHeaders { - fn get_header(&self, key: &str) -> Option<&str> { self.0.get(key).map(|e| e.as_str()) } -} - -impl From> for RequestHeaders { - fn from(req: HeaderMap) -> Self { - let mut h = HashMap::new(); - for (k, v) in req.iter() { - h.insert(k.to_string(), v.to_str().unwrap().to_string()); - } - RequestHeaders(h) - } -} - -impl PayjoinExecuter for LDKPayjoinExecuter { - async fn request_to_psbt( - &self, request: Request, - ) -> Result> { - let headers = request.headers().clone(); - let url = request.uri().path().to_string(); - let body = request.into_body().collect().await?; - let body = String::from_utf8(body.to_bytes().to_vec()).unwrap(); - let unchecked_proposal = payjoin::receive::UncheckedProposal::from_request( - body.as_bytes(), - &url, - RequestHeaders::from(headers) - ).unwrap(); - let mut channel = self.channel_scheduler.lock().await.pop().unwrap(); - let funding_tx_params = channel.clone().funding_tx_params().unwrap(); - let temporary_channel_id = funding_tx_params.temporary_channel_id.clone(); - let counterparty_node_id = channel.clone().node_id; - let payjoin_proposal = match self.wallet.check_incoming_payjoin_request(unchecked_proposal) { - Ok(p) => p, - Err(e) => { - dbg!(&e); - return Err(Box::new(e)); - } - }; - let psbt = payjoin_proposal.psbt(); - let funding_tx = match self.wallet.create_payjoin_funding_tx(funding_tx_params, psbt.clone()) { - Ok(tx) => tx, - Err(e) => { - dbg!(&e); - return Err(Box::new(e)); - } - }; - channel.set_funding_tx(funding_tx.clone()); - let _ = self.channel_manager.funding_transaction_generated( - &temporary_channel_id, - &counterparty_node_id, - funding_tx - ).unwrap(); - - Ok(psbt.to_string()) - } -} - -pub struct LDKPayjoin { - scheduler: Arc>>>, -} - -impl LDKPayjoin { - pub fn new(executer: LDKPayjoinExecuter) -> Self { - let payjoin_scheduler = PayjoinScheduler::new(executer); - Self { scheduler: Arc::new(Mutex::new(payjoin_scheduler)) } - } - - pub async fn serve(&self, stream: tokio::net::TcpStream) -> Result<(), tokio::task::JoinError> { - self.scheduler.lock().await.serve(stream).await - } -} diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 4c65e03d0..c9a8b0e17 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -1,5 +1,5 @@ +use crate::channel_scheduler::ChannelScheduler; use crate::logger::{log_bytes, log_debug, log_error, log_trace, Logger}; -use crate::pj_new_crate::ChannelScheduler; use lightning::chain::chaininterface::BroadcasterInterface; use lightning::util::ser::Writeable; @@ -32,9 +32,17 @@ impl TransactionBroadcaster where L::Target: Logger, { - pub(crate) fn new(esplora_client: EsploraClient, logger: L, channel_scheduler: Arc>) -> Self { + pub(crate) fn new( + esplora_client: EsploraClient, logger: L, channel_scheduler: Arc>, + ) -> Self { let (queue_sender, queue_receiver) = mpsc::channel(BCAST_PACKAGE_QUEUE_SIZE); - Self { queue_sender, queue_receiver: Mutex::new(queue_receiver), esplora_client, logger, channel_scheduler } + Self { + queue_sender, + queue_receiver: Mutex::new(queue_receiver), + esplora_client, + logger, + channel_scheduler, + } } pub(crate) async fn process_queue(&self) { @@ -46,7 +54,11 @@ where } else { match self.esplora_client.broadcast(tx).await { Ok(()) => { - log_trace!(self.logger, "Successfully broadcast transaction {}", tx.txid()); + log_trace!( + self.logger, + "Successfully broadcast transaction {}", + tx.txid() + ); }, Err(e) => match e { esplora_client::Error::Reqwest(_) => { diff --git a/src/wallet.rs b/src/wallet.rs index 41fb9ad32..8d9c79843 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,9 +1,7 @@ use crate::logger::{log_error, log_info, log_trace, Logger}; -use crate::pj_new_crate::{FundingTxParams, ScheduledChannel}; use crate::Error; -use bitcoin::psbt::Psbt; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; @@ -26,10 +24,8 @@ use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; -use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid}; -use payjoin::receive::{PayjoinProposal, UncheckedProposal}; +use bitcoin::{ScriptBuf, Transaction, TxOut, Txid}; -use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Condvar, Mutex}; use std::time::Duration; @@ -118,132 +114,108 @@ where res } - pub(crate) fn create_payjoin_funding_tx(&self, funding_tx_params: FundingTxParams, mut psbt: Psbt) -> Result { - let _fee_rate = FeeRate::from_sat_per_kwu( - self.fee_estimator.get_est_sat_per_1000_weight(ConfirmationTarget::NonAnchorChannelFee) as f32, - ); - let locked_wallet = self.inner.lock().unwrap(); - psbt.unsigned_tx.lock_time = funding_tx_params.locktime.into(); - let out_put = bitcoin::TxOut { - value: 100000, - script_pubkey: ScriptBuf::from_bytes(funding_tx_params.output_script.clone()), - }; - psbt.unsigned_tx.output.push(out_put); - psbt.unsigned_tx.output.retain(|output| { - !locked_wallet.is_mine(&output.script_pubkey).unwrap() - || ( - locked_wallet.is_mine(&output.script_pubkey).unwrap() && - output.script_pubkey == ScriptBuf::from_bytes(funding_tx_params.output_script.clone()) - ) - }); - dbg!(&psbt); - let mut default_sign_options = SignOptions::default(); - default_sign_options.try_finalize = false; - default_sign_options.trust_witness_utxo = true; - match locked_wallet.sign(&mut psbt, default_sign_options) { - Ok(finalized) => { - if !finalized { - dbg!("Not finalized"); - // return Err(Error::OnchainTxCreationFailed); - } else { - dbg!("(shouldnt happen) finalized"); - } - }, - Err(err) => { - dbg!(&err); - return Err(err.into()); - }, - } - - Ok(psbt.extract_tx()) + pub(crate) fn is_mine(&self, script: &ScriptBuf) -> Result { + Ok(self.inner.lock().unwrap().is_mine(script)?) } - pub(crate) fn check_incoming_payjoin_request(&self, proposal: UncheckedProposal) -> Result { - dbg!("entered check_incoming_payjoin_request"); - let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast(); - dbg!("entered check_incoming_payjoin_request 1"); - let receiver = self.inner.lock().unwrap(); - dbg!("entered check_incoming_payjoin_request 2"); - - // Receive Check 1: Can Broadcast - let proposal = proposal - .check_broadcast_suitability(None, |_tx| { - Ok(true) - }) - .expect("Payjoin proposal should be broadcastable"); - dbg!("entered check_incoming_payjoin_request 3"); - - // Receive Check 2: receiver can't sign for proposal inputs - let proposal = proposal - .check_inputs_not_owned(|input| { - Ok(receiver.is_mine(&input).unwrap()) - }) - .expect("Receiver should not own any of the inputs"); - dbg!("entered check_incoming_payjoin_request 4"); - - // Receive Check 3: receiver can't sign for proposal inputs - let proposal = proposal.check_no_mixed_input_scripts().unwrap(); - dbg!("entered check_incoming_payjoin_request 5"); - - // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. - let payjoin_proposal = proposal - .check_no_inputs_seen_before(|_| Ok(false)) - .unwrap() - .identify_receiver_outputs(|output_script| { - dbg!(&output_script); - let is_mine = receiver.is_mine(&output_script).unwrap(); - dbg!(&is_mine); - Ok(is_mine) - }) - .expect("Receiver should have at least one output"); - dbg!("entered check_incoming_payjoin_request 6"); - - // Select receiver payjoin inputs. TODO Lock them. - // let available_inputs = receiver.list_unspent().unwrap(); - // let candidate_inputs: HashMap = available_inputs - // .iter() - // .map(|i| (Amount::from_sat(i.txout.value), OutPoint { txid: i.outpoint.txid, vout: i.outpoint.vout })) - // .collect(); - - // let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); - // let selected_utxo = available_inputs - // .iter() - // .find(|i| i.outpoint.txid == selected_outpoint.txid && i.outpoint.vout == selected_outpoint.vout) - // .unwrap(); - - // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, - // let txo_to_contribute = bitcoin::TxOut { - // value: selected_utxo.txout.value, - // script_pubkey: selected_utxo.txout.script_pubkey.clone() - // }; - // let outpoint_to_contribute = - // bitcoin::OutPoint { txid: selected_utxo.outpoint.txid, vout: selected_utxo.outpoint.vout }; - // payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); - - // let receiver_substitute_address = self.get_new_address().unwrap(); - // payjoin.substitute_output_address(receiver_substitute_address); - let payjoin_proposal = match payjoin_proposal.finalize_proposal( - |psbt: &Psbt| { - dbg!("entered check_incoming_payjoin_request 6.0"); - if receiver.sign(&mut psbt.clone(), SignOptions::default()).unwrap() { - dbg!("entered check_incoming_payjoin_request 6.1"); - Ok(psbt.clone()) - } else { - dbg!("entered check_incoming_payjoin_request 6.2"); - panic!("Not able to sign our payjoin proposal psbt") - } - }, - Some(bitcoin::FeeRate::MIN), - ) { - Ok(p) => p, - Err(e) => { - dbg!(&e); - panic!("Not able to finalize our payjoin proposal") - }, - }; - dbg!("entered check_incoming_payjoin_request 7"); - Ok(payjoin_proposal) - } + //pub(crate) fn check_incoming_payjoin_request(&self, proposal: UncheckedProposal) -> Result { + // let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast(); + // let receiver = self.inner.lock().unwrap(); + // // Receive Check 1: Can Broadcast + // let proposal = proposal + // .check_broadcast_suitability(None, |_tx| { + // Ok(true) + // }) + // .expect("Payjoin proposal should be broadcastable"); + // // Receive Check 2: receiver can't sign for proposal inputs + // let proposal = proposal + // .check_inputs_not_owned(|input| { + // Ok(receiver.is_mine(&input).unwrap()) + // }) + // .expect("Receiver should not own any of the inputs"); + // // Receive Check 3: receiver can't sign for proposal inputs + // let proposal = proposal.check_no_mixed_input_scripts().unwrap(); + // // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. + // let payjoin_proposal = proposal + // .check_no_inputs_seen_before(|_| Ok(false)) + // .unwrap() + // .identify_receiver_outputs(|output_script| { + // dbg!(&output_script); + // let is_mine = receiver.is_mine(&output_script).unwrap(); + // dbg!(&is_mine); + // Ok(is_mine) + // }) + // .expect("Receiver should have at least one output"); + + // let _fee_rate = FeeRate::from_sat_per_kwu( + // self.fee_estimator.get_est_sat_per_1000_weight(ConfirmationTarget::NonAnchorChannelFee) as f32, + // ); + // // let mut psbt = payjoin_proposal.psbt(); + // // let psbt = payjoin_proposal + // // psbt.unsigned_tx.lock_time = funding_tx_params.locktime.into(); + // // let out_put = bitcoin::TxOut { + // // value: 100000, + // // script_pubkey: ScriptBuf::from_bytes(funding_tx_params.output_script.clone()), + // // }; + // // psbt.unsigned_tx.output.push(out_put); + // // psbt.unsigned_tx.output.retain(|output| { + // // !locked_wallet.is_mine(&output.script_pubkey).unwrap() + // // || ( + // // locked_wallet.is_mine(&output.script_pubkey).unwrap() && + // // output.script_pubkey == ScriptBuf::from_bytes(funding_tx_params.output_script.clone()) + // // ) + // // }); + // // + // // Ok(psbt.extract_tx()) + // // end + // // + + // // Select receiver payjoin inputs. TODO Lock them. + // // let available_inputs = receiver.list_unspent().unwrap(); + // // let candidate_inputs: HashMap = available_inputs + // // .iter() + // // .map(|i| (Amount::from_sat(i.txout.value), OutPoint { txid: i.outpoint.txid, vout: i.outpoint.vout })) + // // .collect(); + + // // let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); + // // let selected_utxo = available_inputs + // // .iter() + // // .find(|i| i.outpoint.txid == selected_outpoint.txid && i.outpoint.vout == selected_outpoint.vout) + // // .unwrap(); + + // // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, + // // let funding_output = bitcoin::TxOut { + // // value: 100000, + // // script_pubkey: ScriptBuf::from_bytes(funding_tx_params.output_script.clone()), + // // }; + // // let outpoint_to_contribute = + // // bitcoin::OutPoint { txid: selected_utxo.outpoint.txid, vout: selected_utxo.outpoint.vout }; + // // payjoin_proposal.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); + + // // let receiver_substitute_address = self.get_new_address().unwrap(); + // // payjoin.substitute_output_address(receiver_substitute_address); + // let payjoin_proposal = match payjoin_proposal.finalize_proposal( + // |psbt: &Psbt| { + // dbg!("entered check_incoming_payjoin_request 6.0"); + // if receiver.sign(&mut psbt.clone(), SignOptions::default()).unwrap() { + // dbg!("entered check_incoming_payjoin_request 6.1"); + // Ok(psbt.clone()) + // } else { + // dbg!("entered check_incoming_payjoin_request 6.2"); + // panic!("Not able to sign our payjoin proposal psbt") + // } + // }, + // Some(bitcoin::FeeRate::MIN), + // ) { + // Ok(p) => p, + // Err(e) => { + // dbg!(&e); + // panic!("Not able to finalize our payjoin proposal") + // }, + // }; + // dbg!("entered check_incoming_payjoin_request 7"); + // Ok(payjoin_proposal.psbt().clone()) + //} pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget, diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs index 509984e87..cb826db1f 100644 --- a/tests/integration_tests_payjoin.rs +++ b/tests/integration_tests_payjoin.rs @@ -1,13 +1,17 @@ mod common; -use crate::common::{generate_blocks_and_wait, premine_and_distribute_funds, random_config, setup_node, setup_two_nodes}; -use bitcoin::Amount; +use crate::common::{ + expect_channel_pending_event, expect_channel_ready_event, generate_blocks_and_wait, + premine_and_distribute_funds, setup_two_nodes, wait_for_tx, +}; +use bitcoin::{Amount, Txid}; use bitcoincore_rpc::{Client as BitcoindClient, RpcApi}; use common::setup_bitcoind_and_electrsd; -use lightning::ln::msgs::SocketAddress; +use ldk_node::Event; mod mock_payjoin_sender { use bitcoin::base64; -use bitcoincore_rpc::Client as BitcoindClient; + use bitcoin::Txid; + use bitcoincore_rpc::Client as BitcoindClient; use bitcoincore_rpc::RpcApi; use payjoin::bitcoin::address::NetworkChecked; use std::collections::HashMap; @@ -18,7 +22,7 @@ use bitcoincore_rpc::Client as BitcoindClient; pub fn try_payjoin( sender_wallet: &BitcoindClient, pj_uri: payjoin::Uri<'static, NetworkChecked>, - ) -> (String, String) { + ) -> (String, Txid) { // Step 1. Extract the parameters from the payjoin URI let amount_to_send = pj_uri.amount.unwrap(); let receiver_address = pj_uri.address.clone(); @@ -43,7 +47,7 @@ use bitcoincore_rpc::Client as BitcoindClient; sender_wallet.wallet_process_psbt(&sender_psbt.psbt, None, None, None).unwrap().psbt; let psbt = Psbt::from_str(&psbt).unwrap(); // Step 4. Construct the request with the PSBT and parameters - let (req, ctx) = RequestBuilder::from_psbt_and_uri(psbt.clone(), pj_uri) + let (req, _ctx) = RequestBuilder::from_psbt_and_uri(psbt.clone(), pj_uri) .unwrap() .build_with_additional_fee( bitcoincore_rpc::bitcoin::Amount::from_sat(1), @@ -55,8 +59,6 @@ use bitcoincore_rpc::Client as BitcoindClient; .extract_v1() .unwrap(); // Step 5. Send the request and receive response - // let payjoin_url = pj_uri.extras.e - // BITCOIN:BCRT1Q0S724W239Z2XQGSZV6TE96HMYLEDCTX3GDFZEP?amount=0.01&pj=https://localhost:3000 let url_http = req.url.as_str().replace("https", "http"); let res = reqwest::blocking::Client::new(); let res = res @@ -66,11 +68,12 @@ use bitcoincore_rpc::Client as BitcoindClient; .send() .unwrap(); let res = res.text().unwrap(); + let psbt = Psbt::deserialize(&base64::decode(res.clone()).unwrap()).unwrap(); // Step 6. Process the response // // An `Ok` response should include a Payjoin Proposal PSBT. // Check that it's signed, following protocol, not trying to steal or otherwise error. - let psbt = ctx.process_response(&mut res.as_bytes()).unwrap(); + // let psbt = ctx.process_response(&mut res.as_bytes()).unwrap(); //// Step 7. Sign and finalize the Payjoin Proposal PSBT //// //// Most software can handle adding the last signatures to a PSBT without issue. @@ -81,9 +84,7 @@ use bitcoincore_rpc::Client as BitcoindClient; let tx = sender_wallet.finalize_psbt(&psbt, Some(true)).unwrap().hex.unwrap(); //// Step 8. Broadcast the Payjoin Transaction let txid = sender_wallet.send_raw_transaction(&tx).unwrap(); - dbg!(&txid); - //txid - (res, txid.to_string()) + (res, txid) } } @@ -91,7 +92,7 @@ use bitcoincore_rpc::Client as BitcoindClient; fn payjoin() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let payjoin_sender_wallet: BitcoindClient = bitcoind.create_wallet("payjoin_sender").unwrap(); - let (node_a, node_b) = setup_two_nodes(&electrsd, true); + let (node_a, node_b) = setup_two_nodes(&electrsd, false); let addr_a = node_a.new_onchain_address().unwrap(); let addr_sender = payjoin_sender_wallet.get_new_address(None, None).unwrap().assume_checked(); let premine_amount_sat = 100_000_00; @@ -110,33 +111,47 @@ fn payjoin() { assert_eq!(node_a.next_event(), None); assert_eq!(node_a.list_channels().len(), 0); - let funding_amount_sat = 100_000; + let funding_amount_sat = 80_000; let pj_uri = tokio::runtime::Runtime::new().unwrap().handle().block_on(async { - let pj_uri = node_a.schedule_payjoin_channel( - funding_amount_sat, - None, - false, - node_b.node_id(), - node_b.listening_addresses().unwrap().get(0).unwrap().clone()).await; + let pj_uri = node_a + .schedule_payjoin_channel( + funding_amount_sat, + None, + false, + node_b.node_id(), + node_b.listening_addresses().unwrap().get(0).unwrap().clone(), + ) + .await; pj_uri }); // sleep for 1 seconds std::thread::sleep(std::time::Duration::from_secs(1)); - let (_, _txid) = mock_payjoin_sender::try_payjoin( + let (_, txid) = mock_payjoin_sender::try_payjoin( &payjoin_sender_wallet, payjoin::Uri::try_from(pj_uri.unwrap()).unwrap().assume_checked(), ); - // generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 8); + expect_channel_pending_event!(node_a, node_b.node_id()); + expect_channel_pending_event!(node_b, node_a.node_id()); + wait_for_tx(&electrsd.client, Txid::from(txid)); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); let a = node_a.list_channels(); let channel = a.get(0).unwrap(); - dbg!(&channel); - // Bug: Txid saved on our side is not the same as the one on the sender side. Do we need a - // different way to track the transaction? - // assert_eq!(txid, channel.funding_txo.unwrap().txid.to_string()); + assert_eq!(txid, channel.funding_txo.unwrap().txid); assert_eq!(channel.channel_value_sats, funding_amount_sat); + assert_eq!(channel.confirmations.unwrap(), 6); assert!(channel.is_channel_ready); assert!(channel.is_usable); + + assert_eq!(node_a.list_peers().get(0).unwrap().is_connected, true); + assert_eq!(node_a.list_peers().get(0).unwrap().is_persisted, true); + assert_eq!(node_a.list_peers().get(0).unwrap().node_id, node_b.node_id()); + + let invoice_amount_1_msat = 2500_000; + let invoice = node_b.receive_payment(invoice_amount_1_msat, "test", 1000).unwrap(); + assert!(node_a.send_payment(&invoice).is_ok()); }