1- use std:: sync:: Arc ;
1+ use std:: sync:: { Arc , Mutex } ;
22
33use anyhow:: { anyhow, Context , Result } ;
44use payjoin:: bitcoin:: consensus:: encode:: serialize_hex;
@@ -17,22 +17,49 @@ use crate::app::{handle_interrupt, http_agent};
1717use crate :: db:: v2:: { ReceiverPersister , SenderPersister } ;
1818use crate :: db:: Database ;
1919
20+ #[ derive( Debug , Clone ) ]
21+ pub struct RelayState {
22+ selected_relay : Option < payjoin:: Url > ,
23+ #[ cfg( not( feature = "_danger-local-https" ) ) ]
24+ failed_relays : Vec < payjoin:: Url > ,
25+ }
26+
27+ impl RelayState {
28+ #[ cfg( feature = "_danger-local-https" ) ]
29+ pub fn new ( ) -> Self { RelayState { selected_relay : None } }
30+ #[ cfg( not( feature = "_danger-local-https" ) ) ]
31+ pub fn new ( ) -> Self { RelayState { selected_relay : None , failed_relays : Vec :: new ( ) } }
32+
33+ #[ cfg( not( feature = "_danger-local-https" ) ) ]
34+ pub fn set_selected_relay ( & mut self , relay : payjoin:: Url ) { self . selected_relay = Some ( relay) ; }
35+
36+ pub fn get_selected_relay ( & self ) -> Option < payjoin:: Url > { self . selected_relay . clone ( ) }
37+
38+ #[ cfg( not( feature = "_danger-local-https" ) ) ]
39+ pub fn add_failed_relay ( & mut self , relay : payjoin:: Url ) { self . failed_relays . push ( relay) ; }
40+
41+ #[ cfg( not( feature = "_danger-local-https" ) ) ]
42+ pub fn get_failed_relays ( & self ) -> Vec < payjoin:: Url > { self . failed_relays . clone ( ) }
43+ }
44+
2045#[ derive( Clone ) ]
2146pub ( crate ) struct App {
2247 config : Config ,
2348 db : Arc < Database > ,
2449 wallet : BitcoindWallet ,
2550 interrupt : watch:: Receiver < ( ) > ,
51+ relay_state : Arc < Mutex < RelayState > > ,
2652}
2753
2854#[ async_trait:: async_trait]
2955impl AppTrait for App {
3056 fn new ( config : Config ) -> Result < Self > {
3157 let db = Arc :: new ( Database :: create ( & config. db_path ) ?) ;
58+ let relay_state = Arc :: new ( Mutex :: new ( RelayState :: new ( ) ) ) ;
3259 let ( interrupt_tx, interrupt_rx) = watch:: channel ( ( ) ) ;
3360 tokio:: spawn ( handle_interrupt ( interrupt_tx) ) ;
3461 let wallet = BitcoindWallet :: new ( & config. bitcoind ) ?;
35- let app = Self { config, db, wallet, interrupt : interrupt_rx } ;
62+ let app = Self { config, db, wallet, interrupt : interrupt_rx, relay_state } ;
3663 app. wallet ( )
3764 . network ( )
3865 . context ( "Failed to connect to bitcoind. Check config RPC connection." ) ?;
@@ -69,7 +96,8 @@ impl AppTrait for App {
6996
7097 async fn receive_payjoin ( & self , amount : Amount ) -> Result < ( ) > {
7198 let address = self . wallet ( ) . get_new_address ( ) ?;
72- let ohttp_keys = unwrap_ohttp_keys_or_else_fetch ( & self . config ) . await ?;
99+ let ohttp_keys =
100+ unwrap_ohttp_keys_or_else_fetch ( & self . config , self . relay_state . clone ( ) ) . await ?;
73101 let mut persister = ReceiverPersister :: new ( self . db . clone ( ) ) ;
74102 let new_receiver = NewReceiver :: new (
75103 address,
@@ -151,6 +179,8 @@ impl App {
151179 println ! ( "Receive session established" ) ;
152180 let mut pj_uri = session. pj_uri ( ) ;
153181 pj_uri. amount = amount;
182+ let ohttp_relay = self . unwrap_relay_or_else_fetch ( ) . await ?;
183+
154184 println ! ( "Request Payjoin by sharing this Payjoin Uri:" ) ;
155185 println ! ( "{pj_uri}" ) ;
156186
@@ -168,14 +198,12 @@ impl App {
168198 let mut payjoin_proposal = match self . process_v2_proposal ( receiver. clone ( ) ) {
169199 Ok ( proposal) => proposal,
170200 Err ( Error :: ReplyToSender ( e) ) => {
171- return Err (
172- handle_recoverable_error ( e, receiver, & self . config . v2 ( ) ?. ohttp_relay ) . await
173- ) ;
201+ return Err ( handle_recoverable_error ( e, receiver, & ohttp_relay) . await ) ;
174202 }
175203 Err ( e) => return Err ( e. into ( ) ) ,
176204 } ;
177205 let ( req, ohttp_ctx) = payjoin_proposal
178- . extract_req ( & self . config . v2 ( ) ? . ohttp_relay )
206+ . extract_req ( ohttp_relay)
179207 . map_err ( |e| anyhow ! ( "v2 req extraction failed {}" , e) ) ?;
180208 println ! ( "Got a request from the sender. Responding with a Payjoin proposal." ) ;
181209 let res = post_request ( req) . await ?;
@@ -192,15 +220,16 @@ impl App {
192220 }
193221
194222 async fn long_poll_post ( & self , req_ctx : & mut Sender ) -> Result < Psbt > {
195- match req_ctx. extract_v2 ( self . config . v2 ( ) ?. ohttp_relay . clone ( ) ) {
223+ let ohttp_relay = self . unwrap_relay_or_else_fetch ( ) . await ?;
224+
225+ match req_ctx. extract_v2 ( ohttp_relay. clone ( ) ) {
196226 Ok ( ( req, ctx) ) => {
197227 println ! ( "Posting Original PSBT Payload request..." ) ;
198228 let response = post_request ( req) . await ?;
199229 println ! ( "Sent fallback transaction" ) ;
200230 let v2_ctx = Arc :: new ( ctx. process_response ( & response. bytes ( ) . await ?) ?) ;
201231 loop {
202- let ( req, ohttp_ctx) =
203- v2_ctx. extract_req ( self . config . v2 ( ) ?. ohttp_relay . clone ( ) ) ?;
232+ let ( req, ohttp_ctx) = v2_ctx. extract_req ( & ohttp_relay) ?;
204233 let response = post_request ( req) . await ?;
205234 match v2_ctx. process_response ( & response. bytes ( ) . await ?, ohttp_ctx) {
206235 Ok ( Some ( psbt) ) => return Ok ( psbt) ,
@@ -236,8 +265,10 @@ impl App {
236265 & self ,
237266 session : & mut payjoin:: receive:: v2:: Receiver ,
238267 ) -> Result < payjoin:: receive:: v2:: UncheckedProposal > {
268+ let ohttp_relay = self . unwrap_relay_or_else_fetch ( ) . await ?;
269+
239270 loop {
240- let ( req, context) = session. extract_req ( & self . config . v2 ( ) ? . ohttp_relay ) ?;
271+ let ( req, context) = session. extract_req ( & ohttp_relay) ?;
241272 println ! ( "Polling receive request..." ) ;
242273 let ohttp_response = post_request ( req) . await ?;
243274 let proposal = session
@@ -289,6 +320,16 @@ impl App {
289320 log:: debug!( "Receiver's Payjoin proposal PSBT Rsponse: {payjoin_proposal_psbt:#?}" ) ;
290321 Ok ( payjoin_proposal)
291322 }
323+
324+ async fn unwrap_relay_or_else_fetch ( & self ) -> Result < payjoin:: Url > {
325+ let selected_relay =
326+ self . relay_state . lock ( ) . expect ( "Lock should not be poisoned" ) . get_selected_relay ( ) ;
327+ let ohttp_relay = match selected_relay {
328+ Some ( relay) => relay,
329+ None => validate_relay ( & self . config , self . relay_state . clone ( ) ) . await ?,
330+ } ;
331+ Ok ( ohttp_relay)
332+ }
292333}
293334
294335/// Handle request error by sending an error response over the directory
@@ -335,26 +376,144 @@ fn try_contributing_inputs(
335376 . commit_inputs ( ) )
336377}
337378
338- async fn unwrap_ohttp_keys_or_else_fetch ( config : & Config ) -> Result < payjoin:: OhttpKeys > {
379+ async fn unwrap_ohttp_keys_or_else_fetch (
380+ config : & Config ,
381+ relay_state : Arc < Mutex < RelayState > > ,
382+ ) -> Result < payjoin:: OhttpKeys > {
339383 if let Some ( keys) = config. v2 ( ) ?. ohttp_keys . clone ( ) {
340384 println ! ( "Using OHTTP Keys from config" ) ;
341385 Ok ( keys)
342386 } else {
343387 println ! ( "Bootstrapping private network transport over Oblivious HTTP" ) ;
344- let ohttp_relay = config. v2 ( ) ?. ohttp_relay . clone ( ) ;
345- let payjoin_directory = config. v2 ( ) ?. pj_directory . clone ( ) ;
346- #[ cfg( feature = "_danger-local-https" ) ]
388+
389+ fetch_keys ( config, relay_state. clone ( ) )
390+ . await
391+ . and_then ( |keys| keys. ok_or_else ( || anyhow:: anyhow!( "No OHTTP keys found" ) ) )
392+ }
393+ }
394+
395+ #[ cfg( not( feature = "_danger-local-https" ) ) ]
396+ async fn fetch_keys (
397+ config : & Config ,
398+ relay_state : Arc < Mutex < RelayState > > ,
399+ ) -> Result < Option < payjoin:: OhttpKeys > > {
400+ use payjoin:: bitcoin:: secp256k1:: rand:: prelude:: SliceRandom ;
401+ let payjoin_directory = config. v2 ( ) ?. pj_directory . clone ( ) ;
402+ let relays = config. v2 ( ) ?. ohttp_relays . clone ( ) ;
403+
404+ loop {
405+ let failed_relays =
406+ relay_state. lock ( ) . expect ( "Lock should not be poisoned" ) . get_failed_relays ( ) ;
407+
408+ let remaining_relays: Vec < _ > =
409+ relays. iter ( ) . filter ( |r| !failed_relays. contains ( r) ) . cloned ( ) . collect ( ) ;
410+
411+ if remaining_relays. is_empty ( ) {
412+ return Err ( anyhow ! ( "No valid relays available" ) ) ;
413+ }
414+
415+ let selected_relay =
416+ match remaining_relays. choose ( & mut payjoin:: bitcoin:: key:: rand:: thread_rng ( ) ) {
417+ Some ( relay) => relay. clone ( ) ,
418+ None => return Err ( anyhow ! ( "Failed to select from remaining relays" ) ) ,
419+ } ;
420+
421+ relay_state
422+ . lock ( )
423+ . expect ( "Lock should not be poisoned" )
424+ . set_selected_relay ( selected_relay. clone ( ) ) ;
425+
347426 let ohttp_keys = {
348- let cert_der = crate :: app:: read_local_cert ( ) ?;
349- payjoin:: io:: fetch_ohttp_keys_with_cert ( ohttp_relay, payjoin_directory, cert_der)
350- . await ?
427+ payjoin:: io:: fetch_ohttp_keys ( selected_relay. clone ( ) , payjoin_directory. clone ( ) ) . await
351428 } ;
352- #[ cfg( not( feature = "_danger-local-https" ) ) ]
353- let ohttp_keys = payjoin:: io:: fetch_ohttp_keys ( ohttp_relay, payjoin_directory) . await ?;
354- Ok ( ohttp_keys)
429+
430+ match ohttp_keys {
431+ Ok ( keys) => return Ok ( Some ( keys) ) ,
432+ Err ( payjoin:: io:: Error :: UnexpectedStatusCode ( e) ) => {
433+ return Err ( payjoin:: io:: Error :: UnexpectedStatusCode ( e) . into ( ) ) ;
434+ }
435+ Err ( e) => {
436+ log:: debug!( "Failed to connect to relay: {selected_relay}, {e:?}" ) ;
437+ relay_state
438+ . lock ( )
439+ . expect ( "Lock should not be poisoned" )
440+ . add_failed_relay ( selected_relay) ;
441+ }
442+ }
443+ }
444+ }
445+
446+ ///Local relays are incapable of acting as proxies so we must opportunistically fetch keys from the config
447+ #[ cfg( feature = "_danger-local-https" ) ]
448+ async fn fetch_keys (
449+ config : & Config ,
450+ _relay_state : Arc < Mutex < RelayState > > ,
451+ ) -> Result < Option < payjoin:: OhttpKeys > > {
452+ let keys = config. v2 ( ) ?. ohttp_keys . clone ( ) . expect ( "No OHTTP keys set" ) ;
453+
454+ Ok ( Some ( keys) )
455+ }
456+
457+ #[ cfg( not( feature = "_danger-local-https" ) ) ]
458+ async fn validate_relay (
459+ config : & Config ,
460+ relay_state : Arc < Mutex < RelayState > > ,
461+ ) -> Result < payjoin:: Url > {
462+ use payjoin:: bitcoin:: secp256k1:: rand:: prelude:: SliceRandom ;
463+ let payjoin_directory = config. v2 ( ) ?. pj_directory . clone ( ) ;
464+ let relays = config. v2 ( ) ?. ohttp_relays . clone ( ) ;
465+
466+ loop {
467+ let failed_relays =
468+ relay_state. lock ( ) . expect ( "Lock should not be poisoned" ) . get_failed_relays ( ) ;
469+
470+ let remaining_relays: Vec < _ > =
471+ relays. iter ( ) . filter ( |r| !failed_relays. contains ( r) ) . cloned ( ) . collect ( ) ;
472+
473+ if remaining_relays. is_empty ( ) {
474+ return Err ( anyhow ! ( "No valid relays available" ) ) ;
475+ }
476+
477+ let selected_relay =
478+ match remaining_relays. choose ( & mut payjoin:: bitcoin:: key:: rand:: thread_rng ( ) ) {
479+ Some ( relay) => relay. clone ( ) ,
480+ None => return Err ( anyhow ! ( "Failed to select from remaining relays" ) ) ,
481+ } ;
482+
483+ relay_state
484+ . lock ( )
485+ . expect ( "Lock should not be poisoned" )
486+ . set_selected_relay ( selected_relay. clone ( ) ) ;
487+
488+ let ohttp_keys =
489+ payjoin:: io:: fetch_ohttp_keys ( selected_relay. clone ( ) , payjoin_directory. clone ( ) ) . await ;
490+
491+ match ohttp_keys {
492+ Ok ( _) => return Ok ( selected_relay) ,
493+ Err ( payjoin:: io:: Error :: UnexpectedStatusCode ( e) ) => {
494+ return Err ( payjoin:: io:: Error :: UnexpectedStatusCode ( e) . into ( ) ) ;
495+ }
496+ Err ( e) => {
497+ log:: debug!( "Failed to connect to relay: {selected_relay}, {e:?}" ) ;
498+ relay_state
499+ . lock ( )
500+ . expect ( "Lock should not be poisoned" )
501+ . add_failed_relay ( selected_relay) ;
502+ }
503+ }
355504 }
356505}
357506
507+ #[ cfg( feature = "_danger-local-https" ) ]
508+ async fn validate_relay (
509+ config : & Config ,
510+ _relay_state : Arc < Mutex < RelayState > > ,
511+ ) -> Result < payjoin:: Url > {
512+ let relay = config. v2 ( ) ?. ohttp_relays . first ( ) . expect ( "no OHTTP relay set" ) . clone ( ) ;
513+
514+ Ok ( relay)
515+ }
516+
358517async fn post_request ( req : payjoin:: Request ) -> Result < reqwest:: Response > {
359518 let http = http_agent ( ) ?;
360519 http. post ( req. url )
0 commit comments