Skip to content

Commit ae2dbda

Browse files
committed
fix(reachability): keep established relays canary-verified
Bind relay publication to an opaque prepared allocation, require majority maintenance evidence, withdraw an established relay immediately when that quorum rejects it, and keep DHT address publication synchronized with current peers.\n\nConsume saorsa-transport's fix/pr136-provisional-relay branch so PR #136 is built against the matching provisional-relay lifecycle.\n\nSemVer: patch
1 parent ad76faa commit ae2dbda

12 files changed

Lines changed: 1119 additions & 395 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ once_cell = "1.21"
6464
dashmap = "6"
6565

6666
# Networking
67-
saorsa-transport = "0.35.2"
67+
saorsa-transport = { git = "https://github.com/WithAutonomi/saorsa-transport.git", branch = "fix/pr136-provisional-relay" }
6868

6969
# Core-specific dependencies
7070
dirs = "6.0"

src/adaptive/dht.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,21 @@ impl AdaptiveDHT {
277277
.peer_addresses_for_dial_typed(peer_id)
278278
.await
279279
}
280+
281+
/// Ensure the shared DHT dial coordinator has an authenticated channel.
282+
///
283+
/// Keeping application reconnects on the same path as iterative lookups
284+
/// means both callers share address-failure suppression and never create
285+
/// independent retry loops against a known-bad relay.
286+
pub(crate) async fn ensure_peer_channel(
287+
&self,
288+
peer_id: &PeerId,
289+
candidates: &[(MultiAddr, AddressType)],
290+
) -> Result<()> {
291+
self.dht_manager
292+
.ensure_peer_channel(peer_id, candidates)
293+
.await
294+
}
280295
}
281296

282297
#[cfg(test)]

src/dht_network_manager.rs

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,25 +1099,32 @@ impl DialFailureCache {
10991099
}
11001100

