proto/discovery: sans-IO state-machine migration + no_std reductions

Migrate the FMP discovery decision logic out of the async handlers into
synchronous, runtime-agnostic sans-IO state machines owned by the protocol
structs, with I/O pushed to the edges. Pulls the full decision surface into a
pure core (backoff, rate-limit, planners, response routing), consolidates the
tests into a per-module tree with a shared crate testutil, and injects a u64
wall-clock so the core is free of Instant and std time.

Also brings the module toward no_std+alloc: the four discovery maps use
alloc::collections::BTreeMap (HashMap's RandomState is std-only), Arc is spelled
alloc::sync::Arc, the backoff-reset log lives in the shell (the core returns the
cleared count so observability stays out of the pure core), and the crate root
names alloc directly. The one remaining tether is ProtocolError's
std::error::Error coupling in the wire codec.

First subsystem of the broader sans-IO refactor; establishes the extraction
patterns and conventions carried forward to the remaining protocols.
This commit is contained in:
Johnathan Corgan
2026-07-05 21:59:21 +00:00
parent 1dbfefc9d0
commit e03b206f62
18 changed files with 2136 additions and 1039 deletions
+29 -23
View File
@@ -5,8 +5,7 @@
//! response routing.
use super::*;
use crate::node::RecentRequest;
use crate::protocol::{LookupRequest, LookupResponse};
use crate::proto::discovery::{LookupRequest, LookupResponse, RecentRequest};
use crate::tree::TreeCoordinate;
use spanning_tree::{
cleanup_nodes, generate_random_edges, lock_large_network_test, process_available_packets,
@@ -23,7 +22,7 @@ async fn test_request_decode_error() {
let from = make_node_addr(0xAA);
// Too-short payload: should log error and return without panic
node.handle_lookup_request(&from, &[0x00; 5]).await;
assert!(node.recent_requests.is_empty());
assert!(node.discovery.recent_requests.is_empty());
}
#[tokio::test]
@@ -39,11 +38,11 @@ async fn test_request_dedup() {
// First request: accepted
node.handle_lookup_request(&from, payload).await;
assert_eq!(node.recent_requests.len(), 1);
assert_eq!(node.discovery.recent_requests.len(), 1);
// Duplicate request: dropped
node.handle_lookup_request(&from, payload).await;
assert_eq!(node.recent_requests.len(), 1);
assert_eq!(node.discovery.recent_requests.len(), 1);
}
#[tokio::test]
@@ -61,7 +60,7 @@ async fn test_request_target_is_self() {
// Should succeed without panic (response send will fail silently
// since we have no peers to route toward origin)
node.handle_lookup_request(&from, payload).await;
assert!(node.recent_requests.contains_key(&777));
assert!(node.discovery.recent_requests.contains_key(&777));
}
#[tokio::test]
@@ -77,7 +76,7 @@ async fn test_request_ttl_zero_not_forwarded() {
node.handle_lookup_request(&from, payload).await;
// Request recorded, but not forwarded (TTL=0, and no peers anyway)
assert!(node.recent_requests.contains_key(&666));
assert!(node.discovery.recent_requests.contains_key(&666));
}
// ============================================================================
@@ -115,7 +114,7 @@ async fn test_response_originator_caches_route() {
let payload = &response.encode()[1..]; // skip msg_type
// No entry in recent_requests for 555 → we're the originator
assert!(!node.recent_requests.contains_key(&555));
assert!(!node.discovery.recent_requests.contains_key(&555));
node.handle_lookup_response(&from, payload).await;
@@ -149,7 +148,8 @@ async fn test_response_transit_needs_recent_request() {
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
node.recent_requests
node.discovery
.recent_requests
.insert(444, RecentRequest::new(make_node_addr(0xDD), now_ms));
// Handle response — should try to reverse-path forward to 0xDD
@@ -319,14 +319,16 @@ async fn test_recent_request_expiry() {
.as_millis() as u64;
// Insert an old request (11 seconds ago)
node.recent_requests
node.discovery
.recent_requests
.insert(123, RecentRequest::new(make_node_addr(1), now_ms - 11_000));
// Insert a recent request
node.recent_requests
node.discovery
.recent_requests
.insert(456, RecentRequest::new(make_node_addr(2), now_ms));
assert_eq!(node.recent_requests.len(), 2);
assert_eq!(node.discovery.recent_requests.len(), 2);
// Trigger purge via a new lookup request
let target = make_node_addr(0xBB);
@@ -338,9 +340,9 @@ async fn test_recent_request_expiry() {
.await;
// Old entry (123) should be purged, recent entry (456) and new entry (789) kept
assert!(!node.recent_requests.contains_key(&123));
assert!(node.recent_requests.contains_key(&456));
assert!(node.recent_requests.contains_key(&789));
assert!(!node.discovery.recent_requests.contains_key(&123));
assert!(node.discovery.recent_requests.contains_key(&456));
assert!(node.discovery.recent_requests.contains_key(&789));
}
// ============================================================================
@@ -379,7 +381,7 @@ async fn test_request_forwarding_two_node() {
// Node1 should have recorded the request
assert!(
nodes[1].node.recent_requests.contains_key(&42),
nodes[1].node.discovery.recent_requests.contains_key(&42),
"Node 1 should have recorded the forwarded request"
);
@@ -447,13 +449,13 @@ async fn test_request_three_node_chain() {
// Node1 should have been a transit node (has the request_id in recent_requests)
assert!(
!nodes[1].node.recent_requests.is_empty(),
!nodes[1].node.discovery.recent_requests.is_empty(),
"Node 1 should have recorded the forwarded request"
);
// Node2 should have received the request (it's the target)
assert!(
!nodes[2].node.recent_requests.is_empty(),
!nodes[2].node.discovery.recent_requests.is_empty(),
"Node 2 should have received the request"
);
@@ -502,7 +504,7 @@ async fn test_request_dedup_convergent_paths() {
// Node2 (the target) must have received the request
assert!(
nodes[2].node.recent_requests.contains_key(&300),
nodes[2].node.discovery.recent_requests.contains_key(&300),
"Node 2 (target) should have received the request"
);
@@ -1037,8 +1039,8 @@ async fn test_open_discovery_sweep_queues_eligible_skips_filtered() {
#[tokio::test]
async fn test_check_pending_lookups_default_sequence_unreachable() {
use crate::bloom::BloomFilter;
use crate::node::handlers::discovery::PendingLookup;
use crate::peer::ActivePeer;
use crate::proto::discovery::PendingLookup;
use crate::transport::LinkId;
use std::sync::mpsc;
@@ -1106,7 +1108,8 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
// Inject a PendingLookup directly: attempt=1, last_sent_ms=0. This
// mirrors the post-condition of a successful `maybe_initiate_lookup`
// at t=0 without depending on wall-clock-derived `Self::now_ms()`.
node.pending_lookups
node.discovery
.pending_lookups
.insert(target_addr, PendingLookup::new(0));
let baseline_initiated = node.metrics().discovery.req_initiated.get();
@@ -1116,6 +1119,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
node.check_pending_lookups(1100).await;
{
let entry = node
.discovery
.pending_lookups
.get(&target_addr)
.expect("still pending");
@@ -1132,6 +1136,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
node.check_pending_lookups(3100).await;
{
let entry = node
.discovery
.pending_lookups
.get(&target_addr)
.expect("still pending");
@@ -1148,6 +1153,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
node.check_pending_lookups(7100).await;
{
let entry = node
.discovery
.pending_lookups
.get(&target_addr)
.expect("still pending");
@@ -1163,7 +1169,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
// --- Just-before-final: at t=15099ms the 8s window is not yet reached ---
node.check_pending_lookups(15_099).await;
assert!(
node.pending_lookups.contains_key(&target_addr),
node.discovery.pending_lookups.contains_key(&target_addr),
"8s window not yet expired: pending_lookup must persist"
);
assert_eq!(
@@ -1186,7 +1192,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
// Pending lookup is dropped.
assert!(
!node.pending_lookups.contains_key(&target_addr),
!node.discovery.pending_lookups.contains_key(&target_addr),
"final timeout must remove the pending_lookups entry"
);
// resp_timed_out counter ticked.