Skip to content

Commit f5263ff

Browse files
committed
Thread SelectContext through relay selection
Add a SelectContext argument to select() and pass an empty, random context at each call site.
1 parent 8e66ebd commit f5263ff

4 files changed

Lines changed: 42 additions & 11 deletions

File tree

.cargo/mutants.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ exclude_re = [
1212
# Timeout loops
1313
# src/receive/v1/mod.rs
1414
"interleave_shuffle", # Replacing index += 1 with index *= 1 in a loop causes a timeout due to an infinite loop
15+
# src/core/relay.rs
16+
"RelaySelector::mark_failed", # A no-op makes fetch_ohttp_keys_inner never exhaust relays, looping until timeout
1517

1618
# Trivial mutations
1719
# These exclusions are allowing code blocks to run with arithmetic involving zero and as a result are no-ops

payjoin-cli/src/app/v2/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1084,9 +1084,11 @@ impl App {
10841084
E: Into<anyhow::Error>,
10851085
{
10861086
let mut selector = RelaySelector::new(self.config.v2()?.ohttp_relays.clone());
1087+
// A later policy fills Post/Poll, receiver key and time window in here.
1088+
let select_ctx = payjoin::relay::SelectContext::random();
10871089
loop {
10881090
let relay = selector
1089-
.select(&mut payjoin::bitcoin::key::rand::thread_rng())
1091+
.select(&select_ctx, &mut payjoin::bitcoin::key::rand::thread_rng())
10901092
.ok_or_else(|| anyhow!("No valid relays available"))?;
10911093
let (req, ctx) = build(relay.as_str()).map_err(Into::into)?;
10921094
match self.post_request(req).await {

payjoin/src/core/io.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use http::header::ACCEPT;
66
use reqwest::{Client, Proxy};
77

88
use crate::into_url::IntoUrl;
9-
use crate::relay::RelaySelector;
9+
use crate::relay::{RelaySelector, SelectContext};
1010
use crate::{OhttpKeys, Url};
1111

1212
/// Fetch the ohttp keys from the specified payjoin directory via proxy.
@@ -54,9 +54,10 @@ async fn fetch_ohttp_keys_inner(
5454
) -> Result<(OhttpKeys, Url), Error> {
5555
let ohttp_keys_url = payjoin_directory.join("/.well-known/ohttp-gateway")?;
5656
let mut selector = RelaySelector::new(relays.to_vec());
57+
let ctx = SelectContext::random();
5758
let mut last_err: Option<Error> = None;
5859
loop {
59-
let relay = match selector.select(&mut rand::thread_rng()) {
60+
let relay = match selector.select(&ctx, &mut rand::thread_rng()) {
6061
Some(relay) => relay,
6162
None => return Err(last_err.unwrap_or(Error::NoRelaysAvailable)),
6263
};

payjoin/src/core/relay.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@ use bitcoin::secp256k1::rand::{self};
33

44
use crate::Url;
55

6-
/// Picks an OHTTP relay at random, excluding relays marked failed, so clients
7-
/// share one selection policy instead of each diverging.
6+
/// Per-request inputs for [`RelaySelector::select`]. Empty, so selection is
7+
/// unconstrained and uniform-random.
8+
#[derive(Clone, Debug)]
9+
#[non_exhaustive]
10+
pub struct SelectContext {}
11+
12+
impl SelectContext {
13+
/// An unconstrained context: selection is uniform-random.
14+
pub fn random() -> Self { Self {} }
15+
}
16+
17+
/// Picks an OHTTP relay, excluding relays marked failed, so clients share one
18+
/// selection policy instead of each diverging.
819
#[derive(Clone, Debug)]
920
pub struct RelaySelector {
1021
relays: Vec<Url>,
@@ -14,11 +25,12 @@ pub struct RelaySelector {
1425
impl RelaySelector {
1526
pub fn new(relays: Vec<Url>) -> Self { Self { relays, failed: Vec::new() } }
1627

17-
/// Pick a random relay not marked failed, or `None` when none remain.
18-
pub fn select<R: rand::Rng>(&self, rng: &mut R) -> Option<Url> {
28+
/// Pick a relay for `ctx`, never one marked failed, or `None` when none remain.
29+
pub fn select<R: rand::Rng>(&self, _ctx: &SelectContext, rng: &mut R) -> Option<Url> {
1930
self.relays.iter().filter(|r| !self.failed.contains(r)).choose(rng).cloned()
2031
}
2132

33+
/// Record a relay transport failure so `select` avoids it.
2234
pub fn mark_failed(&mut self, relay: &Url) {
2335
if !self.failed.contains(relay) {
2436
self.failed.push(relay.clone());
@@ -44,7 +56,7 @@ mod tests {
4456
fn select_returns_a_configured_relay() {
4557
let selector = RelaySelector::new(relays());
4658
let mut rng = StdRng::seed_from_u64(1);
47-
let picked = selector.select(&mut rng).expect("a relay");
59+
let picked = selector.select(&SelectContext::random(), &mut rng).expect("a relay");
4860
assert!(relays().contains(&picked));
4961
}
5062

@@ -55,7 +67,7 @@ mod tests {
5567
let failed = Url::parse("https://a.example").unwrap();
5668
selector.mark_failed(&failed);
5769
for _ in 0..50 {
58-
assert_ne!(selector.select(&mut rng), Some(failed.clone()));
70+
assert_ne!(selector.select(&SelectContext::random(), &mut rng), Some(failed.clone()));
5971
}
6072
}
6173

@@ -66,13 +78,27 @@ mod tests {
6678
selector.mark_failed(&r);
6779
}
6880
let mut rng = StdRng::seed_from_u64(3);
69-
assert_eq!(selector.select(&mut rng), None);
81+
assert_eq!(selector.select(&SelectContext::random(), &mut rng), None);
7082
}
7183

7284
#[test]
7385
fn select_is_none_when_empty() {
7486
let selector = RelaySelector::new(Vec::new());
7587
let mut rng = StdRng::seed_from_u64(4);
76-
assert_eq!(selector.select(&mut rng), None);
88+
assert_eq!(selector.select(&SelectContext::random(), &mut rng), None);
89+
}
90+
91+
// An empty context selects uniformly across all relays.
92+
#[test]
93+
fn random_context_is_unconstrained() {
94+
let selector = RelaySelector::new(relays());
95+
let mut seen = std::collections::BTreeSet::new();
96+
let mut rng = StdRng::seed_from_u64(5);
97+
for _ in 0..200 {
98+
if let Some(r) = selector.select(&SelectContext::random(), &mut rng) {
99+
seen.insert(r.to_string());
100+
}
101+
}
102+
assert_eq!(seen.len(), relays().len(), "random must reach every relay");
77103
}
78104
}

0 commit comments

Comments
 (0)