11011101
/// ADR-011 self-heal: when a newer authoritative `PublishAddressSet` from
1102-
/// `publisher` is applied, clear any stale dial-failure suppression for its
1103-
/// freshly published socket addresses. A recovered address — for example a relay
1104-
/// that now hands a reconnecting peer back its previous stable port — is
1105-
/// otherwise kept suppressed for up to [`DIAL_FAILURE_CACHE_TTL`] even though the
1106-
/// owner has just re-attested it. The exemption it grants is keyed by
1107-
/// `(publisher, socket)` so only a dial *to that publisher* benefits. Returns the
1108-
/// number of addresses cleared; a no-op when the publish was not applied (stale
1109-
/// or duplicate sequence).
1102+
/// `publisher` is applied, clear stale dial-failure suppression only for socket
1103+
/// addresses absent from `previous_addresses`.
1104+
///
1105+
/// This immediately retries a genuinely withdrawn then recovered address
1106+
/// without treating a sequence-only refresh of an unchanged bad relay as proof
1107+
/// of recovery. The exemption is keyed by `(publisher, socket)` so only a dial
1108+
/// *to that publisher* benefits. Returns the number of addresses cleared; a
1109+
/// no-op when the publish was not applied (stale or duplicate sequence).
11101110
fn clear_dial_failures_for_published(
11111111
cache: &DialFailureCache,
11121112
publisher: &PeerId,
11131113
applied: bool,
1114+
previous_addresses: &[(crate::MultiAddr, AddressType)],
11141115
addresses: &[(crate::MultiAddr, AddressType)],
11151116
) -> usize {
11161117
if !applied {
11171118
return 0;
11181119
}
11191120
let mut cleared = 0;
11201121
for (addr, _ty) in addresses {
1122+
if previous_addresses
1123+
.iter()
1124+
.any(|(previous, _)| previous == addr)
1125+
{
1126+
continue;
1127+
}
11211128
if let Some(socket_addr) = addr.dialable_socket_addr() {
11221129
// Clears this address's own failure and grants (publisher, socket) a
11231130
// short exemption from IP suppression — but never lifts IP-level relay
@@ -3574,7 +3581,7 @@ impl DhtNetworkManager {
35743581
/// broadcast and translate the shared outcome back into a
35753582
/// caller-facing [`P2PError`] — they do not duplicate the
35763583
/// owner's side effects.
3577-
async fn ensure_peer_channel(
3584+
pub(crate) async fn ensure_peer_channel(
35783585
&self,
35793586
peer_id: &PeerId,
35803587
candidates: &[(MultiAddr, AddressType)],
@@ -4468,13 +4475,15 @@ impl DhtNetworkManager {
44684475
);
44694476
}
44704477
let dht = self.dht.read().await;
4478+
let previous_addresses = dht.get_node_addresses_typed(authenticated_sender).await;
44714479
let applied = dht
44724480
.replace_node_addresses(authenticated_sender, filtered_addresses.clone(), *seq)
44734481
.await;
4474-
// ADR-011: a newer authoritative address set just landed — lift
4475-
// stale dial-failure suppression for its addresses so a
4476-
// self-healed (e.g. reclaimed stable relay) address is retried
4477-
// immediately instead of staying suppressed for the cache TTL.
4482+
// ADR-011: a newer authoritative address set just landed. Lift
4483+
// stale dial-failure suppression only for addresses that were
4484+
// absent from the previous record and have genuinely
4485+
// reappeared (for example a reclaimed stable relay). Refreshing
4486+
// an unchanged failed address must not create a redial loop.
44784487
//
44794488
// Race guard: only exempt addresses that are STILL the peer's
44804489
// current record after the apply (read back under the same lock
@@ -4493,6 +4502,7 @@ impl DhtNetworkManager {
44934502
self.dial_failure_cache.as_ref(),
44944503
authenticated_sender,
44954504
true,
4505+
&previous_addresses,
44964506
&still_current,
44974507
)
44984508
} else {
@@ -5664,41 +5674,64 @@ impl DhtNetworkManager {
56645674
&self,
56655675
typed_addresses: Vec<(crate::MultiAddr, AddressType)>,
56665676
peers: &[DHTNode],
5667-
) {
5677+
) -> Vec<PeerId> {
56685678
let seq = Self::next_publish_seq();
56695679
let op = DhtNetworkOperation::PublishAddressSet {
56705680
seq,
56715681
addresses: typed_addresses.clone(),
56725682
};
5683+
let mut confirmed = Vec::new();
5684+
let mut publishes = FuturesUnordered::new();
56735685
for peer in peers {
56745686
if peer.peer_id == self.config.peer_id {
56755687
continue; // Skip self
56765688
}
56775689
// Pass the peer's typed addresses through directly so
56785690
// send_dht_request avoids a redundant routing-table read for
56795691
// a peer we already have in hand.
5692+
let peer_id = peer.peer_id;
56805693
let peer_typed = peer.typed_addresses();
5681-
match self
5682-
.send_dht_request(&peer.peer_id, op.clone(), Some(&peer_typed))
5683-
.await
5684-
{
5685-
Ok(_) => {
5694+
let op = op.clone();
5695+
publishes.push(async move {
5696+
(
5697+
peer_id,
5698+
self.send_dht_request(&peer_id, op, Some(&peer_typed)).await,
5699+
)
5700+
});
5701+
}
5702+
5703+
// A withdrawal must not spend one full request timeout per unavailable
5704+
// peer while the rest of the network continues dialing the old relay.
5705+
// Fan the full replacement out concurrently and retain the exact
5706+
// acknowledgers so the driver can retry only missing replicas.
5707+
while let Some((peer_id, result)) = publishes.next().await {
5708+
match result {
5709+
Ok(DhtNetworkResult::PublishAddressAck) => {
5710+
confirmed.push(peer_id);
56865711
debug!(
5687-
peer = %peer.peer_id.to_hex(),
5712+
peer = %peer_id.to_hex(),
56885713
addrs = typed_addresses.len(),
56895714
seq,
56905715
"published address set to peer",
56915716
);
56925717
}
5718+
Ok(other) => {
5719+
debug!(
5720+
peer = %peer_id.to_hex(),
5721+
result = ?other,
5722+
"Peer returned an unexpected address publication response"
5723+
);
5724+
}
56935725
Err(e) => {
56945726
debug!(
56955727
"Failed to publish address set to peer {}: {}",
5696-
peer.peer_id.to_hex(),
5728+
peer_id.to_hex(),
56975729
e
56985730
);
56995731
}
57005732
}
57015733
}
5734+
confirmed
57025735
}
57035736

57045737
/// Generate the next monotonic publish sequence number.
@@ -7257,7 +7290,7 @@ mod tests {
72577290

72587291
// A stale/duplicate publish (not applied) must NOT lift suppression.
72597292
assert_eq!(
7260-
clear_dial_failures_for_published(&cache, &peer, false, &addrs),
7293+
clear_dial_failures_for_published(&cache, &peer, false, &[], &addrs),
72617294
0
72627295
);
72637296
assert!(cache.is_failed(&relay_sa, AddressType::Relay));
@@ -7266,13 +7299,53 @@ mod tests {
72667299
// A newer applied publish clears the per-address failures (neither IP is
72677300
// over the suppression threshold here, so both become dialable).
72687301
assert_eq!(
7269-
clear_dial_failures_for_published(&cache, &peer, true, &addrs),
7302+
clear_dial_failures_for_published(&cache, &peer, true, &[], &addrs),
72707303
2
72717304
);
72727305
assert!(!cache.is_failed(&relay_sa, AddressType::Relay));
72737306
assert!(!cache.is_failed(&direct_sa, AddressType::Direct));
72747307
}
72757308

7309+
#[test]
7310+
fn repeated_unchanged_publish_does_not_clear_fresh_dial_failure() {
7311+
let cache = DialFailureCache::new();
7312+
let peer = PeerId::from_bytes([1; 32]);
7313+
let relay = crate::MultiAddr::quic(sock("203.0.113.7:9000"));
7314+
let relay_sa = relay.dialable_socket_addr().expect("dialable");
7315+
let addrs = vec![(relay, AddressType::Relay)];
7316+
7317+
cache.record_failure(relay_sa, AddressType::Relay);
7318+
assert_eq!(
7319+
clear_dial_failures_for_published(&cache, &peer, true, &addrs, &addrs),
7320+
0
7321+
);
7322+
assert!(
7323+
cache.is_failed_for_dial(&peer, &relay_sa, AddressType::Relay),
7324+
"refreshing an unchanged bad relay must not create a retry loop"
7325+
);
7326+
}
7327+
7328+
#[test]
7329+
fn relay_reappearing_after_withdrawal_clears_old_dial_failure() {
7330+
let cache = DialFailureCache::new();
7331+
let peer = PeerId::from_bytes([1; 32]);
7332+
let relay = crate::MultiAddr::quic(sock("203.0.113.7:9000"));
7333+
let direct = crate::MultiAddr::quic(sock("203.0.113.8:9000"));
7334+
let relay_sa = relay.dialable_socket_addr().expect("dialable");
7335+
let previous = vec![(direct.clone(), AddressType::Direct)];
7336+
let current = vec![(relay, AddressType::Relay), (direct, AddressType::Direct)];
7337+
7338+
cache.record_failure(relay_sa, AddressType::Relay);
7339+
assert_eq!(
7340+
clear_dial_failures_for_published(&cache, &peer, true, &previous, &current),
7341+
1
7342+
);
7343+
assert!(
7344+
!cache.is_failed_for_dial(&peer, &relay_sa, AddressType::Relay),
7345+
"a genuinely withdrawn then re-acquired relay should be retried"
7346+
);
7347+
}
7348+
72767349
#[test]
72777350
fn publish_self_heal_keeps_ip_tier_suppression_for_others() {
72787351
let cache = DialFailureCache::new();
@@ -7293,7 +7366,7 @@ mod tests {
72937366
// The owner re-attests one of those addresses via an applied publish.
72947367
let addrs = vec![(crate::MultiAddr::quic(socks[0]), AddressType::Relay)];
72957368
assert_eq!(
7296-
clear_dial_failures_for_published(&cache, &peer, true, &addrs),
7369+
clear_dial_failures_for_published(&cache, &peer, true, &[], &addrs),
72977370
1
72987371
);
72997372

@@ -7334,7 +7407,7 @@ mod tests {
73347407
// Its owner reclaims and re-attests it via an applied publish.
73357408
let addrs = vec![(crate::MultiAddr::quic(socks[0]), AddressType::Relay)];
73367409
assert_eq!(
7337-
clear_dial_failures_for_published(&cache, &owner, true, &addrs),
7410+
clear_dial_failures_for_published(&cache, &owner, true, &[], &addrs),
73387411
1
73397412
);
73407413

src/identity/node_identity.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ pub fn peer_id_from_public_key_bytes(bytes: &[u8]) -> Result<PeerId> {
7272
Ok(peer_id_from_public_key(&public_key))
7373
}
7474

75+
/// Create a [`PeerId`] from an authenticated ML-DSA-65 TLS SPKI.
76+
///
77+
/// `saorsa-transport` exposes the exact peer certificate identity from a
78+
/// completed QUIC/TLS handshake as DER-encoded SubjectPublicKeyInfo. Validate
79+
/// the DER shape, algorithm identifier, absent ML-DSA parameters, and
80+
/// byte-aligned key before deriving the overlay identity from the raw key.
81+
pub(crate) fn peer_id_from_public_key_spki(spki_bytes: &[u8]) -> Result<PeerId> {
82+
let public_key =
83+
saorsa_transport::crypto::raw_public_keys::pqc::extract_public_key_from_spki(spki_bytes)
84+
.map_err(|e| {
85+
P2PError::Identity(IdentityError::InvalidFormat(
86+
format!("Invalid ML-DSA SubjectPublicKeyInfo: {e}").into(),
87+
))
88+
})?;
89+
90+
peer_id_from_public_key_bytes(public_key.as_bytes())
91+
}
92+
7593
/// Public node identity information (without secret keys) - safe to clone
7694
#[derive(Clone)]
7795
pub struct PublicNodeIdentity {
@@ -331,6 +349,37 @@ impl NodeIdentity {
331349
mod tests {
332350
use super::*;
333351

352+
fn ml_dsa_65_spki(public_key: &[u8]) -> Vec<u8> {
353+
const OID: [u8; 9] = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12];
354+
let bit_string_len = public_key.len() + 1;
355+
let algorithm_len = 2 + OID.len();
356+
let algorithm_total_len = 2 + algorithm_len;
357+
let bit_string_total_len = 4 + bit_string_len;
358+
let outer_len = algorithm_total_len + bit_string_total_len;
359+
360+
let mut encoded = Vec::with_capacity(4 + outer_len);
361+
encoded.extend_from_slice(&[
362+
0x30,
363+
0x82,
364+
(outer_len >> 8) as u8,
365+
outer_len as u8,
366+
0x30,
367+
algorithm_len as u8,
368+
0x06,
369+
OID.len() as u8,
370+
]);
371+
encoded.extend_from_slice(&OID);
372+
encoded.extend_from_slice(&[
373+
0x03,
374+
0x82,
375+
(bit_string_len >> 8) as u8,
376+
bit_string_len as u8,
377+
0x00,
378+
]);
379+
encoded.extend_from_slice(public_key);
380+
encoded
381+
}
382+
334383
#[test]
335384
fn test_peer_id_generation() {
336385
let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
@@ -345,6 +394,37 @@ mod tests {
345394
assert_eq!(peer_id, peer_id2);
346395
}
347396

397+
#[test]
398+
fn transport_spki_derives_same_peer_id_as_raw_key() {
399+
let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
400+
.expect("ML-DSA key generation should succeed");
401+
let spki = ml_dsa_65_spki(public_key.as_bytes());
402+
403+
let from_spki =
404+
peer_id_from_public_key_spki(&spki).expect("valid ML-DSA SPKI should parse");
405+
406+
assert_eq!(from_spki, peer_id_from_public_key(&public_key));
407+
}
408+
409+
#[test]
410+
fn transport_spki_rejects_wrong_algorithm() {
411+
let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
412+
.expect("ML-DSA key generation should succeed");
413+
let mut spki = ml_dsa_65_spki(public_key.as_bytes());
414+
let oid_last_byte = 16;
415+
spki[oid_last_byte] = 0x11;
416+
417+
assert!(peer_id_from_public_key_spki(&spki).is_err());
418+
}
419+
420+
#[test]
421+
fn transport_spki_rejects_raw_public_key_bytes() {
422+
let (public_key, _secret_key) = crate::quantum_crypto::generate_ml_dsa_keypair()
423+
.expect("ML-DSA key generation should succeed");
424+
425+
assert!(peer_id_from_public_key_spki(public_key.as_bytes()).is_err());
426+
}
427+
348428
#[test]
349429
fn test_xor_distance() {
350430
let id1 = PeerId([0u8; 32]);

0 commit comments

Comments
 (0)