1- //! OHTTP relay selection and key bootstrapping for the payjoin-cli.
1+ //! OHTTP relay and payjoin directory selection / key bootstrapping for the payjoin-cli.
22//!
3- //! [`RelayManager `] tracks relays that have failed, excluding them from
4- //! future selections for the lifetime of the [`RelayManager `].
3+ //! [`MailroomManager `] tracks relays and directories that have failed,
4+ //! excluding them from future selections for the lifetime of the [`MailroomManager `].
55//!
6- //! `unwrap_ohttp_keys_or_else_fetch` returns user-supplied keys when present,
7- //! otherwise selects a relay at random from the configured list,
8- //! excluding relays that [`RelayManager`] has marked as failed,
9- //! to avoid a fixed contact pattern at the network layer.
6+ //! `unwrap_ohttp_keys_or_else_fetch_from_directory` returns user-supplied keys
7+ //! when present, otherwise selects a relay at random from the configured list
8+ //! (excluding failed relays) to fetch OHTTP keys from the given directory.
9+ //!
10+ //! `fetch_ohttp_keys_from_directory` retries on relay failures (e.g. connection
11+ //! errors) by selecting another relay. Once a directory is chosen for a session
12+ //! it must not change — the directory is embedded in the BIP21 URI at session
13+ //! creation and recovered from the session event log on resume.
1014use std:: sync:: { Arc , Mutex } ;
1115
1216use anyhow:: { anyhow, Result } ;
@@ -15,20 +19,33 @@ use payjoin::Url;
1519use super :: Config ;
1620
1721#[ derive( Debug , Clone ) ]
18- pub struct RelayManager {
22+ pub struct MailroomManager {
1923 config : Config ,
2024 failed_relays : Arc < Mutex < Vec < Url > > > ,
25+ failed_directories : Arc < Mutex < Vec < Url > > > ,
2126}
2227
23- impl RelayManager {
28+ impl MailroomManager {
2429 pub fn new ( config : Config ) -> Self {
25- RelayManager { config, failed_relays : Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) }
30+ MailroomManager {
31+ config,
32+ failed_relays : Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ,
33+ failed_directories : Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ,
34+ }
2635 }
2736
2837 pub fn add_failed_relay ( & self , relay : Url ) {
2938 self . failed_relays . lock ( ) . expect ( "Lock should not be poisoned" ) . push ( relay) ;
3039 }
3140
41+ pub fn clear_failed_relays ( & self ) {
42+ self . failed_relays . lock ( ) . expect ( "Lock should not be poisoned" ) . clear ( ) ;
43+ }
44+
45+ pub fn add_failed_directory ( & self , directory : Url ) {
46+ self . failed_directories . lock ( ) . expect ( "Lock should not be poisoned" ) . push ( directory) ;
47+ }
48+
3249 pub fn choose_relay ( & self ) -> Result < Url > {
3350 use payjoin:: bitcoin:: secp256k1:: rand:: prelude:: SliceRandom ;
3451 let relays = & self . config . v2 ( ) ?. ohttp_relays ;
@@ -46,16 +63,35 @@ impl RelayManager {
4663 . ok_or_else ( || anyhow ! ( "Failed to select from remaining relays" ) )
4764 }
4865
49- pub ( crate ) async fn unwrap_ohttp_keys_or_else_fetch ( & self ) -> Result < ValidatedOhttpKeys > {
66+ pub fn choose_directory ( & self ) -> Result < Url > {
67+ use payjoin:: bitcoin:: secp256k1:: rand:: prelude:: SliceRandom ;
68+ let directories = & self . config . v2 ( ) ?. pj_directories ;
69+ let failed_directories =
70+ self . failed_directories . lock ( ) . expect ( "Lock should not be poisoned" ) ;
71+ let remaining_directories: Vec < _ > =
72+ directories. iter ( ) . filter ( |d| !failed_directories. contains ( d) ) . cloned ( ) . collect ( ) ;
73+
74+ if remaining_directories. is_empty ( ) {
75+ return Err ( anyhow ! ( "No valid directories available" ) ) ;
76+ }
77+
78+ remaining_directories
79+ . choose ( & mut payjoin:: bitcoin:: key:: rand:: thread_rng ( ) )
80+ . cloned ( )
81+ . ok_or_else ( || anyhow ! ( "Failed to select from remaining directories" ) )
82+ }
83+
84+ pub ( crate ) async fn unwrap_ohttp_keys_or_else_fetch_from_directory (
85+ & self ,
86+ directory : & Url ,
87+ ) -> Result < ValidatedOhttpKeys > {
5088 if let Some ( ohttp_keys) = self . config . v2 ( ) ?. ohttp_keys . clone ( ) {
5189 return Ok ( ValidatedOhttpKeys { ohttp_keys } ) ;
5290 }
53- self . fetch_ohttp_keys ( ) . await
91+ self . fetch_ohttp_keys_from_directory ( directory ) . await
5492 }
5593
56- async fn fetch_ohttp_keys ( & self ) -> Result < ValidatedOhttpKeys > {
57- let payjoin_directory = & self . config . v2 ( ) ?. pj_directory ;
58-
94+ async fn fetch_ohttp_keys_from_directory ( & self , directory : & Url ) -> Result < ValidatedOhttpKeys > {
5995 loop {
6096 let selected_relay = self . choose_relay ( ) ?;
6197
@@ -66,27 +102,27 @@ impl RelayManager {
66102 let cert_der = std:: fs:: read ( cert_path) ?;
67103 payjoin:: io:: fetch_ohttp_keys_with_cert (
68104 selected_relay. as_str ( ) ,
69- payjoin_directory . as_str ( ) ,
105+ directory . as_str ( ) ,
70106 & cert_der,
71107 )
72108 . await
73109 } else {
74- payjoin:: io:: fetch_ohttp_keys (
75- selected_relay. as_str ( ) ,
76- payjoin_directory. as_str ( ) ,
77- )
78- . await
110+ payjoin:: io:: fetch_ohttp_keys ( selected_relay. as_str ( ) , directory. as_str ( ) )
111+ . await
79112 }
80113 }
81114 #[ cfg( not( feature = "_manual-tls" ) ) ]
82- payjoin:: io:: fetch_ohttp_keys ( selected_relay. as_str ( ) , payjoin_directory. as_str ( ) )
83- . await
115+ payjoin:: io:: fetch_ohttp_keys ( selected_relay. as_str ( ) , directory. as_str ( ) ) . await
84116 } ;
85117
86118 match ohttp_keys {
87119 Ok ( keys) => return Ok ( ValidatedOhttpKeys { ohttp_keys : keys } ) ,
88120 Err ( payjoin:: io:: Error :: UnexpectedStatusCode ( e) ) => {
89- return Err ( payjoin:: io:: Error :: UnexpectedStatusCode ( e) . into ( ) ) ;
121+ tracing:: debug!(
122+ "Directory {directory} returned unexpected status via relay {selected_relay}: {e:?}"
123+ ) ;
124+ self . add_failed_directory ( directory. clone ( ) ) ;
125+ return Err ( anyhow ! ( "Directory {directory} returned unexpected status: {e}" ) ) ;
90126 }
91127 Err ( e) => {
92128 tracing:: debug!( "Failed to connect to relay: {selected_relay}, {e:?}" ) ;
0 commit comments