Skip to content

Commit f92ae86

Browse files
committed
Nest address screening inside V1 config
Since address screening is only relevant when V1 is enabled, it doesn't make much sense to expose the blocked_* config otherwise. This replaces the `enable_v1` bool with a new `v1` config section. The presence / absence of that config section indicates whether to enable v1 or not, and blocked address settings can be configured within that section if the access-control feature is enabled.
1 parent 58e7ccf commit f92ae86

6 files changed

Lines changed: 81 additions & 63 deletions

File tree

payjoin-directory/src/lib.rs

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ impl BlockedAddresses {
9494
}
9595
}
9696

97+
/// V1 protocol configuration.
98+
///
99+
/// Its presence in [`Service`] enables the V1 fallback path;
100+
/// its contents carry optional blocklist screening.
101+
#[derive(Clone, Default)]
102+
pub struct V1 {
103+
blocked_addresses: Option<BlockedAddresses>,
104+
}
105+
106+
impl V1 {
107+
pub fn new(blocked_addresses: Option<BlockedAddresses>) -> Self { Self { blocked_addresses } }
108+
}
109+
97110
fn parse_address_lines(text: &str) -> std::collections::HashSet<bitcoin::ScriptBuf> {
98111
text.lines()
99112
.filter_map(|l| {
@@ -117,8 +130,7 @@ pub struct Service<D: Db> {
117130
db: D,
118131
ohttp: ohttp::Server,
119132
sentinel_tag: SentinelTag,
120-
enable_v1: bool,
121-
blocked_addresses: Option<BlockedAddresses>,
133+
v1: Option<V1>,
122134
}
123135

124136
impl<D: Db, B> tower::Service<Request<B>> for Service<D>
@@ -142,13 +154,8 @@ where
142154
}
143155

144156
impl<D: Db> Service<D> {
145-
pub fn new(db: D, ohttp: ohttp::Server, sentinel_tag: SentinelTag, enable_v1: bool) -> Self {
146-
Self { db, ohttp, sentinel_tag, enable_v1, blocked_addresses: None }
147-
}
148-
149-
pub fn with_blocked_addresses(mut self, addrs: BlockedAddresses) -> Self {
150-
self.blocked_addresses = Some(addrs);
151-
self
157+
pub fn new(db: D, ohttp: ohttp::Server, sentinel_tag: SentinelTag, v1: Option<V1>) -> Self {
158+
Self { db, ohttp, sentinel_tag, v1 }
152159
}
153160

154161
#[cfg(feature = "_manual-tls")]
@@ -294,7 +301,7 @@ impl<D: Db> Service<D> {
294301
B: Body<Data = Bytes> + Send + 'static,
295302
B::Error: Into<BoxError>,
296303
{
297-
if self.enable_v1 {
304+
if self.v1.is_some() {
298305
self.post_fallback_v1(id, query, body).await
299306
} else {
300307
let _ = (id, query, body);
@@ -382,7 +389,7 @@ impl<D: Db> Service<D> {
382389
match (parts.method, path_segments.as_slice()) {
383390
(Method::POST, &["", id]) => self.post_mailbox(id, body).await,
384391
(Method::GET, &["", id]) => self.get_mailbox(id).await,
385-
(Method::PUT, &["", id]) if self.enable_v1 => self.put_payjoin_v1(id, body).await,
392+
(Method::PUT, &["", id]) if self.v1.is_some() => self.put_payjoin_v1(id, body).await,
386393
_ => Ok(not_found()),
387394
}
388395
}
@@ -472,7 +479,7 @@ impl<D: Db> Service<D> {
472479
Err(_) => return Ok(bad_request_body_res),
473480
};
474481

475-
if let Some(blocked) = &self.blocked_addresses {
482+
if let Some(blocked) = self.v1.as_ref().and_then(|v| v.blocked_addresses.as_ref()) {
476483
let scripts = blocked.0.read().await;
477484
if !scripts.is_empty() {
478485
match screen_v1_addresses(&body_str, &scripts) {
@@ -763,12 +770,12 @@ mod tests {
763770

764771
use super::*;
765772

766-
async fn test_service(enable_v1: bool) -> Service<FilesDb> {
773+
async fn test_service(v1: Option<V1>) -> Service<FilesDb> {
767774
let dir = tempfile::tempdir().expect("tempdir");
768775
let db = FilesDb::init(Duration::from_millis(100), dir.keep()).await.expect("db init");
769776
let ohttp: ohttp::Server =
770777
key_config::gen_ohttp_server_config().expect("ohttp config").into();
771-
Service::new(db, ohttp, SentinelTag::new([0u8; 32]), enable_v1)
778+
Service::new(db, ohttp, SentinelTag::new([0u8; 32]), v1)
772779
}
773780

774781
/// A valid ShortId encoded as bech32 for use in URL paths.
@@ -785,7 +792,7 @@ mod tests {
785792

786793
#[tokio::test]
787794
async fn post_v1_when_disabled_returns_version_unsupported() {
788-
let mut svc = test_service(false).await;
795+
let mut svc = test_service(None).await;
789796
let id = valid_short_id_path();
790797
let req = Request::builder()
791798
.method(Method::POST)
@@ -802,7 +809,7 @@ mod tests {
802809

803810
#[tokio::test]
804811
async fn post_v1_with_invalid_body_returns_reject() {
805-
let mut svc = test_service(true).await;
812+
let mut svc = test_service(Some(V1::new(None))).await;
806813
let id = valid_short_id_path();
807814
let req = Request::builder()
808815
.method(Method::POST)
@@ -819,7 +826,7 @@ mod tests {
819826

820827
#[tokio::test]
821828
async fn post_v1_with_no_receiver_returns_unavailable() {
822-
let mut svc = test_service(true).await;
829+
let mut svc = test_service(Some(V1::new(None))).await;
823830
let id = valid_short_id_path();
824831
let req = Request::builder()
825832
.method(Method::POST)

payjoin-directory/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ async fn main() -> Result<(), BoxError> {
2929
.await
3030
.expect("Failed to initialize persistent storage");
3131

32-
let service = Service::new(db, ohttp.into(), SentinelTag::new([0u8; 32]), config.enable_v1);
32+
let v1 = if config.enable_v1 { Some(V1::new(None)) } else { None };
33+
let service = Service::new(db, ohttp.into(), SentinelTag::new([0u8; 32]), v1);
3334

3435
let listener = TcpListener::bind(config.listen_addr).await?;
3536

payjoin-mailroom/config.example.toml

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@
1313
# Request timeout in seconds
1414
# timeout = 30
1515

16+
# --- V1 protocol ---
17+
# Uncomment the [v1] section to enable V1 fallback support.
18+
# (address screening requires `access-control` feature)
19+
# [v1]
20+
21+
# Path to a local file containing blocked Bitcoin addresses (one per line).
22+
# blocked_addresses_path = "/path/to/blocked_addresses.txt"
23+
24+
# URL to periodically fetch an updated blocked-address list from.
25+
# blocked_addresses_url = "https://example.com/blocked_addresses.txt"
26+
27+
# How often (in seconds) to refresh the remote address list (default: 86400).
28+
# blocked_addresses_refresh_secs = 86400
29+
1630
# --- Access-control (requires `access-control` feature) ---
1731
# [access_control]
1832

@@ -23,13 +37,3 @@
2337

2438
# ISO 3166-1 alpha-2 country codes whose requests should be blocked.
2539
# blocked_regions = ["CU", "IR", "KP", "SY"]
26-
27-
# Path to a local file containing blocked Bitcoin addresses (one per line).
28-
# Used for V1 PSBT screening.
29-
# blocked_addresses_path = "/path/to/blocked_addresses.txt"
30-
31-
# URL to periodically fetch an updated blocked-address list from.
32-
# blocked_addresses_url = "https://example.com/blocked_addresses.txt"
33-
34-
# How often (in seconds) to refresh the remote address list (default: 86400).
35-
# blocked_addresses_refresh_secs = 86400

payjoin-mailroom/src/config.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub struct Config {
1212
pub storage_dir: PathBuf,
1313
#[serde(deserialize_with = "deserialize_duration_secs")]
1414
pub timeout: Duration,
15-
pub enable_v1: bool,
15+
pub v1: Option<V1Config>,
1616
#[cfg(feature = "telemetry")]
1717
pub telemetry: Option<TelemetryConfig>,
1818
#[cfg(feature = "acme")]
@@ -21,6 +21,21 @@ pub struct Config {
2121
pub access_control: Option<AccessControlConfig>,
2222
}
2323

24+
/// V1 protocol configuration.
25+
///
26+
/// Present in [`Config`] to enable the V1 fallback path.
27+
/// Contains optional address-screening settings that only apply to V1.
28+
#[derive(Debug, Clone, Deserialize, Default)]
29+
#[serde(default)]
30+
pub struct V1Config {
31+
#[cfg(feature = "access-control")]
32+
pub blocked_addresses_path: Option<PathBuf>,
33+
#[cfg(feature = "access-control")]
34+
pub blocked_addresses_url: Option<String>,
35+
#[cfg(feature = "access-control")]
36+
pub blocked_addresses_refresh_secs: Option<u64>,
37+
}
38+
2439
#[cfg(feature = "telemetry")]
2540
#[derive(Debug, Clone, Deserialize)]
2641
pub struct TelemetryConfig {
@@ -44,9 +59,6 @@ pub struct AcmeConfig {
4459
pub struct AccessControlConfig {
4560
pub geo_db_path: Option<PathBuf>,
4661
pub blocked_regions: Vec<String>,
47-
pub blocked_addresses_path: Option<PathBuf>,
48-
pub blocked_addresses_url: Option<String>,
49-
pub blocked_addresses_refresh_secs: Option<u64>,
5062
}
5163

5264
#[cfg(feature = "acme")]
@@ -72,7 +84,7 @@ impl Default for Config {
7284
listener: "[::]:8080".parse().expect("valid default listener address"),
7385
storage_dir: PathBuf::from("./data"),
7486
timeout: Duration::from_secs(30),
75-
enable_v1: false,
87+
v1: None,
7688
#[cfg(feature = "telemetry")]
7789
telemetry: None,
7890
#[cfg(feature = "acme")]
@@ -96,13 +108,13 @@ impl Config {
96108
listener: ListenerAddress,
97109
storage_dir: PathBuf,
98110
timeout: Duration,
99-
enable_v1: bool,
111+
v1: Option<V1Config>,
100112
) -> Self {
101113
Self {
102114
listener,
103115
storage_dir,
104116
timeout,
105-
enable_v1,
117+
v1,
106118
#[cfg(feature = "telemetry")]
107119
telemetry: None,
108120
#[cfg(feature = "acme")]

payjoin-mailroom/src/lib.rs

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,7 @@ pub async fn serve(config: Config, meter_provider: Option<SdkMeterProvider>) ->
3939
#[cfg(feature = "access-control")]
4040
let geoip = init_geoip(&config).await?;
4141

42-
#[allow(unused_mut)]
43-
let mut directory = init_directory(&config, sentinel_tag).await?;
44-
#[cfg(feature = "access-control")]
45-
if let Some(blocked) = init_blocked_addresses(&config).await? {
46-
directory = directory.with_blocked_addresses(blocked);
47-
}
42+
let directory = init_directory(&config, sentinel_tag).await?;
4843

4944
let services = Services {
5045
directory,
@@ -87,12 +82,7 @@ pub async fn serve_manual_tls(
8782
#[cfg(feature = "access-control")]
8883
let geoip = init_geoip(&config).await?;
8984

90-
#[allow(unused_mut)]
91-
let mut directory = init_directory(&config, sentinel_tag).await?;
92-
#[cfg(feature = "access-control")]
93-
if let Some(blocked) = init_blocked_addresses(&config).await? {
94-
directory = directory.with_blocked_addresses(blocked);
95-
}
85+
let directory = init_directory(&config, sentinel_tag).await?;
9686

9787
let services = Services {
9888
directory,
@@ -156,12 +146,7 @@ pub async fn serve_acme(
156146
#[cfg(feature = "access-control")]
157147
let geoip = init_geoip(&config).await?;
158148

159-
#[allow(unused_mut)]
160-
let mut directory = init_directory(&config, sentinel_tag).await?;
161-
#[cfg(feature = "access-control")]
162-
if let Some(blocked) = init_blocked_addresses(&config).await? {
163-
directory = directory.with_blocked_addresses(blocked);
164-
}
149+
let directory = init_directory(&config, sentinel_tag).await?;
165150

166151
let services = Services {
167152
directory,
@@ -231,7 +216,16 @@ async fn init_directory(
231216
let ohttp_keys_dir = config.storage_dir.join("ohttp-keys");
232217
let ohttp_config = init_ohttp_config(&ohttp_keys_dir)?;
233218

234-
Ok(payjoin_directory::Service::new(db, ohttp_config.into(), sentinel_tag, config.enable_v1))
219+
let v1 = if config.v1.is_some() {
220+
#[cfg(feature = "access-control")]
221+
let blocked = init_blocked_addresses(config).await?;
222+
#[cfg(not(feature = "access-control"))]
223+
let blocked = None;
224+
Some(payjoin_directory::V1::new(blocked))
225+
} else {
226+
None
227+
};
228+
Ok(payjoin_directory::Service::new(db, ohttp_config.into(), sentinel_tag, v1))
235229
}
236230

237231
#[cfg(feature = "access-control")]
@@ -252,18 +246,18 @@ async fn init_geoip(
252246
async fn init_blocked_addresses(
253247
config: &Config,
254248
) -> anyhow::Result<Option<payjoin_directory::BlockedAddresses>> {
255-
let ac_config = match &config.access_control {
249+
let v1_config = match &config.v1 {
256250
Some(c) => c,
257251
None => return Ok(None),
258252
};
259253

260254
// Neither file nor URL configured
261-
if ac_config.blocked_addresses_path.is_none() && ac_config.blocked_addresses_url.is_none() {
255+
if v1_config.blocked_addresses_path.is_none() && v1_config.blocked_addresses_url.is_none() {
262256
return Ok(None);
263257
}
264258

265259
// Load initial addresses from file if available
266-
let blocked = match &ac_config.blocked_addresses_path {
260+
let blocked = match &v1_config.blocked_addresses_path {
267261
Some(path) => {
268262
let text = access_control::load_blocked_address_text(path)?;
269263
let ba = payjoin_directory::BlockedAddresses::from_address_lines(&text);
@@ -274,10 +268,10 @@ async fn init_blocked_addresses(
274268
};
275269

276270
// If URL configured, try initial fetch and spawn background updater
277-
if let Some(url) = &ac_config.blocked_addresses_url {
271+
if let Some(url) = &v1_config.blocked_addresses_url {
278272
let cache_path = config.storage_dir.join("blocked_addresses_cache.txt");
279273
let refresh = std::time::Duration::from_secs(
280-
ac_config.blocked_addresses_refresh_secs.unwrap_or(86400),
274+
v1_config.blocked_addresses_refresh_secs.unwrap_or(86400),
281275
);
282276

283277
// Try initial fetch; fall back to cache on failure
@@ -432,7 +426,7 @@ mod tests {
432426
"[::]:0".parse().expect("valid listener address"),
433427
tempdir.path().to_path_buf(),
434428
Duration::from_secs(2),
435-
false,
429+
None,
436430
);
437431

438432
let mut root_store = RootCertStore::empty();
@@ -527,7 +521,7 @@ mod tests {
527521
"[::]:0".parse().expect("valid listener address"),
528522
tempdir.path().to_path_buf(),
529523
Duration::from_secs(2),
530-
false,
524+
None,
531525
);
532526

533527
let sentinel_tag = generate_sentinel_tag();

payjoin-test-utils/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ pub async fn init_directory(
121121
"[::]:0".parse().expect("valid listener address"),
122122
tempdir.path().to_path_buf(),
123123
Duration::from_secs(2),
124-
true,
124+
Some(payjoin_mailroom::config::V1Config::default()),
125125
);
126126

127127
let tls_config = RustlsConfig::from_der(vec![local_cert_key.0], local_cert_key.1).await?;
@@ -149,7 +149,7 @@ async fn init_ohttp_relay(
149149
"[::]:0".parse().expect("valid listener address"),
150150
tempdir.path().to_path_buf(),
151151
Duration::from_secs(2),
152-
false,
152+
None,
153153
);
154154

155155
let (port, handle) = payjoin_mailroom::serve_manual_tls(config, None, root_store)

0 commit comments

Comments
 (0)