#[cfg(debug_assertions)]
use std::backtrace::Backtrace as StdTrace;
use std::time::Duration;
use freenet_stdlib::prelude::{ContractInstanceId, ContractKey};
use tokio::sync::mpsc::error::SendError;
use crate::{
config::GlobalExecutor,
contract::{ContractError, ExecutorError},
message::{Transaction, TransactionType},
node::{ConnectionError, OpManager},
ring::{Location, PeerKeyLocation, RingError},
};
pub(crate) mod bootstrap;
pub(crate) mod connect;
pub(crate) mod get;
pub(crate) mod op_ctx;
pub(crate) mod orphan_streams;
pub(crate) mod put;
pub(crate) mod stream_progress;
pub(crate) mod subscribe;
#[cfg(test)]
pub(crate) mod test_utils;
pub(crate) mod update;
pub(crate) mod visited_peers;
pub(crate) use op_ctx::OpCtx;
pub(crate) use visited_peers::VisitedPeers;
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum OpOutcome<'a> {
ContractOpSuccess {
target_peer: &'a PeerKeyLocation,
contract_location: Location,
first_response_time: Duration,
payload_size: usize,
payload_transfer_time: Duration,
},
ContractOpSuccessUntimed {
target_peer: &'a PeerKeyLocation,
contract_location: Location,
},
ContractOpFailure {
target_peer: &'a PeerKeyLocation,
contract_location: Location,
},
Incomplete,
Irrelevant,
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum OpError {
#[error(transparent)]
ConnError(#[from] ConnectionError),
#[error(transparent)]
RingError(#[from] RingError),
#[error(transparent)]
ContractError(#[from] ContractError),
#[error(transparent)]
ExecutorError(#[from] ExecutorError),
#[error("unexpected operation state")]
UnexpectedOpState,
#[error(
"cannot perform a state transition from the current state with the provided input (tx: {tx})"
)]
InvalidStateTransition {
tx: Transaction,
#[cfg(debug_assertions)]
state: Option<Box<dyn std::fmt::Debug + Send + Sync>>,
#[cfg(debug_assertions)]
trace: StdTrace,
},
#[error("failed notifying, channel closed")]
NotificationError,
#[error("awaited peer {peer} disconnected before replying")]
PeerDisconnected { peer: std::net::SocketAddr },
#[error("notification channel error: {0}")]
NotificationChannelError(String),
#[allow(dead_code)]
#[error("unspected transaction type, trying to get a {0:?} from a {1:?}")]
IncorrectTxType(TransactionType, TransactionType),
#[allow(dead_code)]
#[error("op not present: {0}")]
OpNotPresent(Transaction),
#[error("stream was cancelled")]
StreamCancelled,
#[error("failed to claim orphan stream")]
OrphanStreamClaimFailed,
#[error("node is shutting down; client operation rejected")]
NodeShuttingDown,
#[error("contract {instance_id} is banned on this node; request rejected")]
ContractBanned { instance_id: ContractInstanceId },
}
impl OpError {
pub fn invalid_transition(tx: Transaction) -> Self {
Self::InvalidStateTransition {
tx,
#[cfg(debug_assertions)]
state: None,
#[cfg(debug_assertions)]
trace: StdTrace::force_capture(),
}
}
pub fn is_contract_exec_rejection(&self) -> bool {
matches!(self, Self::ExecutorError(e) if e.is_contract_exec_rejection())
}
pub fn is_missing_contract_parameters(&self) -> bool {
matches!(self, Self::ExecutorError(e) if e.is_missing_contract_parameters())
}
pub fn is_invalid_update_rejection(&self) -> bool {
matches!(self, Self::ExecutorError(e) if e.is_invalid_update_rejection())
}
pub fn is_contract_queue_full(&self) -> bool {
matches!(self, Self::ExecutorError(e) if e.is_contract_queue_full())
}
}
impl<T> From<SendError<T>> for OpError {
fn from(_: SendError<T>) -> OpError {
OpError::NotificationError
}
}
pub(crate) fn reject_if_contract_banned(
op_manager: &OpManager,
instance_id: &ContractInstanceId,
) -> Result<(), OpError> {
reject_if_contract_banned_on(&op_manager.ring.contract_ban_list, instance_id)
}
pub(crate) fn reject_if_contract_banned_on(
ban_list: &crate::ring::contract_ban_list::ContractBanList,
instance_id: &ContractInstanceId,
) -> Result<(), OpError> {
if ban_list.is_banned(instance_id) {
tracing::debug!(
%instance_id,
phase = "egress_banned_reject",
"rejecting client-originated request for banned contract"
);
return Err(OpError::ContractBanned {
instance_id: *instance_id,
});
}
Ok(())
}
pub(crate) async fn announce_contract_hosted(op_manager: &OpManager, key: &ContractKey) {
if let Some(announcement) = op_manager.neighbor_hosting.on_contract_hosted(key) {
tracing::debug!(
%key,
"NEIGHBOR_HOSTING: Announcing contract hosted to neighbors"
);
if let Err(err) = op_manager
.notify_node_event(crate::message::NodeEvent::BroadcastHostingUpdate {
message: announcement,
})
.await
{
tracing::warn!(
contract = %key,
error = %err,
phase = "error",
"NEIGHBOR_HOSTING: Failed to broadcast hosting announcement"
);
}
}
}
pub(crate) fn announce_contract_unhosted(op_manager: &OpManager, key: &ContractKey) {
if let Some(retraction) = op_manager.neighbor_hosting.on_contract_unhosted(key) {
tracing::debug!(
%key,
"NEIGHBOR_HOSTING: Retracting hosting advertisement to neighbors"
);
if let Err(err) =
op_manager.try_notify_node_event(crate::message::NodeEvent::BroadcastHostingUpdate {
message: retraction,
})
{
tracing::debug!(
contract = %key,
error = %err,
"NEIGHBOR_HOSTING: retraction broadcast dropped (best-effort; \
healed by periodic full-set re-request)"
);
}
}
}
pub(crate) fn consult_advertised_hosts(
op_manager: &OpManager,
instance_id: &ContractInstanceId,
max_hosts: usize,
is_excluded: impl Fn(std::net::SocketAddr) -> bool,
) -> Vec<(PeerKeyLocation, std::net::SocketAddr)> {
crate::config::GlobalTestMetrics::record_terminal_consult_attempt();
crate::node::network_status::record_terminal_consult_attempt();
let advertised: Vec<PeerKeyLocation> = op_manager
.neighbor_hosting
.neighbors_with_contract_id(instance_id)
.into_iter()
.filter_map(|pub_key| {
op_manager
.ring
.connection_manager
.get_peer_by_pub_key(&pub_key)
})
.collect();
let hosts = rank_advertised_hosts(
Location::from(instance_id),
advertised,
max_hosts,
is_excluded,
);
if !hosts.is_empty() {
crate::config::GlobalTestMetrics::record_terminal_consult_hit();
crate::node::network_status::record_terminal_consult_hit();
tracing::debug!(
%instance_id,
advertised_hosts = hosts.len(),
"TERMINAL_CONSULT: found advertised host(s) off the routing path"
);
}
hosts
}
fn rank_advertised_hosts(
target: Location,
advertised: Vec<PeerKeyLocation>,
max_hosts: usize,
is_excluded: impl Fn(std::net::SocketAddr) -> bool,
) -> Vec<(PeerKeyLocation, std::net::SocketAddr)> {
let mut candidates: Vec<(PeerKeyLocation, std::net::SocketAddr, Location)> = advertised
.into_iter()
.filter_map(|peer| {
let addr = peer.socket_addr()?;
let loc = peer.location()?;
if is_excluded(addr) {
return None;
}
Some((peer, addr, loc))
})
.collect();
candidates.sort_by(|(_, a_addr, a_loc), (_, b_addr, b_loc)| {
a_loc
.distance(target)
.partial_cmp(&b_loc.distance(target))
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a_addr.cmp(b_addr))
});
candidates.truncate(max_hosts);
candidates
.into_iter()
.map(|(peer, addr, _loc)| (peer, addr))
.collect()
}
pub(crate) fn record_terminal_consult_outcome(resolved_found: bool) {
if resolved_found {
crate::config::GlobalTestMetrics::record_terminal_consult_resolved_found();
crate::node::network_status::record_terminal_consult_resolved_found();
} else {
crate::config::GlobalTestMetrics::record_terminal_consult_still_not_found();
crate::node::network_status::record_terminal_consult_still_not_found();
}
}
pub(crate) fn reclaim_evicted_contract(
op_manager: &OpManager,
key: ContractKey,
expected_generation: u64,
) {
if op_manager.ring.contract_in_use(&key) {
tracing::debug!(
contract = %key,
"Skipping disk reclamation for evicted contract — still in use \
(client subscription or downstream subscriber); queued for retry"
);
op_manager
.ring
.pending_reclamation_add(key, expected_generation);
return;
}
op_manager.notify_contract_handler_fire_and_forget_prioritized(
crate::contract::ContractHandlerEvent::EvictContract {
key,
expected_generation,
},
crate::contract::Priority::Background,
);
}
pub(crate) async fn broadcast_change_interests(
op_manager: &OpManager,
added: Vec<ContractKey>,
removed: Vec<ContractKey>,
) {
use crate::ring::interest::contract_hash;
if added.is_empty() && removed.is_empty() {
return;
}
let added_hashes: Vec<u32> = added.iter().map(contract_hash).collect();
let removed_hashes: Vec<u32> = removed.iter().map(contract_hash).collect();
tracing::debug!(
added_count = added_hashes.len(),
removed_count = removed_hashes.len(),
"Broadcasting ChangeInterests to neighbors"
);
if let Err(err) =
op_manager.try_notify_node_event(crate::message::NodeEvent::BroadcastChangeInterests {
added: added_hashes,
removed: removed_hashes,
})
{
tracing::debug!(
error = %err,
"Failed to broadcast ChangeInterests (best-effort)"
);
}
}
pub(super) fn start_subscription_request(
op_manager: &OpManager,
parent_tx: Transaction,
key: ContractKey,
blocking: bool,
) -> Transaction {
let child_tx = Transaction::new_child_of::<subscribe::SubscribeMsg>(&parent_tx);
tracing::debug!(
%parent_tx,
%child_tx,
%key,
blocking,
"spawning child subscription operation (driver)"
);
let op_manager_arc = std::sync::Arc::new(op_manager.clone());
let instance_id = *key.id();
GlobalExecutor::spawn(async move {
subscribe::run_client_subscribe(op_manager_arc, instance_id, child_tx).await;
});
child_tx
}
pub(crate) async fn has_contract(
op_manager: &OpManager,
instance_id: ContractInstanceId,
) -> Result<Option<ContractKey>, OpError> {
match op_manager
.notify_contract_handler(crate::contract::ContractHandlerEvent::GetQuery {
instance_id,
return_contract_code: false,
})
.await?
{
crate::contract::ContractHandlerEvent::GetResponse {
key,
response: Ok(crate::contract::StoreResponse { state: Some(_), .. }),
} => Ok(key),
crate::contract::ContractHandlerEvent::DelegateRequest { .. }
| crate::contract::ContractHandlerEvent::DelegateResponse(_)
| crate::contract::ContractHandlerEvent::ExportUserSecrets { .. }
| crate::contract::ContractHandlerEvent::ExportUserSecretsResponse(_)
| crate::contract::ContractHandlerEvent::ImportSecrets { .. }
| crate::contract::ContractHandlerEvent::ImportSecretsResponse(_)
| crate::contract::ContractHandlerEvent::PutQuery { .. }
| crate::contract::ContractHandlerEvent::PutResponse { .. }
| crate::contract::ContractHandlerEvent::GetQuery { .. }
| crate::contract::ContractHandlerEvent::GetResponse { .. }
| crate::contract::ContractHandlerEvent::UpdateQuery { .. }
| crate::contract::ContractHandlerEvent::UpdateResponse { .. }
| crate::contract::ContractHandlerEvent::UpdateNoChange { .. }
| crate::contract::ContractHandlerEvent::RegisterSubscriberListener { .. }
| crate::contract::ContractHandlerEvent::RegisterSubscriberListenerResponse
| crate::contract::ContractHandlerEvent::QuerySubscriptions { .. }
| crate::contract::ContractHandlerEvent::QuerySubscriptionsResponse
| crate::contract::ContractHandlerEvent::GetSummaryQuery { .. }
| crate::contract::ContractHandlerEvent::GetSummaryResponse { .. }
| crate::contract::ContractHandlerEvent::GetDeltaQuery { .. }
| crate::contract::ContractHandlerEvent::GetDeltaResponse { .. }
| crate::contract::ContractHandlerEvent::ClientDisconnect { .. }
| crate::contract::ContractHandlerEvent::EvictContract { .. } => Ok(None),
}
}
pub(crate) fn should_use_streaming(streaming_threshold: usize, payload_size: usize) -> bool {
payload_size > streaming_threshold
}
const STREAMING_THROUGHPUT_FLOOR_BPS: usize = 20 * 1024;
const STREAMING_MIN_DRAIN_SECS: u64 = 30;
pub(crate) const STREAMING_ATTEMPT_TIMEOUT_CAP: std::time::Duration =
std::time::Duration::from_secs(600);
pub(crate) fn streaming_aware_attempt_timeout(
streaming_threshold: usize,
payload_size: usize,
) -> std::time::Duration {
if !should_use_streaming(streaming_threshold, payload_size) {
return crate::config::OPERATION_TTL;
}
let drain_secs =
((payload_size / STREAMING_THROUGHPUT_FLOOR_BPS) as u64).max(STREAMING_MIN_DRAIN_SECS);
let total = crate::config::OPERATION_TTL + std::time::Duration::from_secs(drain_secs);
total.min(STREAMING_ATTEMPT_TIMEOUT_CAP)
}
pub(crate) fn record_relay_route_event(
op_manager: &OpManager,
next_hop: PeerKeyLocation,
contract_location: Location,
outcome: crate::router::RouteOutcome,
op_type: crate::node::network_status::OpType,
) {
#[cfg(any(test, feature = "testing"))]
{
use std::sync::atomic::Ordering;
let counter = match op_type {
crate::node::network_status::OpType::Get => &RELAY_GET_ROUTE_EVENT_COUNT,
crate::node::network_status::OpType::Put => &RELAY_PUT_ROUTE_EVENT_COUNT,
crate::node::network_status::OpType::Update => &RELAY_UPDATE_ROUTE_EVENT_COUNT,
crate::node::network_status::OpType::Subscribe => &RELAY_SUBSCRIBE_ROUTE_EVENT_COUNT,
};
counter.fetch_add(1, Ordering::Relaxed);
}
op_manager
.ring
.router
.write()
.add_event(crate::router::RouteEvent {
peer: next_hop,
contract_location,
outcome,
op_type: Some(op_type),
});
}
#[cfg(any(test, feature = "testing"))]
pub static RELAY_GET_ROUTE_EVENT_COUNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[cfg(any(test, feature = "testing"))]
pub static RELAY_PUT_ROUTE_EVENT_COUNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[cfg(any(test, feature = "testing"))]
pub static RELAY_UPDATE_ROUTE_EVENT_COUNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[cfg(any(test, feature = "testing"))]
pub static RELAY_SUBSCRIBE_ROUTE_EVENT_COUNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[cfg(test)]
mod ordering_invariant_tests {
use super::test_utils::MockNetworkBridge;
use crate::message::{NetMessage, NetMessageV1, Transaction};
use crate::node::NetworkBridge;
use crate::operations::connect::ConnectMsg;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
#[tokio::test]
async fn mock_network_bridge_records_send_ordering() {
let bridge = MockNetworkBridge::new();
let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5000);
let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 5001);
let tx1 = Transaction::new::<ConnectMsg>();
let tx2 = Transaction::new::<ConnectMsg>();
bridge
.send(addr1, NetMessage::V1(NetMessageV1::Aborted(tx1)))
.await
.unwrap();
bridge
.send(addr2, NetMessage::V1(NetMessageV1::Aborted(tx2)))
.await
.unwrap();
let sent = bridge.sent_messages();
assert_eq!(sent.len(), 2);
assert_eq!(sent[0].0, addr1, "First send should be to addr1");
assert_eq!(sent[1].0, addr2, "Second send should be to addr2");
}
#[test]
fn push_before_send_invariant_is_documented() {
}
}
#[cfg(test)]
mod streaming_tests {
use super::{
STREAMING_ATTEMPT_TIMEOUT_CAP, should_use_streaming, streaming_aware_attempt_timeout,
};
use crate::config::OPERATION_TTL;
use std::time::Duration;
const DEFAULT_THRESHOLD: usize = 64 * 1024;
#[test]
fn test_streaming_respects_threshold() {
assert!(!should_use_streaming(DEFAULT_THRESHOLD, 0));
assert!(!should_use_streaming(DEFAULT_THRESHOLD, 1000));
assert!(!should_use_streaming(DEFAULT_THRESHOLD, DEFAULT_THRESHOLD)); assert!(should_use_streaming(
DEFAULT_THRESHOLD,
DEFAULT_THRESHOLD + 1
)); assert!(should_use_streaming(DEFAULT_THRESHOLD, 1024 * 1024)); }
#[test]
fn test_streaming_custom_threshold() {
let custom_threshold = 128 * 1024; assert!(!should_use_streaming(custom_threshold, 64 * 1024));
assert!(!should_use_streaming(custom_threshold, custom_threshold));
assert!(should_use_streaming(custom_threshold, custom_threshold + 1));
}
#[test]
fn test_streaming_zero_threshold() {
assert!(!should_use_streaming(0, 0));
assert!(should_use_streaming(0, 1));
assert!(should_use_streaming(0, 100));
}
#[test]
fn non_streaming_payload_uses_operation_ttl() {
assert_eq!(
streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, 0),
OPERATION_TTL
);
assert_eq!(
streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, 1024),
OPERATION_TTL
);
assert_eq!(
streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, DEFAULT_THRESHOLD),
OPERATION_TTL
);
}
#[test]
fn website_payload_attempt_timeout_exceeds_observed_completion() {
let website_payload_size = 2_460_242; let timeout = streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, website_payload_size);
let observed_completion = Duration::from_secs(63);
assert!(
timeout > observed_completion,
"streaming-aware timeout ({timeout:?}) must exceed observed \
completion time ({observed_completion:?}) so the retry loop \
does not fire while the original streaming PUT is still in \
flight (issue #4001)"
);
assert!(
timeout > OPERATION_TTL,
"streaming-aware timeout ({timeout:?}) must exceed OPERATION_TTL \
({OPERATION_TTL:?}); otherwise the fix is a no-op for the bug \
reported in #4001"
);
}
#[test]
fn streaming_timeout_scales_with_payload_size() {
let small_streaming = streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, 1_000_000);
let medium_streaming = streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, 10_000_000);
assert!(
small_streaming < medium_streaming,
"1 MB timeout ({small_streaming:?}) must be smaller than \
10 MB timeout ({medium_streaming:?})"
);
assert!(small_streaming > OPERATION_TTL);
assert!(medium_streaming > OPERATION_TTL);
}
#[test]
fn streaming_timeout_capped_at_ceiling() {
let huge = streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, 1024 * 1024 * 1024);
assert_eq!(
huge, STREAMING_ATTEMPT_TIMEOUT_CAP,
"huge payloads must clamp to the cap"
);
}
#[test]
fn streaming_timeout_jumps_above_threshold_boundary() {
let at_threshold = streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, DEFAULT_THRESHOLD);
let just_above = streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, DEFAULT_THRESHOLD + 1);
let in_truncation_gap =
streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, DEFAULT_THRESHOLD + 19 * 1024);
assert_eq!(at_threshold, OPERATION_TTL);
assert!(
just_above > OPERATION_TTL,
"just-above-threshold timeout {just_above:?} must STRICTLY \
exceed OPERATION_TTL ({OPERATION_TTL:?}); a fix that lets \
this equal OPERATION_TTL is a no-op for the size range \
(threshold, threshold + 20 KiB) — exactly the truncation \
gap STREAMING_MIN_DRAIN_SECS exists to close (#4001 \
skeptical review)"
);
assert!(
in_truncation_gap > OPERATION_TTL,
"payload in the truncation gap (threshold + 19 KiB) must \
exceed OPERATION_TTL — STREAMING_MIN_DRAIN_SECS guarantees it"
);
}
#[test]
fn streaming_timeout_min_drain_floor() {
let just_above = streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, DEFAULT_THRESHOLD + 1);
assert_eq!(
just_above,
OPERATION_TTL + Duration::from_secs(super::STREAMING_MIN_DRAIN_SECS),
"streaming-eligible payloads must get at least \
OPERATION_TTL + STREAMING_MIN_DRAIN_SECS"
);
}
#[test]
fn streaming_timeout_cap_boundary() {
const FLOOR_BPS: usize = 20 * 1024;
let scaling_max_bytes =
(STREAMING_ATTEMPT_TIMEOUT_CAP - OPERATION_TTL).as_secs() as usize * FLOOR_BPS;
let just_below_cap = streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, scaling_max_bytes);
let at_cap = streaming_aware_attempt_timeout(DEFAULT_THRESHOLD, scaling_max_bytes + 1);
assert_eq!(just_below_cap, STREAMING_ATTEMPT_TIMEOUT_CAP);
assert_eq!(at_cap, STREAMING_ATTEMPT_TIMEOUT_CAP);
}
}
#[cfg(test)]
mod sub_op_subscribe_pin_tests {
fn extract_start_subscription_request_body() -> &'static str {
let src = include_str!("operations.rs");
let head = ["fn ", "start_subscription_request("].concat();
let start = src
.find(&head)
.expect("`fn start_subscription_request(` must exist in operations.rs");
let body_open = src[start..]
.find('{')
.map(|off| start + off)
.expect("expected `{` after function signature");
let mut depth: i32 = 0;
let mut end = body_open;
for (i, ch) in src[body_open..].char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = body_open + i + 1;
break;
}
}
_ => {}
}
}
assert!(
end > body_open,
"failed to find matching `}}` for start_subscription_request"
);
&src[start..end]
}
#[test]
fn start_subscription_request_uses_run_client_subscribe() {
let body = extract_start_subscription_request_body();
assert!(
!body.contains("subscribe::request_subscribe"),
"`start_subscription_request` must route through \
`subscribe::run_client_subscribe`, not `request_subscribe`."
);
}
#[test]
fn start_subscription_request_does_not_register_with_sub_op_tracker() {
let body = extract_start_subscription_request_body();
assert!(
!body.contains("expect_and_register_sub_operation"),
"`start_subscription_request` must NOT register with a \
sub-operation tracker."
);
assert!(
!body.contains("sub_operation_failed"),
"`start_subscription_request` must NOT propagate failures \
via `sub_operation_failed` — the subscribe driver \
publishes its own `HostResult::Err`."
);
}
#[test]
fn start_subscription_request_spawns_driver_driver() {
let body = extract_start_subscription_request_body();
assert!(
body.contains("subscribe::run_client_subscribe"),
"`start_subscription_request` must spawn the driver \
subscribe driver `subscribe::run_client_subscribe` — \
matches the `maybe_subscribe_child` pattern in \
`put/op_ctx_task.rs` and `get/op_ctx_task.rs`."
);
}
}
#[cfg(test)]
mod reclaim_retraction_pin_tests {
fn extract_fn_body(src: &'static str, needle: &str) -> &'static str {
let start = src
.find(needle)
.unwrap_or_else(|| panic!("`{needle}` must exist"));
let body_open = src[start..]
.find('{')
.map(|off| start + off)
.expect("expected `{` after function signature");
let mut depth: i32 = 0;
let mut end = body_open;
for (i, ch) in src[body_open..].char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = body_open + i + 1;
break;
}
}
_ => {}
}
}
assert!(
end > body_open,
"failed to find matching `}}` for `{needle}`"
);
&src[start..end]
}
#[test]
fn reclaim_evicted_contract_does_not_retract_at_the_decision_site() {
let src = include_str!("operations.rs");
let needle = ["fn ", "reclaim_evicted_contract("].concat();
let body = extract_fn_body(src, &needle);
assert!(
!body.contains("announce_contract_unhosted("),
"`reclaim_evicted_contract` must NOT retract at the eviction decision — \
the deletion-time guards may skip the delete, which would leave the \
node holding fresh state it no longer advertises. The retraction is \
wired on the confirmed-delete path in `RuntimePool::remove_contract`."
);
}
#[test]
fn remove_contract_retracts_on_any_confirmed_delete_not_on_err() {
let src = include_str!("contract/executor/runtime/pool.rs");
let needle = ["fn ", "remove_contract("].concat();
let body = extract_fn_body(src, &needle);
assert!(
body.contains("announce_contract_unhosted("),
"`RuntimePool::remove_contract` must retract the hosting advertisement \
on the confirmed delete (Fix 1, invariant 1)."
);
assert!(
body.contains("ReclaimOutcome::Full | ReclaimOutcome::Partial"),
"the retraction must be gated on `ReclaimOutcome::Full | ReclaimOutcome::\
Partial` (any required half gone) — a `Partial` half-delete can no \
longer be served, so it must retract too; `Err` (nothing deleted) must \
not."
);
let outcome = body
.find("ReclaimOutcome::Full")
.expect("the reclaim-outcome match must exist");
let retract = body
.find("announce_contract_unhosted(")
.expect("the retraction call must exist");
assert!(
retract > outcome,
"the retraction must come after the reclaim-outcome match (post-guards), \
so a guard-bail does NOT retract a contract whose state is still present"
);
}
}
#[cfg(test)]
mod egress_banned_gate_tests {
use super::{OpError, reject_if_contract_banned_on};
use crate::ring::contract_ban_list::{BanReason, ContractBanList};
use crate::util::time_source::{SharedMockTimeSource, TimeSource};
use freenet_stdlib::prelude::ContractInstanceId;
use std::sync::Arc;
use std::time::Duration;
fn mk_contract(byte: u8) -> ContractInstanceId {
ContractInstanceId::new([byte; 32])
}
fn mk_ban_list() -> (ContractBanList, SharedMockTimeSource) {
let ts = SharedMockTimeSource::new();
let bl = ContractBanList::new(Arc::new(ts.clone()));
(bl, ts)
}
#[test]
fn banned_contract_is_rejected_with_typed_error() {
let (bl, ts) = mk_ban_list();
let banned = mk_contract(1);
bl.ban(
banned,
ts.now() + Duration::from_secs(60),
BanReason::AutoMad,
);
match reject_if_contract_banned_on(&bl, &banned) {
Err(OpError::ContractBanned { instance_id }) => {
assert_eq!(
instance_id, banned,
"the rejected id must be the banned contract's id, not some other id"
);
}
other => panic!(
"banned contract must be rejected with OpError::ContractBanned, got {other:?}"
),
}
}
#[test]
fn unbanned_contract_passes() {
let (bl, _ts) = mk_ban_list();
assert!(
reject_if_contract_banned_on(&bl, &mk_contract(2)).is_ok(),
"a contract that is not banned must pass the egress gate"
);
}
#[test]
fn ban_is_scoped_to_the_specific_contract_id() {
let (bl, ts) = mk_ban_list();
let banned = mk_contract(1);
let other = mk_contract(2);
bl.ban(
banned,
ts.now() + Duration::from_secs(60),
BanReason::AutoMad,
);
assert!(
reject_if_contract_banned_on(&bl, &banned).is_err(),
"the banned contract must be rejected"
);
assert!(
reject_if_contract_banned_on(&bl, &other).is_ok(),
"a different, unbanned contract must NOT be rejected by another contract's ban"
);
}
#[test]
fn expired_ban_no_longer_rejects() {
let (bl, ts) = mk_ban_list();
let contract = mk_contract(1);
bl.ban(
contract,
ts.now() + Duration::from_secs(60),
BanReason::AutoMad,
);
assert!(
reject_if_contract_banned_on(&bl, &contract).is_err(),
"contract must be rejected while the ban is active"
);
ts.advance_time(Duration::from_secs(61));
assert!(
reject_if_contract_banned_on(&bl, &contract).is_ok(),
"contract must pass the gate once its ban TTL has expired"
);
}
}
#[cfg(test)]
mod terminal_consult_tests {
use super::rank_advertised_hosts;
use crate::ring::{Location, PeerKeyLocation};
use crate::transport::TransportKeypair;
use std::net::SocketAddr;
fn peer_at(addr: &str) -> PeerKeyLocation {
let addr: SocketAddr = addr.parse().unwrap();
PeerKeyLocation::new(TransportKeypair::new().public().clone(), addr)
}
#[test]
fn ranks_closest_advertised_host_to_key_first() {
let a = peer_at("127.0.0.1:1000");
let b = peer_at("127.0.0.1:2000");
let c = peer_at("127.0.0.1:3000");
let target = b.location().unwrap();
let ranked =
rank_advertised_hosts(target, vec![a.clone(), b.clone(), c.clone()], 3, |_| false);
assert_eq!(ranked.len(), 3);
assert_eq!(
ranked[0].0.pub_key(),
b.pub_key(),
"the advertised host closest to the key must be returned first"
);
}
#[test]
fn truncates_to_max_hosts() {
let peers = vec![
peer_at("127.0.0.1:1000"),
peer_at("127.0.0.1:2000"),
peer_at("127.0.0.1:3000"),
];
let target = Location::from_address(&"127.0.0.1:9000".parse().unwrap());
let ranked = rank_advertised_hosts(target, peers, 2, |_| false);
assert_eq!(ranked.len(), 2, "must return at most max_hosts candidates");
}
#[test]
fn excludes_filtered_addresses() {
let keep = peer_at("127.0.0.1:1000");
let skip = peer_at("127.0.0.1:2000");
let skip_addr = skip.socket_addr().unwrap();
let target = Location::from_address(&"127.0.0.1:9000".parse().unwrap());
let ranked =
rank_advertised_hosts(target, vec![keep.clone(), skip.clone()], 5, move |addr| {
addr == skip_addr
});
assert_eq!(
ranked.len(),
1,
"the excluded (already-visited) host must be dropped"
);
assert_eq!(ranked[0].0.pub_key(), keep.pub_key());
assert!(
ranked.iter().all(|(_, addr)| *addr != skip_addr),
"the excluded address must never be returned as a forward target"
);
}
#[test]
fn empty_when_no_advertised_hosts() {
let target = Location::from_address(&"127.0.0.1:9000".parse().unwrap());
let ranked = rank_advertised_hosts(target, vec![], 2, |_| false);
assert!(ranked.is_empty());
}
#[test]
fn terminal_consult_metrics_reset_and_increment() {
use crate::config::GlobalTestMetrics;
GlobalTestMetrics::reset();
assert_eq!(GlobalTestMetrics::terminal_consult_attempts(), 0);
assert_eq!(GlobalTestMetrics::terminal_consult_hits(), 0);
assert_eq!(GlobalTestMetrics::terminal_consult_resolved_found(), 0);
assert_eq!(GlobalTestMetrics::terminal_consult_still_not_found(), 0);
GlobalTestMetrics::record_terminal_consult_attempt();
GlobalTestMetrics::record_terminal_consult_hit();
super::record_terminal_consult_outcome(true);
super::record_terminal_consult_outcome(false);
assert_eq!(GlobalTestMetrics::terminal_consult_attempts(), 1);
assert_eq!(GlobalTestMetrics::terminal_consult_hits(), 1);
assert_eq!(GlobalTestMetrics::terminal_consult_resolved_found(), 1);
assert_eq!(GlobalTestMetrics::terminal_consult_still_not_found(), 1);
GlobalTestMetrics::reset();
}
}