use std::{collections::HashSet, sync::Arc};
use dashmap::{DashMap, DashSet};
use freenet_stdlib::prelude::{ContractInstanceId, ContractKey};
use tracing::{debug, info, trace};
use crate::message::NeighborHostingMessage;
use crate::transport::TransportPublicKey;
pub struct NeighborHostingResult {
pub response: Option<NeighborHostingMessage>,
pub overlapping_contracts: Vec<ContractInstanceId>,
}
impl NeighborHostingResult {
fn response_only(response: NeighborHostingMessage) -> Self {
Self {
response: Some(response),
overlapping_contracts: vec![],
}
}
fn empty() -> Self {
Self {
response: None,
overlapping_contracts: vec![],
}
}
}
pub struct NeighborHostingManager {
my_contracts: Arc<DashSet<ContractInstanceId>>,
neighbor_contracts: DashMap<TransportPublicKey, HashSet<ContractInstanceId>>,
}
impl Default for NeighborHostingManager {
fn default() -> Self {
Self::new()
}
}
impl NeighborHostingManager {
pub fn new() -> Self {
Self {
my_contracts: Arc::new(DashSet::new()),
neighbor_contracts: DashMap::new(),
}
}
pub fn on_contract_hosted(&self, contract_key: &ContractKey) -> Option<NeighborHostingMessage> {
let contract_id = *contract_key.id();
if self.my_contracts.insert(contract_id) {
info!(
contract = %contract_key,
"NEIGHBOR_HOSTING: Added contract to locally hosted"
);
Some(NeighborHostingMessage::HostingAnnounce {
added: vec![contract_id],
removed: vec![],
is_response: false,
})
} else {
trace!(
contract = %contract_key,
"NEIGHBOR_HOSTING: Contract already hosted locally"
);
None
}
}
pub fn on_contract_unhosted(
&self,
contract_key: &ContractKey,
) -> Option<NeighborHostingMessage> {
let contract_id = *contract_key.id();
if self.my_contracts.remove(&contract_id).is_some() {
debug!(
contract = %contract_key,
"NEIGHBOR_HOSTING: Removed contract from locally hosted (retracting advertisement)"
);
crate::config::GlobalTestMetrics::record_neighbor_hosting_retraction();
Some(NeighborHostingMessage::HostingAnnounce {
added: vec![],
removed: vec![contract_id],
is_response: false,
})
} else {
None
}
}
pub fn handle_message(
&self,
from: &TransportPublicKey,
message: NeighborHostingMessage,
) -> NeighborHostingResult {
match message {
NeighborHostingMessage::HostingAnnounce {
added,
removed,
is_response,
} => {
let previously_known: HashSet<ContractInstanceId> = self
.neighbor_contracts
.get(from)
.map(
|entry: dashmap::mapref::one::Ref<
'_,
TransportPublicKey,
HashSet<ContractInstanceId>,
>| entry.value().clone(),
)
.unwrap_or_default();
if let Some(mut entry) = self.neighbor_contracts.get_mut(from) {
entry.extend(added.iter().copied());
for id in &removed {
entry.remove(id);
}
} else if !added.is_empty() {
self.neighbor_contracts
.insert(from.clone(), added.iter().copied().collect());
}
crate::config::GlobalTestMetrics::record_neighbor_hosting_update();
let neighbor_contracts: usize = self
.neighbor_contracts
.get(from)
.map(
|entry: dashmap::mapref::one::Ref<
'_,
TransportPublicKey,
HashSet<ContractInstanceId>,
>| entry.value().len(),
)
.unwrap_or(0);
info!(
peer = %from,
total_contracts = neighbor_contracts,
is_response = is_response,
"NEIGHBOR_HOSTING: Updated neighbor hosting state"
);
if is_response {
return NeighborHostingResult::empty();
}
let overlapping: Vec<ContractInstanceId> = added
.iter()
.filter(|id| !previously_known.contains(id)) .filter(|id| self.my_contracts.contains(*id)) .copied()
.collect();
let response = if !overlapping.is_empty() {
debug!(
peer = %from,
overlapping_count = overlapping.len(),
"NEIGHBOR_HOSTING: Responding with reciprocal announcement for shared contracts"
);
Some(NeighborHostingMessage::HostingAnnounce {
added: overlapping.clone(),
removed: vec![],
is_response: true, })
} else {
None
};
NeighborHostingResult {
response,
overlapping_contracts: overlapping,
}
}
NeighborHostingMessage::HostingStateRequest => {
let mut contracts: Vec<ContractInstanceId> =
self.my_contracts.iter().map(|r| *r.key()).collect();
contracts.sort_by_key(|a| a.to_string());
debug!(
peer = %from,
hosted_count = contracts.len(),
"NEIGHBOR_HOSTING: Responding to hosting state request"
);
NeighborHostingResult::response_only(NeighborHostingMessage::HostingStateResponse {
contracts,
})
}
NeighborHostingMessage::HostingStateResponse { contracts } => {
let count = contracts.len();
let previously_known: HashSet<ContractInstanceId> = self
.neighbor_contracts
.get(from)
.map(|entry| entry.value().clone())
.unwrap_or_default();
self.neighbor_contracts
.insert(from.clone(), contracts.iter().copied().collect());
crate::config::GlobalTestMetrics::record_neighbor_hosting_update();
let overlapping: Vec<ContractInstanceId> = contracts
.iter()
.filter(|id| !previously_known.contains(*id)) .filter(|id| self.my_contracts.contains(*id)) .copied()
.collect();
info!(
peer = %from,
contracts = count,
overlapping = overlapping.len(),
"NEIGHBOR_HOSTING: Received full hosting state from neighbor"
);
let response = if !overlapping.is_empty() {
debug!(
peer = %from,
overlapping_count = overlapping.len(),
"NEIGHBOR_HOSTING: Announcing our overlapping contracts to peer"
);
Some(NeighborHostingMessage::HostingAnnounce {
added: overlapping.clone(),
removed: vec![],
is_response: true, })
} else {
None
};
NeighborHostingResult {
response,
overlapping_contracts: overlapping,
}
}
}
}
pub fn neighbors_with_contract(&self, contract_key: &ContractKey) -> Vec<TransportPublicKey> {
self.neighbors_with_contract_id(contract_key.id())
}
pub fn neighbors_with_contract_id(
&self,
contract_id: &ContractInstanceId,
) -> Vec<TransportPublicKey> {
let mut neighbors: Vec<TransportPublicKey> = self
.neighbor_contracts
.iter()
.filter(
|entry: &dashmap::mapref::multiple::RefMulti<
'_,
TransportPublicKey,
HashSet<ContractInstanceId>,
>| entry.value().contains(contract_id),
)
.map(
|entry: dashmap::mapref::multiple::RefMulti<
'_,
TransportPublicKey,
HashSet<ContractInstanceId>,
>| entry.key().clone(),
)
.collect();
neighbors.sort();
if !neighbors.is_empty() {
debug!(
contract = %contract_id,
neighbor_count = neighbors.len(),
"NEIGHBOR_HOSTING: Found neighbors with contract"
);
}
neighbors
}
pub(crate) fn contracts_for_peer(
&self,
pub_key: &TransportPublicKey,
) -> HashSet<ContractInstanceId> {
self.neighbor_contracts
.get(pub_key)
.map(|entry| entry.value().clone())
.unwrap_or_default()
}
pub fn on_peer_disconnected(&self, pub_key: &TransportPublicKey) {
if let Some((_, removed_cache)) = self.neighbor_contracts.remove(pub_key) {
let count = removed_cache.len();
debug!(
peer = %pub_key,
hosted_contracts = count,
"NEIGHBOR_HOSTING: Removed disconnected peer from neighbor hosting"
);
}
}
pub fn on_ring_connection_established(
&self,
pub_key: &TransportPublicKey,
) -> Option<NeighborHostingMessage> {
debug!(
peer = %pub_key,
"NEIGHBOR_HOSTING: New ring connection, requesting hosting state"
);
Some(NeighborHostingMessage::HostingStateRequest)
}
pub fn initialize_from_hosting_cache(
&self,
contract_ids: impl Iterator<Item = ContractInstanceId>,
) {
let mut count = 0;
for id in contract_ids {
self.my_contracts.insert(id);
count += 1;
}
if count > 0 {
info!(
count,
"NEIGHBOR_HOSTING: Initialized local hosting from persisted contracts"
);
}
}
#[allow(dead_code)]
pub fn is_hosted_locally(&self, contract_key: &ContractKey) -> bool {
self.my_contracts.contains(contract_key.id())
}
#[allow(dead_code)]
pub fn local_hosted_count(&self) -> usize {
self.my_contracts.len()
}
#[allow(dead_code)]
pub fn neighbor_count(&self) -> usize {
self.neighbor_contracts.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::TransportKeypair;
use freenet_stdlib::prelude::{CodeHash, ContractInstanceId};
fn test_contract_key() -> ContractKey {
ContractKey::from_id_and_code(ContractInstanceId::new([1u8; 32]), CodeHash::new([2u8; 32]))
}
fn test_contract_key_2() -> ContractKey {
ContractKey::from_id_and_code(ContractInstanceId::new([3u8; 32]), CodeHash::new([4u8; 32]))
}
fn make_pub_key(_seed: u8) -> TransportPublicKey {
TransportKeypair::new().public().clone()
}
#[test]
fn test_hosting_announcement_on_new_contract() {
let manager = NeighborHostingManager::new();
let key = test_contract_key();
let announcement = manager.on_contract_hosted(&key);
assert!(announcement.is_some());
if let Some(NeighborHostingMessage::HostingAnnounce {
added,
removed,
is_response,
}) = announcement
{
assert_eq!(added.len(), 1);
assert!(removed.is_empty());
assert_eq!(added[0], *key.id());
assert!(
!is_response,
"Initial announcement should not be a response"
);
} else {
panic!("Expected HostingAnnounce");
}
assert!(manager.on_contract_hosted(&key).is_none());
}
#[test]
fn test_contract_unhosting() {
let manager = NeighborHostingManager::new();
let key = test_contract_key();
manager.on_contract_hosted(&key);
let announcement = manager.on_contract_unhosted(&key);
assert!(announcement.is_some());
if let Some(NeighborHostingMessage::HostingAnnounce {
added,
removed,
is_response,
}) = announcement
{
assert!(added.is_empty());
assert_eq!(removed.len(), 1);
assert_eq!(removed[0], *key.id());
assert!(
!is_response,
"Eviction announcement should not be a response"
);
} else {
panic!("Expected HostingAnnounce");
}
assert!(manager.on_contract_unhosted(&key).is_none());
}
#[test]
fn test_neighbor_hosting_tracking() {
let manager = NeighborHostingManager::new();
let key = test_contract_key();
let neighbor = make_pub_key(1);
assert!(manager.neighbors_with_contract(&key).is_empty());
let msg = NeighborHostingMessage::HostingAnnounce {
added: vec![*key.id()],
removed: vec![],
is_response: false,
};
manager.handle_message(&neighbor, msg);
let neighbors = manager.neighbors_with_contract(&key);
assert_eq!(neighbors.len(), 1);
assert_eq!(neighbors[0], neighbor);
}
#[test]
fn test_neighbors_with_contract_id_matches_key_lookup() {
let manager = NeighborHostingManager::new();
let key = test_contract_key();
let neighbor = make_pub_key(1);
assert!(manager.neighbors_with_contract_id(key.id()).is_empty());
manager.handle_message(
&neighbor,
NeighborHostingMessage::HostingAnnounce {
added: vec![*key.id()],
removed: vec![],
is_response: false,
},
);
let by_id = manager.neighbors_with_contract_id(key.id());
let by_key = manager.neighbors_with_contract(&key);
assert_eq!(by_id, by_key);
assert_eq!(by_id.len(), 1);
assert_eq!(by_id[0], neighbor);
assert!(
manager
.neighbors_with_contract_id(test_contract_key_2().id())
.is_empty()
);
}
#[test]
fn test_hosting_state_request_response() {
let manager = NeighborHostingManager::new();
let key1 = test_contract_key();
let key2 = test_contract_key_2();
let neighbor = make_pub_key(1);
manager.on_contract_hosted(&key1);
manager.on_contract_hosted(&key2);
let result = manager.handle_message(&neighbor, NeighborHostingMessage::HostingStateRequest);
assert!(result.response.is_some());
if let Some(NeighborHostingMessage::HostingStateResponse { contracts }) = result.response {
assert_eq!(contracts.len(), 2);
assert!(contracts.contains(key1.id()));
assert!(contracts.contains(key2.id()));
} else {
panic!("Expected HostingStateResponse");
}
}
#[test]
fn test_peer_disconnection() {
let manager = NeighborHostingManager::new();
let key = test_contract_key();
let neighbor = make_pub_key(1);
let msg = NeighborHostingMessage::HostingAnnounce {
added: vec![*key.id()],
removed: vec![],
is_response: false,
};
manager.handle_message(&neighbor, msg);
assert_eq!(manager.neighbor_count(), 1);
manager.on_peer_disconnected(&neighbor);
assert_eq!(manager.neighbor_count(), 0);
assert!(manager.neighbors_with_contract(&key).is_empty());
}
fn test_contract_key_3() -> ContractKey {
ContractKey::from_id_and_code(ContractInstanceId::new([5u8; 32]), CodeHash::new([6u8; 32]))
}
#[test]
fn test_bidirectional_announcement_for_overlapping_contracts() {
let manager_a = NeighborHostingManager::new();
let key = test_contract_key();
let node_b = make_pub_key(2);
manager_a.on_contract_hosted(&key);
let announcement_from_b = NeighborHostingMessage::HostingAnnounce {
added: vec![*key.id()],
removed: vec![],
is_response: false,
};
let result = manager_a.handle_message(&node_b, announcement_from_b);
assert!(
result.response.is_some(),
"Expected reciprocal announcement for overlapping contract"
);
assert_eq!(result.overlapping_contracts.len(), 1);
assert_eq!(result.overlapping_contracts[0], *key.id());
if let Some(NeighborHostingMessage::HostingAnnounce {
added,
removed,
is_response,
}) = result.response
{
assert_eq!(
added.len(),
1,
"Should announce the one overlapping contract"
);
assert_eq!(added[0], *key.id());
assert!(removed.is_empty());
assert!(
is_response,
"Reciprocal announcement should be marked as response"
);
} else {
panic!("Expected HostingAnnounce response");
}
}
#[test]
fn test_no_response_for_non_overlapping_contracts() {
let manager_a = NeighborHostingManager::new();
let key_x = test_contract_key();
let key_y = test_contract_key_2();
let node_b = make_pub_key(2);
manager_a.on_contract_hosted(&key_x);
let announcement = NeighborHostingMessage::HostingAnnounce {
added: vec![*key_y.id()],
removed: vec![],
is_response: false,
};
let result = manager_a.handle_message(&node_b, announcement);
assert!(
result.response.is_none(),
"No response expected when contracts don't overlap"
);
assert!(
result.overlapping_contracts.is_empty(),
"No overlapping contracts when contracts don't match"
);
}
#[test]
fn test_partial_overlap_only_announces_overlapping() {
let manager_a = NeighborHostingManager::new();
let key_x = test_contract_key();
let key_y = test_contract_key_2();
let key_z = test_contract_key_3();
let node_b = make_pub_key(2);
manager_a.on_contract_hosted(&key_x);
manager_a.on_contract_hosted(&key_y);
let announcement = NeighborHostingMessage::HostingAnnounce {
added: vec![*key_x.id(), *key_z.id()],
removed: vec![],
is_response: false,
};
let result = manager_a.handle_message(&node_b, announcement);
assert!(
result.response.is_some(),
"Expected response for partial overlap"
);
assert_eq!(
result.overlapping_contracts.len(),
1,
"Only contract X should overlap"
);
assert_eq!(result.overlapping_contracts[0], *key_x.id());
if let Some(NeighborHostingMessage::HostingAnnounce {
added,
removed,
is_response,
}) = result.response
{
assert_eq!(
added.len(),
1,
"Should only announce the overlapping contract X"
);
assert_eq!(added[0], *key_x.id());
assert!(removed.is_empty());
assert!(is_response, "Response should be marked as response");
} else {
panic!("Expected HostingAnnounce response");
}
}
#[test]
fn test_ping_pong_terminates_after_one_round() {
let manager_a = NeighborHostingManager::new();
let manager_b = NeighborHostingManager::new();
let key = test_contract_key();
let key_a = make_pub_key(1);
let key_b = make_pub_key(2);
manager_a.on_contract_hosted(&key);
manager_b.on_contract_hosted(&key);
let a_announcement = NeighborHostingMessage::HostingAnnounce {
added: vec![*key.id()],
removed: vec![],
is_response: false,
};
let b_result = manager_b.handle_message(&key_a, a_announcement);
assert!(
b_result.response.is_some(),
"B should respond with reciprocal announcement"
);
if let Some(NeighborHostingMessage::HostingAnnounce { is_response, .. }) =
&b_result.response
{
assert!(is_response, "B's response should have is_response=true");
}
let a_second_result = manager_a.handle_message(&key_b, b_result.response.unwrap());
assert!(
a_second_result.response.is_none(),
"Ping-pong must terminate: A should not respond to B's reciprocal announcement"
);
assert!(
a_second_result.overlapping_contracts.is_empty(),
"is_response=true must not trigger proactive state sync"
);
}
#[test]
fn test_ping_pong_with_multiple_contracts() {
let manager_a = NeighborHostingManager::new();
let manager_b = NeighborHostingManager::new();
let key_x = test_contract_key();
let key_y = test_contract_key_2();
let key_a = make_pub_key(1);
let key_b = make_pub_key(2);
manager_a.on_contract_hosted(&key_x);
manager_a.on_contract_hosted(&key_y);
manager_b.on_contract_hosted(&key_x);
manager_b.on_contract_hosted(&key_y);
let a_announcement = NeighborHostingMessage::HostingAnnounce {
added: vec![*key_x.id(), *key_y.id()],
removed: vec![],
is_response: false,
};
let b_result = manager_b.handle_message(&key_a, a_announcement);
assert!(b_result.response.is_some());
if let Some(NeighborHostingMessage::HostingAnnounce {
ref added,
is_response,
..
}) = b_result.response
{
assert_eq!(
added.len(),
2,
"B should announce both overlapping contracts"
);
assert!(is_response, "B's response should have is_response=true");
}
let a_second = manager_a.handle_message(&key_b, b_result.response.unwrap());
assert!(
a_second.response.is_none(),
"Ping-pong must terminate with multiple contracts"
);
}
#[test]
fn test_new_contract_after_initial_exchange_triggers_response() {
let manager_a = NeighborHostingManager::new();
let key_x = test_contract_key();
let key_y = test_contract_key_2();
let key_b = make_pub_key(2);
manager_a.on_contract_hosted(&key_x);
manager_a.on_contract_hosted(&key_y);
let b_announces_x = NeighborHostingMessage::HostingAnnounce {
added: vec![*key_x.id()],
removed: vec![],
is_response: false,
};
let result_x = manager_a.handle_message(&key_b, b_announces_x);
assert!(
result_x.response.is_some(),
"A should respond to initial X announcement"
);
let b_announces_y = NeighborHostingMessage::HostingAnnounce {
added: vec![*key_y.id()],
removed: vec![],
is_response: false,
};
let result_y = manager_a.handle_message(&key_b, b_announces_y);
assert!(
result_y.response.is_some(),
"A should respond to NEW contract Y even after X exchange completed"
);
if let Some(NeighborHostingMessage::HostingAnnounce { added, .. }) = result_y.response {
assert_eq!(added.len(), 1);
assert_eq!(added[0], *key_y.id(), "Response should be for contract Y");
}
}
#[test]
fn test_re_announcing_known_contract_no_response() {
let manager_a = NeighborHostingManager::new();
let key = test_contract_key();
let key_b = make_pub_key(2);
manager_a.on_contract_hosted(&key);
let announcement = NeighborHostingMessage::HostingAnnounce {
added: vec![*key.id()],
removed: vec![],
is_response: false,
};
let first_result = manager_a.handle_message(&key_b, announcement.clone());
assert!(
first_result.response.is_some(),
"First announcement should get response"
);
let second_result = manager_a.handle_message(&key_b, announcement);
assert!(
second_result.response.is_none(),
"Duplicate announcement should not trigger response (already known)"
);
}
#[test]
fn test_bidirectional_awareness_enables_update_forwarding() {
let manager_a = NeighborHostingManager::new();
let manager_b = NeighborHostingManager::new();
let key = test_contract_key();
let key_a = make_pub_key(1);
let key_b = make_pub_key(2);
manager_a.on_contract_hosted(&key);
manager_b.on_contract_hosted(&key);
assert!(
manager_a.neighbors_with_contract(&key).is_empty(),
"A shouldn't know about B yet"
);
assert!(
manager_b.neighbors_with_contract(&key).is_empty(),
"B shouldn't know about A yet"
);
let a_announcement = NeighborHostingMessage::HostingAnnounce {
added: vec![*key.id()],
removed: vec![],
is_response: false,
};
let b_result = manager_b.handle_message(&key_a, a_announcement);
let b_neighbors = manager_b.neighbors_with_contract(&key);
assert_eq!(b_neighbors.len(), 1);
assert_eq!(b_neighbors[0], key_a);
assert!(
manager_a.neighbors_with_contract(&key).is_empty(),
"A still shouldn't know about B"
);
assert!(b_result.response.is_some());
let _ = manager_a.handle_message(&key_b, b_result.response.unwrap());
let a_neighbors = manager_a.neighbors_with_contract(&key);
assert_eq!(a_neighbors.len(), 1);
assert_eq!(a_neighbors[0], key_b);
}
#[test]
fn test_hosting_state_response_triggers_announcement_for_overlapping_contracts() {
let manager_a = NeighborHostingManager::new();
let key = test_contract_key();
let key_b = make_pub_key(2);
manager_a.on_contract_hosted(&key);
let b_state_response = NeighborHostingMessage::HostingStateResponse {
contracts: vec![*key.id()],
};
let a_result = manager_a.handle_message(&key_b, b_state_response);
assert!(
a_result.response.is_some(),
"CRITICAL: A must announce overlapping contracts when receiving HostingStateResponse"
);
assert_eq!(a_result.overlapping_contracts.len(), 1);
assert_eq!(a_result.overlapping_contracts[0], *key.id());
if let Some(NeighborHostingMessage::HostingAnnounce {
added,
removed,
is_response,
}) = a_result.response
{
assert_eq!(
added.len(),
1,
"Should announce the one overlapping contract"
);
assert_eq!(added[0], *key.id());
assert!(removed.is_empty());
assert!(
is_response,
"Should be marked as response to prevent ping-pong"
);
} else {
panic!("Expected HostingAnnounce, got something else");
}
}
#[test]
fn test_hosting_state_response_no_announcement_when_no_overlap() {
let manager_a = NeighborHostingManager::new();
let key_x = test_contract_key();
let key_y = test_contract_key_2();
let key_b = make_pub_key(2);
manager_a.on_contract_hosted(&key_x);
let b_state_response = NeighborHostingMessage::HostingStateResponse {
contracts: vec![*key_y.id()],
};
let result = manager_a.handle_message(&key_b, b_state_response);
assert!(
result.response.is_none(),
"No announcement needed when no contracts overlap"
);
assert!(
result.overlapping_contracts.is_empty(),
"No overlapping contracts when caches don't intersect"
);
let b_neighbors = manager_a.neighbors_with_contract(&key_y);
assert_eq!(b_neighbors.len(), 1);
assert_eq!(b_neighbors[0], key_b);
}
#[test]
fn test_reconnection_bidirectional_awareness_via_state_request_response() {
let manager_a = NeighborHostingManager::new();
let manager_b = NeighborHostingManager::new();
let key = test_contract_key();
let key_a = make_pub_key(1);
let key_b = make_pub_key(2);
manager_a.on_contract_hosted(&key);
manager_b.on_contract_hosted(&key);
assert!(manager_a.neighbors_with_contract(&key).is_empty());
assert!(manager_b.neighbors_with_contract(&key).is_empty());
let a_request = manager_a.on_ring_connection_established(&key_b);
assert!(matches!(
a_request,
Some(NeighborHostingMessage::HostingStateRequest)
));
let b_result = manager_b.handle_message(&key_a, a_request.unwrap());
assert!(matches!(
b_result.response,
Some(NeighborHostingMessage::HostingStateResponse { .. })
));
let a_result = manager_a.handle_message(&key_b, b_result.response.unwrap());
assert!(
a_result.response.is_some(),
"A must announce overlapping contracts after receiving B's state"
);
let a_neighbors = manager_a.neighbors_with_contract(&key);
assert_eq!(a_neighbors.len(), 1, "A should know B has contract X");
assert_eq!(a_neighbors[0], key_b);
let b_final = manager_b.handle_message(&key_a, a_result.response.unwrap());
assert!(
b_final.response.is_none(),
"B should not respond to A's announcement (is_response=true)"
);
let b_neighbors = manager_b.neighbors_with_contract(&key);
assert_eq!(b_neighbors.len(), 1, "B should know A has contract X");
assert_eq!(b_neighbors[0], key_a);
}
#[test]
fn test_initialize_from_hosting_cache() {
let manager = NeighborHostingManager::new();
let key1 = test_contract_key();
let key2 = test_contract_key_2();
assert_eq!(manager.local_hosted_count(), 0);
manager.initialize_from_hosting_cache(vec![*key1.id(), *key2.id()].into_iter());
assert_eq!(manager.local_hosted_count(), 2);
assert!(manager.is_hosted_locally(&key1));
assert!(manager.is_hosted_locally(&key2));
let neighbor = make_pub_key(1);
let result = manager.handle_message(&neighbor, NeighborHostingMessage::HostingStateRequest);
if let Some(NeighborHostingMessage::HostingStateResponse { contracts }) = result.response {
assert_eq!(contracts.len(), 2);
} else {
panic!("Expected HostingStateResponse");
}
assert!(manager.on_contract_hosted(&key1).is_none());
}
#[test]
fn test_removed_announce_converges_at_receiver() {
let manager = NeighborHostingManager::new();
let key = test_contract_key();
let neighbor = make_pub_key(1);
manager.handle_message(
&neighbor,
NeighborHostingMessage::HostingAnnounce {
added: vec![*key.id()],
removed: vec![],
is_response: false,
},
);
assert_eq!(
manager.neighbors_with_contract(&key),
vec![neighbor.clone()]
);
let result = manager.handle_message(
&neighbor,
NeighborHostingMessage::HostingAnnounce {
added: vec![],
removed: vec![*key.id()],
is_response: false,
},
);
assert!(
manager.neighbors_with_contract(&key).is_empty(),
"a removed-announce must retract the neighbor's advertisement"
);
assert!(result.response.is_none());
assert!(result.overlapping_contracts.is_empty());
}
#[test]
fn test_hosting_state_response_full_replace_drops_stale() {
let manager = NeighborHostingManager::new();
let key_x = test_contract_key();
let key_y = test_contract_key_2();
let neighbor = make_pub_key(1);
manager.handle_message(
&neighbor,
NeighborHostingMessage::HostingAnnounce {
added: vec![*key_x.id(), *key_y.id()],
removed: vec![],
is_response: false,
},
);
assert_eq!(
manager.neighbors_with_contract(&key_x),
vec![neighbor.clone()]
);
assert_eq!(
manager.neighbors_with_contract(&key_y),
vec![neighbor.clone()]
);
manager.handle_message(
&neighbor,
NeighborHostingMessage::HostingStateResponse {
contracts: vec![*key_x.id()],
},
);
assert_eq!(
manager.neighbors_with_contract(&key_x),
vec![neighbor.clone()]
);
assert!(
manager.neighbors_with_contract(&key_y).is_empty(),
"a full-set re-request must drop a contract the peer stopped hosting \
(the dropped-retraction heal)"
);
}
#[test]
fn test_hosting_state_response_overlap_only_for_newly_learned() {
let manager = NeighborHostingManager::new();
let key = test_contract_key();
let neighbor = make_pub_key(1);
manager.on_contract_hosted(&key);
let first = manager.handle_message(
&neighbor,
NeighborHostingMessage::HostingStateResponse {
contracts: vec![*key.id()],
},
);
assert_eq!(
first.overlapping_contracts,
vec![*key.id()],
"first response must report the shared contract as a new overlap"
);
let second = manager.handle_message(
&neighbor,
NeighborHostingMessage::HostingStateResponse {
contracts: vec![*key.id()],
},
);
assert!(
second.overlapping_contracts.is_empty(),
"a periodic re-request of an already-known overlap must NOT re-sync state"
);
assert!(second.response.is_none());
manager.on_peer_disconnected(&neighbor);
let after_reconnect = manager.handle_message(
&neighbor,
NeighborHostingMessage::HostingStateResponse {
contracts: vec![*key.id()],
},
);
assert_eq!(
after_reconnect.overlapping_contracts,
vec![*key.id()],
"after reconnect (view cleared) the overlap must be re-reported"
);
}
#[test]
fn test_on_contract_unhosted_is_idempotent() {
let manager = NeighborHostingManager::new();
let key = test_contract_key();
manager.on_contract_hosted(&key);
assert!(manager.is_hosted_locally(&key), "advertised after hosting");
assert!(
manager.on_contract_unhosted(&key).is_some(),
"first unhost emits a retraction"
);
assert!(
!manager.is_hosted_locally(&key),
"no longer advertised after retraction"
);
assert!(
manager.on_contract_unhosted(&key).is_none(),
"repeat unhost is a no-op (idempotent) — the retry path must not re-broadcast"
);
}
}