Skip to content

Commit 65b23e6

Browse files
committed
init
1 parent 980b14c commit 65b23e6

File tree

5 files changed

+365
-3
lines changed

5 files changed

+365
-3
lines changed

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ lightning-liquidity = { version = "0.1.0-alpha", features = ["std"] }
5757

5858
bdk = { version = "0.29.0", default-features = false, features = ["std", "async-interface", "use-esplora-async", "sqlite-bundled", "keys-bip39"]}
5959

60-
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
60+
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] }
6161
rusqlite = { version = "0.28.0", features = ["bundled"] }
6262
bitcoin = "0.30.2"
6363
bip39 = "2.0.0"
@@ -69,6 +69,10 @@ tokio = { version = "1", default-features = false, features = [ "rt-multi-thread
6969
esplora-client = { version = "0.6", default-features = false }
7070
libc = "0.2"
7171
uniffi = { version = "0.26.0", features = ["build"], optional = true }
72+
axum = "0.7.4"
73+
payjoin = { version = "0.13.0", features = ["receive", "send"] }
74+
http-body-util = "0.1.0"
75+
bitcoincore-rpc = "0.17.0"
7276

7377
[target.'cfg(vss)'.dependencies]
7478
vss-client = "0.2"

src/lib.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub mod io;
8585
mod liquidity;
8686
mod logger;
8787
mod message_handler;
88+
mod payjoin;
8889
mod payment_store;
8990
mod peer_store;
9091
mod sweep;
@@ -102,6 +103,7 @@ pub use lightning_invoice;
102103
pub use error::Error as NodeError;
103104
use error::Error;
104105

106+
use ::payjoin::Uri;
105107
pub use event::Event;
106108
pub use types::ChannelConfig;
107109

@@ -151,18 +153,18 @@ use lightning_transaction_sync::EsploraSyncClient;
151153
use lightning::routing::router::{PaymentParameters, RouteParameters};
152154
use lightning_invoice::{payment, Bolt11Invoice, Currency};
153155

154-
use bitcoin::hashes::sha256::Hash as Sha256;
155156
use bitcoin::hashes::Hash;
156157
use bitcoin::secp256k1::PublicKey;
158+
use bitcoin::{hashes::sha256::Hash as Sha256, Amount};
157159

158160
use bitcoin::{Address, Network, Txid};
159161

160162
use rand::Rng;
161163

162-
use std::default::Default;
163164
use std::net::ToSocketAddrs;
164165
use std::sync::{Arc, Mutex, RwLock};
165166
use std::time::{Duration, Instant, SystemTime};
167+
use std::{default::Default, str::FromStr};
166168

167169
#[cfg(feature = "uniffi")]
168170
uniffi::include_scaffolding!("ldk_node");
@@ -794,6 +796,16 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
794796
self.runtime.read().unwrap().is_some()
795797
}
796798

799+
/// Request Payjoin Payment
800+
pub fn payjoin_uri(&self) -> Result<String, Error> {
801+
let address = self.wallet.get_new_address()?;
802+
let amount = Amount::from_sat(1000);
803+
let pj = "https://localhost:3227/payjoin";
804+
let pj_uri_string = format!("{}?amount={}&pj={}", address.to_qr_uri(), amount.to_btc(), pj);
805+
assert!(Uri::from_str(&pj_uri_string).is_ok());
806+
Ok(pj_uri_string)
807+
}
808+
797809
/// Disconnects all peers, stops all running background tasks, and shuts down [`Node`].
798810
///
799811
/// After this returns most API methods will return [`Error::NotRunning`].

src/payjoin.rs

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
*
3+
*
4+
* HTTPS SERVER
5+
*/
6+
7+
use axum::Router;
8+
9+
pub struct HttpServer;
10+
11+
impl HttpServer {
12+
pub async fn new(port: u16, router: Router) {
13+
let url = format!("0.0.0.0:{}", port);
14+
let listener = tokio::net::TcpListener::bind(url).await.unwrap();
15+
axum::serve(listener, router).await.unwrap();
16+
}
17+
}
18+
19+
/*
20+
*
21+
*
22+
* PAYJOIN RECEIVER
23+
*/
24+
25+
pub mod payjoin_receiver {
26+
use axum::extract::State;
27+
use axum::http::HeaderMap;
28+
use axum::response::IntoResponse;
29+
use axum::routing::post;
30+
use axum::{extract::Request, Router};
31+
use bitcoin::address::NetworkChecked;
32+
use bitcoin::psbt::Psbt;
33+
use bitcoin::{base64, Address};
34+
use bitcoincore_rpc::RpcApi;
35+
use http_body_util::BodyExt;
36+
use payjoin::bitcoin::{self, Amount};
37+
use payjoin::receive::{PayjoinProposal, ProvisionalProposal};
38+
use payjoin::Uri;
39+
use std::sync::Arc;
40+
use std::{collections::HashMap, str::FromStr};
41+
42+
use crate::types::Wallet;
43+
44+
use super::HttpServer;
45+
46+
struct Headers(HeaderMap);
47+
48+
impl payjoin::receive::Headers for Headers {
49+
fn get_header(&self, key: &str) -> Option<&str> {
50+
self.0.get(key).and_then(|v| v.to_str().ok())
51+
}
52+
}
53+
54+
fn build_pj_uri(
55+
address: bitcoin::Address, amount: Amount, pj: &'static str,
56+
) -> Uri<'static, NetworkChecked> {
57+
let pj_uri_string = format!("{}?amount={}&pj={}", address.to_qr_uri(), amount.to_btc(), pj);
58+
let pj_uri = Uri::from_str(&pj_uri_string).unwrap();
59+
pj_uri.assume_checked()
60+
}
61+
62+
// Payjoin receiver
63+
//
64+
// This is the code that receives a Payjoin request from a sender.
65+
//
66+
// The receiver flow is:
67+
// 1. Extracting request data
68+
// 2 Check if the Original PSBT can be broadcast
69+
// 3. Check if the sender is trying to make us sign our own inputs
70+
// 4. Check if there are mixed input scripts, breaking stenographic privacy
71+
// 5. Check if we have seen this input before
72+
// 6. Augment a valid proposal to preserve privacy
73+
// 7. Extract the payjoin PSBT and sign it
74+
// 8. Respond to the sender's http request with the signed PSBT as payload
75+
pub struct Receiver {
76+
wallet: Arc<Wallet>,
77+
}
78+
79+
impl Receiver {
80+
pub async fn handle_pj_request(
81+
State(wallet): State<Arc<Wallet>>, request: Request,
82+
) -> impl IntoResponse {
83+
// let receiver_wallet = unimplemented!();
84+
// Step 0: extract request data
85+
let (parts, body) = request.into_parts();
86+
let bytes = body.collect().await.unwrap().to_bytes();
87+
let headers = Headers(parts.headers.clone());
88+
let proposal =
89+
payjoin::receive::UncheckedProposal::from_request(&bytes[..], "", headers).unwrap();
90+
91+
let min_fee_rate = None;
92+
// Step 1: Can the Original PSBT be Broadcast?
93+
// We need to know this transaction is consensus-valid.
94+
let checked_1 =
95+
proposal.check_broadcast_suitability(min_fee_rate, |tx| Ok(true)).unwrap();
96+
// Step 2: Is the sender trying to make us sign our own inputs?
97+
let checked_2 = checked_1.check_inputs_not_owned(|input| Ok(true)).unwrap();
98+
// Step 3: Are there mixed input scripts, breaking stenographic privacy?
99+
let checked_3 = checked_2.check_no_mixed_input_scripts().unwrap();
100+
// Step 4: Have we seen this input before?
101+
//
102+
// Non-interactive i.e. payment processors should be careful to keep track
103+
// of request inputs or else a malicious sender may try and probe
104+
// multiple responses containing the receiver utxos, clustering their wallet.
105+
let checked_4 = checked_3.check_no_inputs_seen_before(|_outpoint| Ok(false)).unwrap();
106+
// Step 5. Augment a valid proposal to preserve privacy
107+
//
108+
// Here's where the PSBT is modified.
109+
// Inputs may be added to break common input ownership heurstic.
110+
// There are a number of ways to select coins and break common input heuristic but
111+
// fail to preserve privacy because of Unnecessary Input Heuristic (UIH).
112+
// Until February 2023, even BTCPay occasionally made these errors.
113+
// Privacy preserving coin selection as implemented in `try_preserving_privacy`
114+
// is precarious to implement yourself may be the most sensitive and valuable part of this kit.
115+
//
116+
// Output substitution is another way to improve privacy and increase functionality.
117+
// For example, if the Original PSBT output address paying the receiver is coming from a static URI,
118+
// a new address may be generated on the fly to avoid address reuse.
119+
// This can even be done from a watch-only wallet.
120+
// Output substitution may also be used to consolidate incoming funds to a remote cold wallet,
121+
// break an output into smaller UTXOs to fulfill exchange orders, open lightning channels, and more.
122+
//
123+
//
124+
// Using methods for coin selection not provided by this library may have dire implications for privacy.
125+
// Significant in-depth research and careful implementation iteration has
126+
// gone into privacy preserving transaction construction.
127+
let mut prov_proposal =
128+
checked_4.identify_receiver_outputs(|output_script| Ok(true)).unwrap();
129+
let unspent = wallet.list_unspent().unwrap();
130+
let _ = Self::try_contributing_inputs(&mut prov_proposal, unspent);
131+
// Select receiver payjoin inputs.
132+
let receiver_substitute_address = wallet.get_new_address().unwrap();
133+
prov_proposal.substitute_output_address(receiver_substitute_address);
134+
// Step 6. Extract the payjoin PSBT and sign it
135+
//
136+
// Fees are applied to the augmented Payjoin Proposal PSBT using calculation factoring both receiver's
137+
// preferred feerate and the sender's fee-related [optional parameters]
138+
// (https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#optional-parameters).
139+
let payjoin_proposal: PayjoinProposal = prov_proposal
140+
.finalize_proposal(
141+
|psbt: &Psbt| Ok(wallet.wallet_process_psbt(psbt).unwrap()),
142+
Some(payjoin::bitcoin::FeeRate::MIN),
143+
)
144+
.unwrap();
145+
// Step 7. Respond to the sender's http request with the signed PSBT as payload
146+
//
147+
// BIP 78 senders require specific PSBT validation constraints regulated by prepare_psbt.
148+
// PSBTv0 was not designed to support input/output modification,
149+
// so the protocol requires this precise preparation step. A future PSBTv2 payjoin protocol may not.
150+
//
151+
// It is critical to pay special care when returning error response messages.
152+
// Responding with internal errors can make a receiver vulnerable to sender probing attacks which cluster UTXOs.
153+
let payjoin_proposal_psbt = payjoin_proposal.psbt();
154+
payjoin_proposal_psbt.to_string()
155+
}
156+
157+
fn try_contributing_inputs(
158+
provisional_proposal: &mut ProvisionalProposal, unspent: Vec<bdk::LocalUtxo>,
159+
) -> Result<(), ()> {
160+
use payjoin::bitcoin::OutPoint;
161+
162+
let available_inputs = unspent;
163+
let candidate_inputs: HashMap<payjoin::bitcoin::Amount, OutPoint> = available_inputs
164+
.iter()
165+
.map(|i| {
166+
(
167+
payjoin::bitcoin::Amount::from_sat(i.txout.value),
168+
OutPoint { txid: i.outpoint.txid, vout: i.outpoint.vout },
169+
)
170+
})
171+
.collect();
172+
173+
let selected_outpoint =
174+
provisional_proposal.try_preserving_privacy(candidate_inputs).unwrap();
175+
let selected_utxo = available_inputs
176+
.iter()
177+
.find(|i| {
178+
i.outpoint.txid == selected_outpoint.txid
179+
&& i.outpoint.vout == selected_outpoint.vout
180+
})
181+
.unwrap();
182+
183+
// calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt
184+
let txo_to_contribute = payjoin::bitcoin::TxOut {
185+
value: selected_utxo.txout.value,
186+
script_pubkey: selected_utxo.txout.script_pubkey.clone(),
187+
};
188+
let outpoint_to_contribute = payjoin::bitcoin::OutPoint {
189+
txid: selected_utxo.outpoint.txid,
190+
vout: selected_utxo.outpoint.vout,
191+
};
192+
provisional_proposal
193+
.contribute_witness_input(txo_to_contribute, outpoint_to_contribute);
194+
Ok(())
195+
}
196+
}
197+
}

src/wallet.rs

+12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::logger::{log_error, log_info, log_trace, Logger};
22

33
use crate::Error;
44

5+
use bitcoin::psbt::Psbt;
56
use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator};
67

78
use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage};
@@ -166,6 +167,17 @@ where
166167
Ok(self.inner.lock().unwrap().get_balance()?)
167168
}
168169

170+
pub(crate) fn list_unspent(&self) -> Result<Vec<bdk::LocalUtxo>, Error> {
171+
Ok(self.inner.lock().unwrap().list_unspent()?)
172+
}
173+
174+
pub(crate) fn wallet_process_psbt(&self, psbt: &Psbt) -> Result<Psbt, Error> {
175+
let wallet = self.inner.lock().unwrap();
176+
let mut psbt = psbt.clone();
177+
wallet.sign(&mut psbt, SignOptions::default())?;
178+
Ok(psbt)
179+
}
180+
169181
/// Send funds to the given address.
170182
///
171183
/// If `amount_msat_or_drain` is `None` the wallet will be drained, i.e., all available funds will be

0 commit comments

Comments
 (0)