diff --git a/Cargo.lock b/Cargo.lock index ec19076..50608d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,11 +56,32 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" +[[package]] +name = "bip21" +version = "0.1.2" +source = "git+https://github.com/DanGould/bip21.git?rev=f74dd0f#f74dd0fb452f7d1e25f2b2d892a170f76f59f752" +dependencies = [ + "bitcoin", + "percent-encoding-rfc3986", +] + +[[package]] +name = "bip78" +version = "0.2.0-preview" +source = "git+https://github.com/dangould/rust-payjoin?branch=receive-logic-v3#ff17f1b3619eaba19c847af39bfb6b17c6c2c259" +dependencies = [ + "base64", + "bip21", + "bitcoin", + "rand", + "url", +] + [[package]] name = "bitcoin" -version = "0.27.1" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d" +checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" dependencies = [ "bech32", "bitcoin_hashes", @@ -179,6 +200,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "futures-channel" version = "0.3.19" @@ -348,6 +379,17 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.8.0" @@ -433,6 +475,7 @@ name = "loptos" version = "0.1.0" dependencies = [ "base64", + "bip78", "bitcoin", "configure_me", "configure_me_codegen", @@ -444,6 +487,7 @@ dependencies = [ "serde_json", "tokio", "tonic_lnd", + "url", ] [[package]] @@ -455,6 +499,12 @@ dependencies = [ "roff", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "memchr" version = "2.4.1" @@ -526,6 +576,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "percent-encoding-rfc3986" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3637c05577168127568a64e9dc5a6887da720efef07b3d9472d45f63ab191166" + [[package]] name = "petgraph" version = "0.5.1" @@ -785,9 +841,9 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.20.3" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" dependencies = [ "secp256k1-sys", "serde", @@ -795,9 +851,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.4.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" dependencies = [ "cc", ] @@ -880,6 +936,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "tokio" version = "1.16.1" @@ -1104,6 +1175,21 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -1122,6 +1208,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + [[package]] name = "void" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index bea4263..d8a0a05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,9 @@ spec = "config_spec.toml" test_paths = [] [dependencies] -bitcoin = { version = "0.27.0", features = ["use-serde"] } +bitcoin = { version = "0.28.1", features = ["use-serde"] } +bip78 = { git = "https://github.com/dangould/rust-payjoin", branch = "receive-logic-v3", features = ["sender", "receiver" ] } +url = "2.2.2" hyper = "0.14.9" tonic_lnd = "0.4.0" tokio = { version = "1.7.1", features = ["rt-multi-thread"] } diff --git a/src/main.rs b/src/main.rs index ed62503..e1dcbfb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::fmt; use std::sync::{Arc, Mutex}; use args::ArgError; +use bip78::receiver::*; use bitcoin::util::address::Address; use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin::{Script, TxOut}; @@ -181,7 +182,7 @@ impl From for CheckError { async fn ensure_connected(client: &mut tonic_lnd::Client, node: &P2PAddress) { let pubkey = node.node_id.to_string(); let peer_addr = - tonic_lnd::rpc::LightningAddress { pubkey: pubkey, host: node.as_host_port().to_string() }; + tonic_lnd::rpc::LightningAddress { pubkey, host: node.as_host_port().to_string() }; let connect_req = tonic_lnd::rpc::ConnectPeerRequest { addr: Some(peer_addr), perm: true, timeout: 60 }; @@ -266,6 +267,11 @@ async fn main() -> Result<(), Box> { Ok(()) } +pub(crate) struct Headers(hyper::HeaderMap); +impl bip78::receiver::Headers for Headers { + fn get_header(&self, key: &str) -> Option<&str> { self.0.get(key)?.to_str().ok() } +} + async fn handle_web_req( mut handler: Handler, req: Request, @@ -293,60 +299,79 @@ async fn handle_web_req( dbg!(req.uri().query()); let mut lnd = handler.client; - let query = req - .uri() - .query() - .into_iter() - .flat_map(|query| query.split('&')) - .map(|kv| { - let eq_pos = kv.find('=').unwrap(); - (&kv[..eq_pos], &kv[(eq_pos + 1)..]) - }) - .collect::>(); - - if query.get("disableoutputsubstitution") == Some(&"1") { - panic!("Output substitution must be enabled"); - } - let base64_bytes = hyper::body::to_bytes(req.into_body()).await?; - let bytes = base64::decode(&base64_bytes).unwrap(); - let mut reader = &*bytes; - let mut psbt = PartiallySignedTransaction::consensus_decode(&mut reader).unwrap(); - eprintln!("Received transaction: {:#?}", psbt); - for input in &mut psbt.global.unsigned_tx.input { - // clear signature - input.script_sig = bitcoin::blockdata::script::Script::new(); + + // extract original PSBT from HTTP request + // TODO: `UncheckedProposal` is not really the proposal, but the original PSBT? Is the naming wrong? + // TODO: Unneeded and/or undocumented traits in bip-78 library? (`Proposal`, `Headers`) + // TODO: We should really save the original PSBT just in case the sender does not sign + // and broadcast the proposed PSBT, or some other error occurs + let headers = Headers(req.headers().clone()); + let query = req.uri().query().map(ToString::to_string); + let body_bytes = dbg!(hyper::body::to_bytes(req.into_body()).await?); + let proposal = UncheckedProposal::from_request(&*body_bytes, query.as_deref(), headers) + .expect("received invalid proposal - TODO: handle this error properly"); + if proposal.is_output_substitution_disabled() { + panic!("Output substitution must be enabled"); // TODO: Proper checks for `pjos` } + let proposal = proposal + .assume_interactive_receive_endpoint() // TODO Check + .assume_no_inputs_owned() // TODO Check + .assume_no_mixed_input_scripts() // This check is silly and could be ignored + .assume_no_inputs_seen_before(); // TODO + + // `psbt` is going to ACTUALLY be the proposal PSBT + let mut psbt = proposal.psbt().clone(); + eprintln!("Received transaction: {:#?}", psbt); // but not yet + psbt.unsigned_tx + .input + .iter_mut() + .for_each(|txin| txin.script_sig = bitcoin::Script::new()); + + // find our output + // TODO: Would it be possible that the user sends to multiple outputs that we own? let (our_output, scheduled_payjoin) = handler .payjoins - .find(&mut psbt.global.unsigned_tx.output) + .find(&mut psbt.unsigned_tx.output) .expect("the transaction doesn't contain our output"); - let total_channel_amount: bitcoin::Amount = scheduled_payjoin + + // TODO: make this a member fn of `ScheduledPayJoin` + // TODO: make sure it is impossible to create a BIP21pj linked to `ScheduledPayJoin` + // with a "total channel amount" larger than requested + fee + let total_channel_amount = scheduled_payjoin .channels .iter() .map(|channel| channel.amount) .fold(bitcoin::Amount::ZERO, std::ops::Add::add); + + // TODO: make this a member fn of `ScheduledPayJoin` let fees = calculate_fees( scheduled_payjoin.channels.len() as u64, scheduled_payjoin.fee_rate, scheduled_payjoin.wallet_amount != bitcoin::Amount::ZERO, ); + // TODO: not great assert_eq!( our_output.value, (total_channel_amount + scheduled_payjoin.wallet_amount + fees).as_sat() ); - let chids = (0..scheduled_payjoin.channels.len()) + let chan_ids = (0..scheduled_payjoin.channels.len()) .into_iter() .map(|_| rand::random::<[u8; 32]>()) .collect::>(); + // these are channel-open txouts // no collect() because of async let mut txouts = Vec::with_capacity(scheduled_payjoin.channels.len()); - for (channel, chid) in scheduled_payjoin.channels.iter().zip(&chids) { + // TODO: Creating `OpenChannelRequest` should be one thing, and async calls another + // TODO: Instead of interfacing with `tonic_lnd` directly, consider having a trait + // so this can be extended in the future + // TODO: What if channel open fails or takes too long? + for (channel, chan_id) in scheduled_payjoin.channels.iter().zip(&chan_ids) { let psbt_shim = tonic_lnd::rpc::PsbtShim { - pending_chan_id: Vec::from(chid as &[_]), + pending_chan_id: Vec::from(chan_id as &[_]), base_psbt: Vec::new(), no_publish: true, }; @@ -383,7 +408,7 @@ async fn handle_web_req( while let Some(message) = update_stream.message().await.expect("failed to receive update") { - assert_eq!(message.pending_chan_id, chid); + assert_eq!(message.pending_chan_id, chan_id); if let Some(update) = message.update { use tonic_lnd::rpc::open_status_update::Update; match update { @@ -392,9 +417,9 @@ async fn handle_web_req( let tx = PartiallySignedTransaction::consensus_decode(&mut bytes) .unwrap(); eprintln!("PSBT received from LND: {:#?}", tx); - assert_eq!(tx.global.unsigned_tx.output.len(), 1); + assert_eq!(tx.unsigned_tx.output.len(), 1); - txouts.extend(tx.global.unsigned_tx.output); + txouts.extend(tx.unsigned_tx.output); break; } // panic? @@ -412,17 +437,17 @@ async fn handle_web_req( *our_output = channel_output; } else { our_output.value = scheduled_payjoin.wallet_amount.as_sat(); - psbt.global.unsigned_tx.output.push(channel_output) + psbt.unsigned_tx.output.push(channel_output) } - psbt.global.unsigned_tx.output.extend(txouts); - psbt.outputs.resize_with(psbt.global.unsigned_tx.output.len(), Default::default); + psbt.unsigned_tx.output.extend(txouts); + psbt.outputs.resize_with(psbt.unsigned_tx.output.len(), Default::default); eprintln!("PSBT to be given to LND: {:#?}", psbt); let mut psbt_bytes = Vec::new(); psbt.consensus_encode(&mut psbt_bytes).unwrap(); - for chid in &chids { + for chid in &chan_ids { let psbt_verify = tonic_lnd::rpc::FundingPsbtVerify { pending_chan_id: Vec::from(chid as &[_]), funded_psbt: psbt_bytes.clone(), @@ -441,7 +466,7 @@ async fn handle_web_req( } // Reset transaction state to be non-finalized - psbt = PartiallySignedTransaction::from_unsigned_tx(psbt.global.unsigned_tx) + let psbt = PartiallySignedTransaction::from_unsigned_tx(psbt.unsigned_tx.clone()) .expect("resetting tx failed"); let mut psbt_bytes = Vec::new(); eprintln!("PSBT that will be returned: {:#?}", psbt);