Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 22b2e82

Browse files
committedMay 29, 2024··
Add ability to receive payjoin transactions
Allows the node wallet to receive payjoin transactions as specified in BIP78.
1 parent cbcbdd7 commit 22b2e82

12 files changed

+771
-19
lines changed
 

‎Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ bdk = { version = "0.29.0", default-features = false, features = ["std", "async-
5959

6060
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
6161
rusqlite = { version = "0.28.0", features = ["bundled"] }
62-
bitcoin = "0.30.2"
62+
bitcoin = { version = "0.30.2", features = ["bitcoinconsensus"] }
6363
bip39 = "2.0.0"
6464

6565
rand = "0.8.5"

‎bindings/ldk_node.udl

+3
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ enum NodeError {
157157
"PayjoinRequestCreationFailed",
158158
"PayjoinResponseProcessingFailed",
159159
"PayjoinRequestTimeout",
160+
"PayjoinReceiverUnavailable",
161+
"PayjoinReceiverRequestValidationFailed",
162+
"PayjoinReceiverEnrollementFailed"
160163
};
161164

162165
dictionary NodeStatus {

‎src/builder.rs

+74-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::io::sqlite_store::SqliteStore;
1111
use crate::liquidity::LiquiditySource;
1212
use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
1313
use crate::message_handler::NodeCustomMessageHandler;
14+
use crate::payjoin_receiver::PayjoinReceiver;
1415
use crate::payment::store::PaymentStore;
1516
use crate::peer_store::PeerStore;
1617
use crate::tx_broadcaster::TransactionBroadcaster;
@@ -99,6 +100,13 @@ struct PayjoinSenderConfig {
99100
payjoin_relay: String,
100101
}
101102

103+
#[derive(Debug, Clone)]
104+
struct PayjoinReceiverConfig {
105+
payjoin_relay: String,
106+
payjoin_directory: String,
107+
ohttp_keys: Option<String>,
108+
}
109+
102110
impl Default for LiquiditySourceConfig {
103111
fn default() -> Self {
104112
Self { lsps2_service: None }
@@ -179,6 +187,7 @@ pub struct NodeBuilder {
179187
gossip_source_config: Option<GossipSourceConfig>,
180188
liquidity_source_config: Option<LiquiditySourceConfig>,
181189
payjoin_sender_config: Option<PayjoinSenderConfig>,
190+
payjoin_receiver_config: Option<PayjoinReceiverConfig>,
182191
}
183192

184193
impl NodeBuilder {
@@ -195,13 +204,15 @@ impl NodeBuilder {
195204
let gossip_source_config = None;
196205
let liquidity_source_config = None;
197206
let payjoin_sender_config = None;
207+
let payjoin_receiver_config = None;
198208
Self {
199209
config,
200210
entropy_source_config,
201211
chain_data_source_config,
202212
gossip_source_config,
203213
liquidity_source_config,
204214
payjoin_sender_config,
215+
payjoin_receiver_config,
205216
}
206217
}
207218

@@ -262,6 +273,15 @@ impl NodeBuilder {
262273
self
263274
}
264275

276+
/// Configures the [`Node`] instance to enable receiving payjoin transactions.
277+
pub fn set_payjoin_receiver_config(
278+
&mut self, payjoin_relay: String, payjoin_directory: String, ohttp_keys: Option<String>,
279+
) -> &mut Self {
280+
self.payjoin_receiver_config =
281+
Some(PayjoinReceiverConfig { payjoin_relay, payjoin_directory, ohttp_keys });
282+
self
283+
}
284+
265285
/// Configures the [`Node`] instance to source its inbound liquidity from the given
266286
/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
267287
/// service.
@@ -381,6 +401,7 @@ impl NodeBuilder {
381401
self.gossip_source_config.as_ref(),
382402
self.liquidity_source_config.as_ref(),
383403
self.payjoin_sender_config.as_ref(),
404+
self.payjoin_receiver_config.as_ref(),
384405
seed_bytes,
385406
logger,
386407
vss_store,
@@ -403,6 +424,7 @@ impl NodeBuilder {
403424
self.gossip_source_config.as_ref(),
404425
self.liquidity_source_config.as_ref(),
405426
self.payjoin_sender_config.as_ref(),
427+
self.payjoin_receiver_config.as_ref(),
406428
seed_bytes,
407429
logger,
408430
kv_store,
@@ -475,6 +497,17 @@ impl ArcedNodeBuilder {
475497
self.inner.write().unwrap().set_payjoin_sender_config(payjoin_relay);
476498
}
477499

500+
/// Configures the [`Node`] instance to enable receiving payjoin transactions.
501+
pub fn set_payjoin_receiver_config(
502+
&mut self, payjoin_relay: String, payjoin_directory: String, ohttp_keys: Option<String>,
503+
) {
504+
self.inner.write().unwrap().set_payjoin_receiver_config(
505+
payjoin_relay,
506+
payjoin_directory,
507+
ohttp_keys,
508+
);
509+
}
510+
478511
/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
479512
/// server.
480513
pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
@@ -544,7 +577,8 @@ fn build_with_store_internal(
544577
config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
545578
gossip_source_config: Option<&GossipSourceConfig>,
546579
liquidity_source_config: Option<&LiquiditySourceConfig>,
547-
payjoin_sender_config: Option<&PayjoinSenderConfig>, seed_bytes: [u8; 64],
580+
payjoin_sender_config: Option<&PayjoinSenderConfig>,
581+
payjoin_receiver_config: Option<&PayjoinReceiverConfig>, seed_bytes: [u8; 64],
548582
logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
549583
) -> Result<Node, BuildError> {
550584
// Initialize the on-chain wallet and chain access
@@ -1010,6 +1044,44 @@ fn build_with_store_internal(
10101044
}
10111045
});
10121046

1047+
let payjoin_receiver = payjoin_receiver_config.as_ref().and_then(|prc| {
1048+
match (payjoin::Url::parse(&prc.payjoin_directory), payjoin::Url::parse(&prc.payjoin_relay))
1049+
{
1050+
(Ok(directory), Ok(relay)) => {
1051+
let ohttp_keys = match prc.ohttp_keys.clone() {
1052+
Some(keys) => {
1053+
let keys = match bitcoin::base64::decode(keys) {
1054+
Ok(keys) => keys,
1055+
Err(e) => {
1056+
log_info!(logger, "Failed to decode ohttp keys: the provided key is not a valid Base64 string {}", e);
1057+
return None;
1058+
},
1059+
};
1060+
match payjoin::OhttpKeys::decode(&keys) {
1061+
Ok(ohttp_keys) => Some(ohttp_keys),
1062+
Err(e) => {
1063+
log_info!(logger, "Failed to decode ohttp keys, make sure you provided a valid Ohttp Key as provided by the payjoin directory: {}", e);
1064+
return None;
1065+
},
1066+
}
1067+
},
1068+
None => None,
1069+
};
1070+
Some(Arc::new(PayjoinReceiver::new(
1071+
Arc::clone(&logger),
1072+
Arc::clone(&wallet),
1073+
directory,
1074+
relay,
1075+
ohttp_keys,
1076+
)))
1077+
},
1078+
_ => {
1079+
log_info!(logger, "The provided payjoin relay URL is invalid.");
1080+
None
1081+
},
1082+
}
1083+
});
1084+
10131085
let is_listening = Arc::new(AtomicBool::new(false));
10141086
let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
10151087
let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -1030,6 +1102,7 @@ fn build_with_store_internal(
10301102
chain_monitor,
10311103
output_sweeper,
10321104
payjoin_sender,
1105+
payjoin_receiver,
10331106
peer_manager,
10341107
connection_manager,
10351108
keys_manager,

‎src/error.rs

+15
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ pub enum Error {
8383
PayjoinResponseProcessingFailed,
8484
/// Payjoin request timed out.
8585
PayjoinRequestTimeout,
86+
/// Failed to access payjoin receiver object.
87+
PayjoinReceiverUnavailable,
88+
/// Failed to enroll payjoin receiver.
89+
PayjoinReceiverEnrollementFailed,
90+
/// Failed to validate an incoming payjoin request.
91+
PayjoinReceiverRequestValidationFailed,
8692
}
8793

8894
impl fmt::Display for Error {
@@ -152,6 +158,15 @@ impl fmt::Display for Error {
152158
Self::PayjoinRequestTimeout => {
153159
write!(f, "Payjoin receiver did not respond to our request within the timeout period. Notice they can still broadcast the original PSBT we shared with them")
154160
},
161+
Self::PayjoinReceiverUnavailable => {
162+
write!(f, "Failed to access payjoin receiver object. Make sure you have enabled Payjoin receiving support.")
163+
},
164+
Self::PayjoinReceiverRequestValidationFailed => {
165+
write!(f, "Failed to validate an incoming payjoin request. Payjoin sender request didnt pass the payjoin validation steps.")
166+
},
167+
Self::PayjoinReceiverEnrollementFailed => {
168+
write!(f, "Failed to enroll payjoin receiver. Make sure the configured Payjoin directory & Payjoin relay are available.")
169+
},
155170
}
156171
}
157172
}

‎src/io/utils.rs

+7
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,13 @@ pub(crate) fn check_namespace_key_validity(
511511
Ok(())
512512
}
513513

514+
pub(crate) fn ohttp_headers() -> reqwest::header::HeaderMap<reqwest::header::HeaderValue> {
515+
let mut headers = reqwest::header::HeaderMap::new();
516+
let header_value = reqwest::header::HeaderValue::from_static("message/ohttp-req");
517+
headers.insert(reqwest::header::CONTENT_TYPE, header_value);
518+
headers
519+
}
520+
514521
#[cfg(test)]
515522
mod tests {
516523
use super::*;

‎src/lib.rs

+28
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ pub mod io;
8888
mod liquidity;
8989
mod logger;
9090
mod message_handler;
91+
mod payjoin_receiver;
9192
mod payjoin_sender;
9293
pub mod payment;
9394
mod peer_store;
@@ -131,6 +132,7 @@ use connection::ConnectionManager;
131132
use event::{EventHandler, EventQueue};
132133
use gossip::GossipSource;
133134
use liquidity::LiquiditySource;
135+
use payjoin_receiver::PayjoinReceiver;
134136
use payment::store::PaymentStore;
135137
use payment::{Bolt11Payment, OnchainPayment, PayjoinPayment, PaymentDetails, SpontaneousPayment};
136138
use peer_store::{PeerInfo, PeerStore};
@@ -184,6 +186,7 @@ pub struct Node {
184186
peer_manager: Arc<PeerManager>,
185187
connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
186188
payjoin_sender: Option<Arc<PayjoinSender>>,
189+
payjoin_receiver: Option<Arc<PayjoinReceiver>>,
187190
keys_manager: Arc<KeysManager>,
188191
network_graph: Arc<NetworkGraph>,
189192
gossip_source: Arc<GossipSource>,
@@ -620,6 +623,28 @@ impl Node {
620623
}
621624
});
622625

626+
if let Some(payjoin_receiver) = &self.payjoin_receiver {
627+
let mut stop_payjoin_server = self.stop_sender.subscribe();
628+
let payjoin_receiver = Arc::clone(&payjoin_receiver);
629+
let payjoin_check_interval = 5;
630+
runtime.spawn(async move {
631+
let mut payjoin_interval =
632+
tokio::time::interval(Duration::from_secs(payjoin_check_interval));
633+
payjoin_interval.reset();
634+
payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
635+
loop {
636+
tokio::select! {
637+
_ = stop_payjoin_server.changed() => {
638+
return;
639+
}
640+
_ = payjoin_interval.tick() => {
641+
let _ = payjoin_receiver.process_payjoin_request().await;
642+
}
643+
}
644+
}
645+
});
646+
}
647+
623648
let event_handler = Arc::new(EventHandler::new(
624649
Arc::clone(&self.event_queue),
625650
Arc::clone(&self.wallet),
@@ -905,9 +930,11 @@ impl Node {
905930
#[cfg(not(feature = "uniffi"))]
906931
pub fn payjoin_payment(&self) -> PayjoinPayment {
907932
let payjoin_sender = self.payjoin_sender.as_ref();
933+
let payjoin_receiver = self.payjoin_receiver.as_ref();
908934
PayjoinPayment::new(
909935
Arc::clone(&self.runtime),
910936
payjoin_sender.map(Arc::clone),
937+
payjoin_receiver.map(Arc::clone),
911938
Arc::clone(&self.config),
912939
)
913940
}
@@ -926,6 +953,7 @@ impl Node {
926953
PayjoinPayment::new(
927954
Arc::clone(&self.runtime),
928955
payjoin_sender.map(Arc::clone),
956+
None,
929957
Arc::clone(&self.config),
930958
)
931959
}

‎src/payjoin_receiver.rs

+398
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
use crate::error::Error;
2+
use crate::io::utils::ohttp_headers;
3+
use crate::logger::FilesystemLogger;
4+
use crate::types::Wallet;
5+
use lightning::log_info;
6+
use lightning::util::logger::Logger;
7+
use payjoin::receive::v2::{Enrolled, Enroller, PayjoinProposal, UncheckedProposal};
8+
use payjoin::{OhttpKeys, PjUriBuilder};
9+
use payjoin::{PjUri, Url};
10+
use std::ops::Deref;
11+
use std::sync::Arc;
12+
use tokio::sync::RwLock;
13+
14+
pub(crate) struct PayjoinReceiver {
15+
logger: Arc<FilesystemLogger>,
16+
wallet: Arc<Wallet>,
17+
payjoin_directory: Url,
18+
payjoin_relay: Url,
19+
enrolled: RwLock<Option<Enrolled>>,
20+
ohttp_keys: RwLock<Option<OhttpKeys>>,
21+
}
22+
23+
impl PayjoinReceiver {
24+
pub(crate) fn new(
25+
logger: Arc<FilesystemLogger>, wallet: Arc<Wallet>, payjoin_relay: Url,
26+
payjoin_directory: Url, ohttp_keys: Option<OhttpKeys>,
27+
) -> Self {
28+
Self {
29+
logger,
30+
wallet,
31+
payjoin_directory,
32+
payjoin_relay,
33+
enrolled: RwLock::new(None),
34+
ohttp_keys: RwLock::new(ohttp_keys),
35+
}
36+
}
37+
38+
pub(crate) async fn receive(&self, amount: bitcoin::Amount) -> Result<PjUri, Error> {
39+
if !self.is_enrolled().await {
40+
self.enroll().await?;
41+
}
42+
let enrolled = self.enrolled.read().await;
43+
let enrolled = match enrolled.as_ref() {
44+
Some(enrolled) => enrolled,
45+
None => {
46+
log_info!(self.logger, "Payjoin Receiver: Not enrolled");
47+
return Err(Error::PayjoinReceiverUnavailable);
48+
},
49+
};
50+
let fallback_target = enrolled.fallback_target();
51+
let ohttp_keys = self.ohttp_keys.read().await;
52+
let ohttp_keys = match ohttp_keys.as_ref() {
53+
Some(okeys) => okeys,
54+
None => {
55+
log_info!(self.logger, "Payjoin Receiver: No ohttp keys");
56+
return Err(Error::PayjoinReceiverUnavailable);
57+
},
58+
};
59+
let address = self.wallet.get_new_address()?;
60+
let pj_part = match payjoin::Url::parse(&fallback_target) {
61+
Ok(pj_part) => pj_part,
62+
Err(_) => {
63+
log_info!(self.logger, "Payjoin Receiver: Invalid fallback target");
64+
return Err(Error::PayjoinReceiverUnavailable);
65+
},
66+
};
67+
let payjoin_uri =
68+
PjUriBuilder::new(address, pj_part, Some(ohttp_keys.clone())).amount(amount).build();
69+
Ok(payjoin_uri)
70+
}
71+
72+
pub(crate) async fn process_payjoin_request(&self) {
73+
let mut enrolled = self.enrolled.write().await;
74+
if let Some(mut enrolled) = enrolled.take() {
75+
let min_fee_rate = bitcoin::FeeRate::from_sat_per_vb(1); // FIXME: Use a real fee rate
76+
let (req, context) = match enrolled.extract_req() {
77+
Ok(req) => req,
78+
Err(e) => {
79+
log_info!(
80+
self.logger,
81+
"Payjoin Receiver: Unable to extract enrollement request and context{}",
82+
e
83+
);
84+
return;
85+
},
86+
};
87+
88+
let client = reqwest::Client::new();
89+
let response = match client
90+
.post(req.url.to_string())
91+
.body(req.body)
92+
.headers(ohttp_headers())
93+
.send()
94+
.await
95+
{
96+
Ok(response) => response,
97+
Err(e) => {
98+
log_info!(
99+
self.logger,
100+
"Payjoin Receiver: Unable to fetch payjoin request {}",
101+
e
102+
);
103+
return;
104+
},
105+
};
106+
if response.status() != reqwest::StatusCode::OK {
107+
log_info!(
108+
self.logger,
109+
"Payjoin Receiver: Got non-200 response from directory server {}",
110+
response.status()
111+
);
112+
return;
113+
};
114+
let response = match response.bytes().await {
115+
Ok(response) => response,
116+
Err(e) => {
117+
log_info!(self.logger, "Payjoin Receiver: Error reading response {}", e);
118+
return;
119+
},
120+
};
121+
if response.is_empty() {
122+
log_info!(self.logger, "Payjoin Receiver: Empty response from directory server");
123+
return;
124+
};
125+
let response = match enrolled.process_res(response.to_vec().as_slice(), context) {
126+
Ok(response) => response,
127+
Err(e) => {
128+
log_info!(
129+
self.logger,
130+
"Payjoin Receiver: Unable to process payjoin request {}",
131+
e
132+
);
133+
return;
134+
},
135+
};
136+
let unchecked_proposal = match response {
137+
Some(proposal) => proposal,
138+
None => {
139+
return;
140+
},
141+
};
142+
let mut payjoin_proposal =
143+
match self.validate_payjoin_request(unchecked_proposal, min_fee_rate).await {
144+
Ok(proposal) => proposal,
145+
Err(e) => {
146+
log_info!(self.logger, "Payjoin Validation: {}", e);
147+
return;
148+
},
149+
};
150+
let (receiver_request, _) = match payjoin_proposal.extract_v2_req() {
151+
Ok(req) => req,
152+
Err(e) => {
153+
log_info!(self.logger, "Payjoin Receiver: Unable to extract V2 request {}", e);
154+
return;
155+
},
156+
};
157+
match reqwest::Client::new()
158+
.post(&receiver_request.url.to_string())
159+
.body(receiver_request.body)
160+
.headers(ohttp_headers())
161+
.send()
162+
.await
163+
{
164+
Ok(response) => {
165+
if response.status() == reqwest::StatusCode::OK {
166+
log_info!(self.logger, "Payjoin Receiver: Payjoin response sent to sender");
167+
} else {
168+
log_info!(
169+
self.logger,
170+
"Payjoin Receiver: Got non-200 response from directory {}",
171+
response.status()
172+
);
173+
}
174+
},
175+
Err(e) => {
176+
log_info!(
177+
self.logger,
178+
"Payjoin Receiver: Unable to make request to directory {}",
179+
e
180+
);
181+
},
182+
}
183+
} else {
184+
log_info!(self.logger, "Payjoin Receiver: Unable to get enrolled object");
185+
}
186+
}
187+
188+
async fn enroll(&self) -> Result<(), Error> {
189+
let ohttp_keys = match self.ohttp_keys.read().await.deref() {
190+
Some(okeys) => okeys.clone(),
191+
None => {
192+
let payjoin_directory = &self.payjoin_directory;
193+
let payjoin_directory = match payjoin_directory.join("/ohttp-keys") {
194+
Ok(payjoin_directory) => payjoin_directory,
195+
Err(e) => {
196+
log_info!(
197+
self.logger,
198+
"Payjoin Receiver: Unable to construct ohttp keys url {}",
199+
e
200+
);
201+
return Err(Error::PayjoinReceiverEnrollementFailed);
202+
},
203+
};
204+
let proxy = match reqwest::Proxy::all(self.payjoin_relay.to_string()) {
205+
Ok(proxy) => proxy,
206+
Err(e) => {
207+
log_info!(
208+
self.logger,
209+
"Payjoin Receiver: Unable to construct reqwest proxy {}",
210+
e
211+
);
212+
return Err(Error::PayjoinReceiverEnrollementFailed);
213+
},
214+
};
215+
let client = match reqwest::Client::builder().proxy(proxy).build() {
216+
Ok(client) => client,
217+
Err(e) => {
218+
log_info!(
219+
self.logger,
220+
"Payjoin Receiver: Unable to construct reqwest client {}",
221+
e
222+
);
223+
return Err(Error::PayjoinReceiverEnrollementFailed);
224+
},
225+
};
226+
let response = match client.get(payjoin_directory).send().await {
227+
Ok(response) => response,
228+
Err(e) => {
229+
log_info!(
230+
self.logger,
231+
"Payjoin Receiver: Unable to make request to fetch ohttp keys {}",
232+
e
233+
);
234+
return Err(Error::PayjoinReceiverEnrollementFailed);
235+
},
236+
};
237+
if response.status() != reqwest::StatusCode::OK {
238+
log_info!(
239+
self.logger,
240+
"Payjoin Receiver: Got non 200 response when fetching ohttp keys {}",
241+
response.status()
242+
);
243+
return Err(Error::PayjoinReceiverEnrollementFailed);
244+
}
245+
let response = match response.bytes().await {
246+
Ok(response) => response,
247+
Err(e) => {
248+
log_info!(
249+
self.logger,
250+
"Payjoin Receiver: Error reading ohttp keys response {}",
251+
e
252+
);
253+
return Err(Error::PayjoinReceiverEnrollementFailed);
254+
},
255+
};
256+
OhttpKeys::decode(response.to_vec().as_slice()).map_err(|e| {
257+
log_info!(self.logger, "Payjoin Receiver: Unable to decode ohttp keys {}", e);
258+
Error::PayjoinReceiverEnrollementFailed
259+
})?
260+
},
261+
};
262+
let mut enroller = Enroller::from_directory_config(
263+
self.payjoin_directory.clone(),
264+
ohttp_keys.clone(),
265+
self.payjoin_relay.clone(),
266+
);
267+
let (req, ctx) = match enroller.extract_req() {
268+
Ok(req) => req,
269+
Err(e) => {
270+
log_info!(
271+
self.logger,
272+
"Payjoin Receiver: unable to extract enrollement request {}",
273+
e
274+
);
275+
return Err(Error::PayjoinReceiverEnrollementFailed);
276+
},
277+
};
278+
let response = match reqwest::Client::new()
279+
.post(&req.url.to_string())
280+
.body(req.body)
281+
.headers(ohttp_headers())
282+
.send()
283+
.await
284+
{
285+
Ok(response) => response,
286+
Err(_) => {
287+
log_info!(self.logger, "Payjoin Receiver: unable to make enrollement request");
288+
return Err(Error::PayjoinReceiverEnrollementFailed);
289+
},
290+
};
291+
let response = match response.bytes().await {
292+
Ok(response) => response,
293+
Err(_) => {
294+
panic!("Error reading response");
295+
},
296+
};
297+
let enrolled = match enroller.process_res(response.to_vec().as_slice(), ctx) {
298+
Ok(enrolled) => enrolled,
299+
Err(e) => {
300+
log_info!(
301+
self.logger,
302+
"Payjoin Receiver: unable to process enrollement response {}",
303+
e
304+
);
305+
return Err(Error::PayjoinReceiverEnrollementFailed);
306+
},
307+
};
308+
309+
*self.ohttp_keys.write().await = Some(ohttp_keys);
310+
*self.enrolled.write().await = Some(enrolled);
311+
Ok(())
312+
}
313+
314+
async fn is_enrolled(&self) -> bool {
315+
self.enrolled.read().await.deref().is_some()
316+
&& self.ohttp_keys.read().await.deref().is_some()
317+
}
318+
319+
async fn validate_payjoin_request(
320+
&self, proposal: UncheckedProposal, min_fee_rate: Option<bitcoin::FeeRate>,
321+
) -> Result<PayjoinProposal, Error> {
322+
let tx = proposal.extract_tx_to_schedule_broadcast();
323+
let wallet = &self.wallet;
324+
let verified = wallet.verify_tx(&tx).await;
325+
if verified.is_err() {
326+
log_info!(self.logger, "Invalid transaction");
327+
return Err(Error::PayjoinReceiverRequestValidationFailed);
328+
};
329+
let proposal = proposal
330+
.check_broadcast_suitability(min_fee_rate, |_t| Ok(verified.is_ok()))
331+
.map_err(|e| {
332+
log_info!(self.logger, "Broadcast suitability check failed {}", e);
333+
Error::PayjoinReceiverRequestValidationFailed
334+
})?;
335+
let proposal = proposal
336+
.check_inputs_not_owned(|script| {
337+
Ok(wallet.is_mine(&script.to_owned()).unwrap_or(false))
338+
})
339+
.map_err(|e| {
340+
log_info!(self.logger, "Inputs owned by us check failed {}", e);
341+
Error::PayjoinReceiverRequestValidationFailed
342+
})?;
343+
let proposal = proposal.check_no_mixed_input_scripts().map_err(|e| {
344+
log_info!(self.logger, "Mixed input scripts check failed {}", e);
345+
Error::PayjoinReceiverRequestValidationFailed
346+
})?;
347+
// Fixme: discuss how to handle this, instead of the Ok(false) we should have a way to
348+
// store seen outpoints and check against them
349+
let proposal =
350+
proposal.check_no_inputs_seen_before(|_outpoint| Ok(false)).map_err(|e| {
351+
log_info!(self.logger, "Inputs seen before check failed {}", e);
352+
Error::PayjoinReceiverRequestValidationFailed
353+
})?;
354+
let mut provisional_proposal = proposal
355+
.identify_receiver_outputs(|script| {
356+
Ok(wallet.is_mine(&script.to_owned()).unwrap_or(false))
357+
})
358+
.map_err(|e| {
359+
log_info!(self.logger, "Identify receiver outputs failed {}", e);
360+
Error::PayjoinReceiverRequestValidationFailed
361+
})?;
362+
{
363+
let (candidate_inputs, utxo_set) = wallet.payjoin_receiver_candidate_input()?;
364+
match provisional_proposal.try_preserving_privacy(candidate_inputs) {
365+
Ok(selected_outpoint) => {
366+
if let Some(selected_utxo) = utxo_set.iter().find(|i| {
367+
i.outpoint.txid == selected_outpoint.txid
368+
&& i.outpoint.vout == selected_outpoint.vout
369+
}) {
370+
let txo_to_contribute = bitcoin::TxOut {
371+
value: selected_utxo.txout.value,
372+
script_pubkey: selected_utxo.txout.script_pubkey.clone(),
373+
};
374+
let outpoint_to_contribute = bitcoin::OutPoint {
375+
txid: selected_utxo.outpoint.txid,
376+
vout: selected_utxo.outpoint.vout,
377+
};
378+
provisional_proposal
379+
.contribute_witness_input(txo_to_contribute, outpoint_to_contribute);
380+
}
381+
},
382+
Err(_) => {
383+
log_info!(self.logger, "Failed to select utxos to improve payjoin request privacy. Payjoin proceeds regardless");
384+
},
385+
};
386+
};
387+
let payjoin_proposal = provisional_proposal
388+
.finalize_proposal(
389+
|psbt| Ok(wallet.sign_provisional_payjoin_proposal(psbt.clone()).unwrap()),
390+
None,
391+
)
392+
.map_err(|e| {
393+
log_info!(self.logger, "Finalize proposal failed {}", e);
394+
Error::PayjoinReceiverRequestValidationFailed
395+
})?;
396+
Ok(payjoin_proposal)
397+
}
398+
}

‎src/payjoin_sender.rs

+4-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// An implementation of payjoin v2 sender as described in BIP-77.
22
use crate::error::Error;
3+
use crate::io::utils::ohttp_headers;
34
use crate::logger::FilesystemLogger;
45
use crate::types::Wallet;
56

@@ -86,8 +87,8 @@ where
8687
let response = match client
8788
.post(request.url.clone())
8889
.body(request.body.clone())
89-
.headers(ohttp_req_header())
90-
.timeout(tokio::time::Duration::from_secs(10))
90+
.headers(ohttp_headers())
91+
.timeout(tokio::time::Duration::from_secs(30))
9192
.send()
9293
.await
9394
{
@@ -144,7 +145,7 @@ where
144145
let response = match client
145146
.post(request.url.clone())
146147
.body(request.body.clone())
147-
.headers(ohttp_req_header())
148+
.headers(ohttp_headers())
148149
.send()
149150
.await
150151
{
@@ -251,12 +252,3 @@ where
251252
Ok(txid)
252253
}
253254
}
254-
255-
fn ohttp_req_header() -> reqwest::header::HeaderMap {
256-
let mut headers = reqwest::header::HeaderMap::new();
257-
headers.insert(
258-
reqwest::header::CONTENT_TYPE,
259-
reqwest::header::HeaderValue::from_static("message/ohttp-req"),
260-
);
261-
headers
262-
}

‎src/payment/payjoin.rs

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
//! Holds a payment handler allowing to send Payjoin payments.
22
3+
use payjoin::PjUri;
4+
5+
use crate::payjoin_receiver::PayjoinReceiver;
36
use crate::types::PayjoinSender;
47
use crate::{error::Error, Config};
58

@@ -13,15 +16,16 @@ use std::sync::{Arc, RwLock};
1316
pub struct PayjoinPayment {
1417
runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>,
1518
sender: Option<Arc<PayjoinSender>>,
19+
receiver: Option<Arc<PayjoinReceiver>>,
1620
config: Arc<Config>,
1721
}
1822

1923
impl PayjoinPayment {
2024
pub(crate) fn new(
2125
runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>, sender: Option<Arc<PayjoinSender>>,
22-
config: Arc<Config>,
26+
receiver: Option<Arc<PayjoinReceiver>>, config: Arc<Config>,
2327
) -> Self {
24-
Self { runtime, sender, config }
28+
Self { runtime, sender, receiver, config }
2529
}
2630

2731
/// Send an on chain Payjoin transaction to the address specified in the `payjoin_uri`
@@ -81,4 +85,20 @@ impl PayjoinPayment {
8185
},
8286
}
8387
}
88+
89+
/// Receive an on chain Payjoin transaction
90+
///
91+
/// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
92+
/// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki
93+
pub async fn receive(&self, amount: bitcoin::Amount) -> Result<PjUri, Error> {
94+
let rt_lock = self.runtime.read().unwrap();
95+
if rt_lock.is_none() {
96+
return Err(Error::NotRunning);
97+
}
98+
if let Some(receiver) = &self.receiver {
99+
receiver.receive(amount).await
100+
} else {
101+
Err(Error::PayjoinReceiverUnavailable)
102+
}
103+
}
84104
}

‎src/wallet.rs

+103-2
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@ use lightning::util::message_signing;
1717
use bdk::blockchain::EsploraBlockchain;
1818
use bdk::database::BatchDatabase;
1919
use bdk::wallet::AddressIndex;
20-
use bdk::FeeRate;
20+
use bdk::{FeeRate, LocalUtxo};
2121
use bdk::{SignOptions, SyncOptions};
2222

2323
use bitcoin::bech32::u5;
2424
use bitcoin::blockdata::locktime::absolute::LockTime;
2525
use bitcoin::secp256k1::ecdh::SharedSecret;
2626
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
2727
use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing};
28-
use bitcoin::{ScriptBuf, Transaction, TxOut, Txid};
28+
use bitcoin::{bitcoinconsensus, ScriptBuf, Transaction, TxOut, Txid};
2929

30+
use std::collections::{BTreeMap, HashMap};
3031
use std::ops::Deref;
3132
use std::sync::{Arc, Condvar, Mutex};
3233
use std::time::Duration;
@@ -139,12 +140,112 @@ where
139140
Ok(psbt)
140141
}
141142

143+
// Returns a list of unspent outputs that can be used as inputs to improve the privacy of a
144+
// payjoin transaction.
145+
pub(crate) fn payjoin_receiver_candidate_input(
146+
&self,
147+
) -> Result<(HashMap<bitcoin::Amount, bitcoin::OutPoint>, Vec<LocalUtxo>), Error> {
148+
let locked_wallet = self.inner.lock().unwrap();
149+
let utxo_set = locked_wallet.list_unspent()?;
150+
let candidate_inputs = utxo_set
151+
.iter()
152+
.filter_map(|utxo| {
153+
if !utxo.is_spent {
154+
Some((bitcoin::Amount::from_sat(utxo.txout.value), utxo.outpoint))
155+
} else {
156+
None
157+
}
158+
})
159+
.collect();
160+
Ok((candidate_inputs, utxo_set))
161+
}
162+
142163
pub(crate) fn sign_transaction(&self, psbt: &mut Psbt) -> Result<bool, Error> {
143164
let wallet = self.inner.lock().unwrap();
144165
let is_signed = wallet.sign(psbt, SignOptions::default())?;
145166
Ok(is_signed)
146167
}
147168

169+
pub(crate) fn sign_provisional_payjoin_proposal(&self, mut psbt: Psbt) -> Result<Psbt, Error> {
170+
let wallet = self.inner.lock().unwrap();
171+
let mut sign_options = SignOptions::default();
172+
sign_options.trust_witness_utxo = true;
173+
wallet.sign(&mut psbt, sign_options)?;
174+
// Clear derivation paths and from the PSBT as required by BIP78
175+
psbt.inputs.iter_mut().for_each(|i| {
176+
i.bip32_derivation = BTreeMap::new();
177+
});
178+
psbt.outputs.iter_mut().for_each(|o| {
179+
o.bip32_derivation = BTreeMap::new();
180+
});
181+
Ok(psbt)
182+
}
183+
184+
/// Verifies that the given transaction meets the bitcoin consensus rules.
185+
pub async fn verify_tx(&self, tx: &Transaction) -> Result<(), Error> {
186+
let serialized_tx = bitcoin::consensus::serialize(&tx);
187+
// Loop through all the inputs
188+
for (index, input) in tx.input.iter().enumerate() {
189+
let input = input.clone();
190+
let txid = input.previous_output.txid;
191+
let prev_tx = match self.blockchain.get_tx(&txid).await {
192+
Ok(prev_tx) => prev_tx,
193+
Err(e) => {
194+
log_error!(
195+
self.logger,
196+
"Failed to verify transaction: blockchain error {} for txid {}",
197+
e,
198+
&txid
199+
);
200+
panic!("Failed to verify transaction: blockchain error");
201+
},
202+
};
203+
if let Some(prev_tx) = prev_tx {
204+
let spent_output = match prev_tx.output.get(input.previous_output.vout as usize) {
205+
Some(output) => output,
206+
None => {
207+
log_error!(
208+
self.logger,
209+
"Failed to verify transaction: missing output {} in tx {}",
210+
input.previous_output.vout,
211+
txid
212+
);
213+
panic!("Failed to verify transaction: blockchain error");
214+
},
215+
};
216+
match bitcoinconsensus::verify(
217+
&spent_output.script_pubkey.to_bytes(),
218+
spent_output.value,
219+
&serialized_tx,
220+
index,
221+
) {
222+
Ok(()) => {},
223+
Err(e) => {
224+
log_error!(self.logger, "Failed to verify transaction: {}", e);
225+
panic!("Failed to verify transaction: blockchain error");
226+
},
227+
}
228+
} else {
229+
if tx.is_coin_base() {
230+
continue;
231+
} else {
232+
log_error!(
233+
self.logger,
234+
"Failed to verify transaction: missing previous transaction {}",
235+
txid
236+
);
237+
panic!("Failed to verify transaction: blockchain error");
238+
}
239+
}
240+
}
241+
Ok(())
242+
}
243+
244+
pub(crate) fn is_mine(&self, script: &ScriptBuf) -> Result<bool, Error> {
245+
let locked_wallet = self.inner.lock().unwrap();
246+
Ok(locked_wallet.is_mine(script)?)
247+
}
248+
148249
pub(crate) fn create_funding_transaction(
149250
&self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget,
150251
locktime: LockTime,

‎tests/common/mod.rs

+51-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ type TestNode = Node;
188188
macro_rules! setup_builder {
189189
($builder: ident, $config: expr) => {
190190
#[cfg(feature = "uniffi")]
191-
let $builder = Builder::from_config($config.clone());
191+
let mut $builder = Builder::from_config($config.clone());
192192
#[cfg(not(feature = "uniffi"))]
193193
let mut $builder = Builder::from_config($config.clone());
194194
};
@@ -210,6 +210,22 @@ pub(crate) fn setup_two_nodes(electrsd: &ElectrsD, allow_0conf: bool) -> (TestNo
210210
(node_a, node_b)
211211
}
212212

213+
pub(crate) fn setup_two_payjoin_nodes(
214+
electrsd: &ElectrsD, allow_0conf: bool,
215+
) -> (TestNode, TestNode) {
216+
println!("== Node A ==");
217+
let config_a = random_config();
218+
let node_a_payjoin_receiver = setup_payjoin_receiver_node(electrsd, config_a);
219+
220+
println!("\n== Node B ==");
221+
let mut config_b = random_config();
222+
if allow_0conf {
223+
config_b.trusted_peers_0conf.push(node_a_payjoin_receiver.node_id());
224+
}
225+
let node_b_payjoin_sender = setup_payjoin_sender_node(electrsd, config_b);
226+
(node_a_payjoin_receiver, node_b_payjoin_sender)
227+
}
228+
213229
pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode {
214230
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
215231
setup_builder!(builder, config);
@@ -222,6 +238,40 @@ pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode {
222238
node
223239
}
224240

241+
pub(crate) fn setup_payjoin_sender_node(electrsd: &ElectrsD, config: Config) -> TestNode {
242+
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
243+
setup_builder!(builder, config);
244+
builder.set_esplora_server(esplora_url.clone());
245+
let payjoin_relay = "https://pj.bobspacebkk.com".to_string();
246+
builder.set_payjoin_sender_config(payjoin_relay.clone());
247+
let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into()));
248+
let node = builder.build_with_store(test_sync_store).unwrap();
249+
node.start().unwrap();
250+
assert!(node.status().is_running);
251+
assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());
252+
node
253+
}
254+
255+
pub(crate) fn setup_payjoin_receiver_node(electrsd: &ElectrsD, config: Config) -> TestNode {
256+
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
257+
setup_builder!(builder, config);
258+
builder.set_esplora_server(esplora_url.clone());
259+
let ohttp_keys = [
260+
1, 0, 32, 108, 178, 19, 223, 232, 96, 28, 254, 93, 0, 96, 121, 239, 134, 221, 11, 36, 222,
261+
38, 242, 81, 226, 126, 225, 44, 158, 1, 241, 220, 96, 96, 51, 0, 4, 0, 1, 0, 3,
262+
];
263+
let payjoin_directory = "https://payjo.in".to_string();
264+
let payjoin_relay = "https://pj.bobspacebkk.com".to_string();
265+
let ohttp_keys = bitcoin::base64::encode(ohttp_keys);
266+
builder.set_payjoin_receiver_config(payjoin_directory, payjoin_relay, Some(ohttp_keys));
267+
let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into()));
268+
let node = builder.build_with_store(test_sync_store).unwrap();
269+
node.start().unwrap();
270+
assert!(node.status().is_running);
271+
assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());
272+
node
273+
}
274+
225275
pub(crate) fn generate_blocks_and_wait<E: ElectrumApi>(
226276
bitcoind: &BitcoindClient, electrs: &E, num: usize,
227277
) {

‎tests/integration_tests_payjoin.rs

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
mod common;
2+
3+
use crate::common::{
4+
generate_blocks_and_wait, premine_and_distribute_funds, setup_two_payjoin_nodes, wait_for_tx,
5+
};
6+
use bitcoin::Amount;
7+
use common::setup_bitcoind_and_electrsd;
8+
9+
#[test]
10+
fn send_receive_regular_payjoin_transaction() {
11+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
12+
let (node_a_pj_receiver, node_b_pj_sender) = setup_two_payjoin_nodes(&electrsd, false);
13+
let addr_b = node_b_pj_sender.onchain_payment().new_address().unwrap();
14+
let addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap();
15+
let premine_amount_sat = 100_000_00;
16+
premine_and_distribute_funds(
17+
&bitcoind.client,
18+
&electrsd.client,
19+
vec![addr_b, addr_a],
20+
Amount::from_sat(premine_amount_sat),
21+
);
22+
node_a_pj_receiver.sync_wallets().unwrap();
23+
node_b_pj_sender.sync_wallets().unwrap();
24+
assert_eq!(node_b_pj_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
25+
assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00);
26+
assert_eq!(node_a_pj_receiver.next_event(), None);
27+
assert_eq!(node_a_pj_receiver.list_channels().len(), 0);
28+
let payjoin_payment = node_a_pj_receiver.payjoin_payment();
29+
30+
let payjoin_uri = tokio::runtime::Runtime::new().unwrap().handle().block_on(async {
31+
let payjoin_uri = payjoin_payment.receive(Amount::from_sat(80_000)).await.unwrap();
32+
payjoin_uri
33+
});
34+
let payjoin_uri = payjoin_uri.to_string();
35+
let txid = tokio::runtime::Runtime::new()
36+
.unwrap()
37+
.handle()
38+
.block_on(async {
39+
let txid = node_b_pj_sender
40+
.payjoin_payment()
41+
.send(payjoin::Uri::try_from(payjoin_uri).unwrap(), None, None)
42+
.await;
43+
txid
44+
})
45+
.unwrap();
46+
if txid.is_none() {
47+
dbg!("no txid yet");
48+
loop {
49+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
50+
node_a_pj_receiver.sync_wallets().unwrap();
51+
let node_a_balance = node_a_pj_receiver.list_balances();
52+
if node_a_balance.total_onchain_balance_sats == 80000 + 100_000_00 {
53+
break;
54+
}
55+
}
56+
} else {
57+
dbg!("got txid already");
58+
wait_for_tx(&electrsd.client, txid.unwrap());
59+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
60+
}
61+
node_b_pj_sender.sync_wallets().unwrap();
62+
let node_b_balance = node_b_pj_sender.list_balances();
63+
// The fact that we have "<" here should indicate that we are not handling fees correctly yet
64+
assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000);
65+
}

0 commit comments

Comments
 (0)
Please sign in to comment.