network mapping reconnection fixes
This commit is contained in:
parent
d2a13cb134
commit
a370c18825
|
|
@ -0,0 +1,175 @@
|
||||||
|
use contractless::common::network_startup::get_connections;
|
||||||
|
use contractless::env;
|
||||||
|
use contractless::records::memory::network_mapping::structs::{
|
||||||
|
NETWORK_SNAPSHOT_HEADER_BYTES, NETWORK_SNAPSHOT_MAGIC, NETWORK_SNAPSHOT_VERSION,
|
||||||
|
NODE_DELETED_TIMESTAMP_OFFSET, NODE_MONITOR_COUNT_OFFSET, NODE_RECORD_FIXED_BYTES,
|
||||||
|
};
|
||||||
|
use contractless::records::memory::response_channels::generate_uid;
|
||||||
|
use contractless::standalone_tools::connections::handshake;
|
||||||
|
use contractless::wallets::structures::Wallet;
|
||||||
|
|
||||||
|
fn online_node_count(response: &[u8]) -> Result<usize, String> {
|
||||||
|
if response.len() < NETWORK_SNAPSHOT_HEADER_BYTES {
|
||||||
|
return Err("network mapping response was shorter than its header".to_string());
|
||||||
|
}
|
||||||
|
if &response[..NETWORK_SNAPSHOT_MAGIC.len()] != NETWORK_SNAPSHOT_MAGIC {
|
||||||
|
return Err("network mapping response used an unknown format".to_string());
|
||||||
|
}
|
||||||
|
if response[NETWORK_SNAPSHOT_MAGIC.len()] != NETWORK_SNAPSHOT_VERSION {
|
||||||
|
return Err("network mapping response used an unsupported version".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mapping_len = u32::from_le_bytes(
|
||||||
|
response[5..9]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| "network mapping length was invalid")?,
|
||||||
|
) as usize;
|
||||||
|
let monitor_state_len = u32::from_le_bytes(
|
||||||
|
response[9..13]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| "network monitor-state length was invalid")?,
|
||||||
|
) as usize;
|
||||||
|
let mapping_end = NETWORK_SNAPSHOT_HEADER_BYTES
|
||||||
|
.checked_add(mapping_len)
|
||||||
|
.ok_or_else(|| "network mapping length overflowed".to_string())?;
|
||||||
|
let response_end = mapping_end
|
||||||
|
.checked_add(monitor_state_len)
|
||||||
|
.ok_or_else(|| "network monitor-state length overflowed".to_string())?;
|
||||||
|
if response_end != response.len() {
|
||||||
|
return Err("network mapping response length did not match its header".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset = NETWORK_SNAPSHOT_HEADER_BYTES;
|
||||||
|
let mut online = 0usize;
|
||||||
|
while offset < mapping_end {
|
||||||
|
let fixed_end = offset
|
||||||
|
.checked_add(NODE_RECORD_FIXED_BYTES)
|
||||||
|
.ok_or_else(|| "network node record length overflowed".to_string())?;
|
||||||
|
if fixed_end > mapping_end {
|
||||||
|
return Err("network mapping ended inside a node record".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleted_timestamp = u64::from_le_bytes(
|
||||||
|
response[offset + NODE_DELETED_TIMESTAMP_OFFSET
|
||||||
|
..offset + NODE_DELETED_TIMESTAMP_OFFSET + 8]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| "node deletion timestamp was invalid")?,
|
||||||
|
);
|
||||||
|
let monitor_count = u16::from_le_bytes(
|
||||||
|
response[offset + NODE_MONITOR_COUNT_OFFSET
|
||||||
|
..offset + NODE_MONITOR_COUNT_OFFSET + 2]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| "node monitor count was invalid")?,
|
||||||
|
) as usize;
|
||||||
|
let monitor_bytes = monitor_count
|
||||||
|
.checked_mul(Wallet::SHORT_ADDRESS_BYTES_LENGTH)
|
||||||
|
.ok_or_else(|| "node monitor list length overflowed".to_string())?;
|
||||||
|
let record_end = fixed_end
|
||||||
|
.checked_add(monitor_bytes)
|
||||||
|
.ok_or_else(|| "network node record length overflowed".to_string())?;
|
||||||
|
if record_end > mapping_end {
|
||||||
|
return Err("network mapping ended inside a node monitor list".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if deleted_timestamp == 0 && monitor_count > 0 {
|
||||||
|
online += 1;
|
||||||
|
}
|
||||||
|
offset = record_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(online)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() != 3 {
|
||||||
|
eprintln!("Usage: lookup_online_node_count WALLET_PATH WALLET_KEY");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let wallet_path = args[1].clone();
|
||||||
|
let encryption_key = args[2].clone();
|
||||||
|
let rpc_command = 30;
|
||||||
|
|
||||||
|
for connection in get_connections().await {
|
||||||
|
let socket_address = match connection.parse() {
|
||||||
|
Ok(address) => address,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Skipping invalid configured node {connection}: {err}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let response = handshake::connect_and_handshake(
|
||||||
|
socket_address,
|
||||||
|
String::new(),
|
||||||
|
rpc_command,
|
||||||
|
handshake::HandshakeWallet::WalletKey {
|
||||||
|
encryption_key: encryption_key.clone(),
|
||||||
|
wallet_path: wallet_path.clone(),
|
||||||
|
},
|
||||||
|
generate_uid(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Ok(bytes) => match online_node_count(&bytes) {
|
||||||
|
Ok(count) => {
|
||||||
|
println!("{count}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(err) => eprintln!("Invalid mapping from {connection}: {err}"),
|
||||||
|
},
|
||||||
|
Err(err) => eprintln!("Failed to query {connection}: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("failed to retrieve an online node count");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn snapshot_record(deleted_timestamp: u64, monitors: u16) -> Vec<u8> {
|
||||||
|
let mut record = vec![0u8; NODE_RECORD_FIXED_BYTES];
|
||||||
|
record[NODE_DELETED_TIMESTAMP_OFFSET..NODE_DELETED_TIMESTAMP_OFFSET + 8]
|
||||||
|
.copy_from_slice(&deleted_timestamp.to_le_bytes());
|
||||||
|
record[NODE_MONITOR_COUNT_OFFSET..NODE_MONITOR_COUNT_OFFSET + 2]
|
||||||
|
.copy_from_slice(&monitors.to_le_bytes());
|
||||||
|
record.extend(vec![
|
||||||
|
0u8;
|
||||||
|
monitors as usize * Wallet::SHORT_ADDRESS_BYTES_LENGTH
|
||||||
|
]);
|
||||||
|
record
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(records: &[Vec<u8>]) -> Vec<u8> {
|
||||||
|
let mapping = records.concat();
|
||||||
|
let mut response = Vec::new();
|
||||||
|
response.extend_from_slice(NETWORK_SNAPSHOT_MAGIC);
|
||||||
|
response.push(NETWORK_SNAPSHOT_VERSION);
|
||||||
|
response.extend_from_slice(&(mapping.len() as u32).to_le_bytes());
|
||||||
|
response.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
response.extend_from_slice(&mapping);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn counts_only_active_monitored_nodes() {
|
||||||
|
let response = snapshot(&[
|
||||||
|
snapshot_record(0, 2),
|
||||||
|
snapshot_record(0, 0),
|
||||||
|
snapshot_record(1234, 0),
|
||||||
|
]);
|
||||||
|
assert_eq!(online_node_count(&response).unwrap(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_truncated_monitor_lists() {
|
||||||
|
let mut response = snapshot(&[snapshot_record(0, 1)]);
|
||||||
|
response.pop();
|
||||||
|
assert!(online_node_count(&response).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::records::memory::network_mapping::monitor::{MONITOR_ACTION_ADD, MONIT
|
||||||
use crate::records::memory::network_mapping::structs::{MonitorAddressParams, SignedMonitorEdit};
|
use crate::records::memory::network_mapping::structs::{MonitorAddressParams, SignedMonitorEdit};
|
||||||
use crate::records::memory::network_mapping::NodeInfo;
|
use crate::records::memory::network_mapping::NodeInfo;
|
||||||
use crate::records::memory::response_channels::{
|
use crate::records::memory::response_channels::{
|
||||||
delete_entry, reserve_entry_with_context, Command,
|
delete_entry, reserve_entry_with_context, retire_peer_entries, Command,
|
||||||
};
|
};
|
||||||
use crate::records::memory::structs::{Connection, ConnectionHealth, StoreConnectionParams};
|
use crate::records::memory::structs::{Connection, ConnectionHealth, StoreConnectionParams};
|
||||||
use crate::rpc::client::handshake::connect_and_handshake;
|
use crate::rpc::client::handshake::connect_and_handshake;
|
||||||
|
|
@ -14,8 +14,10 @@ use crate::rpc::client::handshake_processing::{bootstrap_peer_discovery, Bootstr
|
||||||
use crate::rpc::client::structs::Connect;
|
use crate::rpc::client::structs::Connect;
|
||||||
use crate::rpc::command_maps::{RPC_BLOCK_HEIGHT, RPC_SETUP_COMPLETE};
|
use crate::rpc::command_maps::{RPC_BLOCK_HEIGHT, RPC_SETUP_COMPLETE};
|
||||||
use crate::rpc::responses::RpcResponse;
|
use crate::rpc::responses::RpcResponse;
|
||||||
|
use crate::rpc::server::connection_memory_manager::remove_stream_from_memory;
|
||||||
use crate::sled::Db;
|
use crate::sled::Db;
|
||||||
use crate::sleep;
|
use crate::sleep;
|
||||||
|
use crate::startup::connections::spawn_outage_recovery;
|
||||||
use crate::thread_rng;
|
use crate::thread_rng;
|
||||||
use crate::timeout;
|
use crate::timeout;
|
||||||
use crate::wallets::structures::Wallet;
|
use crate::wallets::structures::Wallet;
|
||||||
|
|
@ -122,6 +124,20 @@ fn node_runtime_context() -> Option<NodeRuntimeContext> {
|
||||||
.and_then(|context| context.clone())
|
.and_then(|context| context.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn spawn_outage_recovery_from_runtime() {
|
||||||
|
let Some(context) = node_runtime_context() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
spawn_outage_recovery(context.db, context.wallet, context.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn retire_peer_requests_from_runtime(peer: &str) {
|
||||||
|
let Some(context) = node_runtime_context() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
retire_peer_entries(context.map, peer).await;
|
||||||
|
}
|
||||||
|
|
||||||
async fn reconnect_replacement_inner(excluded_ip: &str) {
|
async fn reconnect_replacement_inner(excluded_ip: &str) {
|
||||||
// When an outgoing peer disappears, try to replace it with another
|
// When an outgoing peer disappears, try to replace it with another
|
||||||
// active node that is not already connected and is not the failed IP.
|
// active node that is not already connected and is not the failed IP.
|
||||||
|
|
@ -185,6 +201,13 @@ async fn retry_dropped_outgoing(ip: String, port: u16) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Losing every miner peer is process-level outage recovery, not an
|
||||||
|
// independent retry of each socket that happened to time out.
|
||||||
|
if miner_connection_count().await == 0 {
|
||||||
|
spawn_outage_recovery(context.db, context.wallet, context.map);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let addr_string = format!("{ip}:{port}");
|
let addr_string = format!("{ip}:{port}");
|
||||||
for attempt in 1..=3 {
|
for attempt in 1..=3 {
|
||||||
// A connection-manager drop is definitive, so the first reconnect is
|
// A connection-manager drop is definitive, so the first reconnect is
|
||||||
|
|
@ -361,9 +384,8 @@ impl Connection {
|
||||||
if ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner)
|
if ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner)
|
||||||
&& connection_info.ready
|
&& connection_info.ready
|
||||||
{
|
{
|
||||||
spawn_monitor_update(
|
spawn_monitor_removal(
|
||||||
ip.clone(),
|
ip.clone(),
|
||||||
MONITOR_ACTION_REMOVE,
|
|
||||||
connection_info.wallet_short_address.clone(),
|
connection_info.wallet_short_address.clone(),
|
||||||
port,
|
port,
|
||||||
);
|
);
|
||||||
|
|
@ -507,9 +529,13 @@ impl Connection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
retire_peer_entries(command_map.clone(), &format!("{ip}:{port}")).await;
|
||||||
if connection_type == ConnectionType::Outgoing {
|
if connection_type == ConnectionType::Outgoing {
|
||||||
spawn_retry_dropped_outgoing(ip.clone(), port);
|
spawn_retry_dropped_outgoing(ip.clone(), port);
|
||||||
}
|
}
|
||||||
|
if miner_connection_count().await == 0 {
|
||||||
|
spawn_outage_recovery_from_runtime();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -590,26 +616,20 @@ impl Connection {
|
||||||
|| !info.local_setup_complete
|
|| !info.local_setup_complete
|
||||||
|| !info.remote_setup_complete
|
|| !info.remote_setup_complete
|
||||||
|| !info.local_setup_acknowledged
|
|| !info.local_setup_acknowledged
|
||||||
|
|| info.monitor_activation_pending
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
info.ready = true;
|
info.monitor_activation_pending = true;
|
||||||
info.catch_up_target = None;
|
spawn_monitor_activation(
|
||||||
spawn_monitor_update(
|
|
||||||
ip.clone(),
|
ip.clone(),
|
||||||
MONITOR_ACTION_ADD,
|
|
||||||
info.wallet_short_address.clone(),
|
info.wallet_short_address.clone(),
|
||||||
port,
|
port,
|
||||||
);
|
|
||||||
Connection::client_checkup(
|
|
||||||
Arc::clone(&info.stream),
|
Arc::clone(&info.stream),
|
||||||
ConnectionType::from_bytes(&info.connection_type).unwrap_or(ConnectionType::Incoming),
|
|
||||||
ip,
|
|
||||||
port,
|
|
||||||
command_map,
|
command_map,
|
||||||
);
|
);
|
||||||
true
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_local_setup_complete(
|
pub fn mark_local_setup_complete(
|
||||||
|
|
@ -741,9 +761,7 @@ impl Connection {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mapping_relay_peer_streams_with_keys(
|
pub fn get_mapping_relay_peer_streams_with_keys(&self) -> Vec<(String, Arc<Mutex<TcpStream>>)> {
|
||||||
&self,
|
|
||||||
) -> Vec<(String, Arc<Mutex<TcpStream>>)> {
|
|
||||||
self.connection_map
|
self.connection_map
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(key, connection_info)| {
|
.filter_map(|(key, connection_info)| {
|
||||||
|
|
@ -948,10 +966,18 @@ impl Connection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_monitor_update(ip: String, action: u8, monitored_address: String, port: u16) {
|
fn spawn_monitor_activation(
|
||||||
tokio::spawn(async move {
|
ip: String,
|
||||||
|
monitored_address: String,
|
||||||
|
port: u16,
|
||||||
|
stream: Arc<Mutex<TcpStream>>,
|
||||||
|
command_map: Arc<Mutex<Command>>,
|
||||||
|
) {
|
||||||
let Some(context) = node_runtime_context() else {
|
let Some(context) = node_runtime_context() else {
|
||||||
warn!("[network_map] node runtime context is not initialized");
|
warn!("[network_map] node runtime context is not initialized");
|
||||||
|
tokio::spawn(async move {
|
||||||
|
remove_stream_from_memory(&stream).await;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !Wallet::short_address_validation(&monitored_address) {
|
if !Wallet::short_address_validation(&monitored_address) {
|
||||||
|
|
@ -961,12 +987,34 @@ fn spawn_monitor_update(ip: String, action: u8, monitored_address: String, port:
|
||||||
if monitored_address == monitoring_address {
|
if monitored_address == monitoring_address {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// A reconnecting membership remains pending until an active sponsor's
|
||||||
|
// monitor-add arrives. Do not let that pending node author its own
|
||||||
|
// monitor event first; monitor validation correctly rejects it.
|
||||||
|
let local_activation = timeout(Duration::from_secs(30), async {
|
||||||
|
while !NodeInfo::is_active_address(&monitoring_address).await {
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
if local_activation.is_err() {
|
||||||
|
warn!(
|
||||||
|
"[network_map] connection setup timed out waiting for local membership activation: address={} peer={}:{}",
|
||||||
|
monitoring_address, ip, port
|
||||||
|
);
|
||||||
|
remove_stream_from_memory(&stream).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate ordering fields only after activation. A timestamp created
|
||||||
|
// before the wait could become stale relative to the activating event.
|
||||||
let timestamp = crate::Utc::now().timestamp_millis() as u64;
|
let timestamp = crate::Utc::now().timestamp_millis() as u64;
|
||||||
let modified_block =
|
let modified_block =
|
||||||
crate::records::block_height::get_block_height::get_height(&context.db)
|
crate::records::block_height::get_block_height::get_height(&context.db)
|
||||||
.saturating_add(1);
|
.saturating_add(1);
|
||||||
let signature = NodeInfo::monitor_signature(
|
let signature = NodeInfo::monitor_signature(
|
||||||
action,
|
MONITOR_ACTION_ADD,
|
||||||
&monitored_address,
|
&monitored_address,
|
||||||
&monitoring_address,
|
&monitoring_address,
|
||||||
&ip,
|
&ip,
|
||||||
|
|
@ -976,7 +1024,7 @@ fn spawn_monitor_update(ip: String, action: u8, monitored_address: String, port:
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let edit = SignedMonitorEdit {
|
let edit = SignedMonitorEdit {
|
||||||
action,
|
action: MONITOR_ACTION_ADD,
|
||||||
monitored_address: monitored_address.clone(),
|
monitored_address: monitored_address.clone(),
|
||||||
monitoring_address,
|
monitoring_address,
|
||||||
target_ip: ip.clone(),
|
target_ip: ip.clone(),
|
||||||
|
|
@ -992,24 +1040,89 @@ fn spawn_monitor_update(ip: String, action: u8, monitored_address: String, port:
|
||||||
wallet: context.wallet.clone(),
|
wallet: context.wallet.clone(),
|
||||||
connections_key: format!("{ip}:{port}"),
|
connections_key: format!("{ip}:{port}"),
|
||||||
};
|
};
|
||||||
if action == MONITOR_ACTION_ADD {
|
|
||||||
for attempt in 1..=40 {
|
|
||||||
let RpcResponse::Binary(response) = NodeInfo::add_monitor(params.clone()).await;
|
let RpcResponse::Binary(response) = NodeInfo::add_monitor(params.clone()).await;
|
||||||
if response.as_slice() == b"Success" {
|
if response.as_slice() != b"Success" {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if attempt == 40 {
|
|
||||||
warn!(
|
warn!(
|
||||||
"[network_map] monitor-add failed after {attempt} attempts: monitored={} monitoring={} error={}",
|
"[network_map] connection setup monitor-add failed: monitored={} monitoring={} error={}",
|
||||||
monitored_address,
|
monitored_address,
|
||||||
params.edit.monitoring_address,
|
params.edit.monitoring_address,
|
||||||
String::from_utf8_lossy(&response)
|
String::from_utf8_lossy(&response)
|
||||||
);
|
);
|
||||||
|
remove_stream_from_memory(&stream).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sleep(Duration::from_millis(250)).await;
|
|
||||||
|
let connection_type = {
|
||||||
|
let ip_bytes = ip_to_binary(&ip);
|
||||||
|
let mut connections = CONNECTIONS.write().await;
|
||||||
|
let Some(connection) = connections.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some((key, info)) = connection.connection_map.iter_mut().find(|(key, info)| {
|
||||||
|
key.ip == ip_bytes
|
||||||
|
&& key.port == port
|
||||||
|
&& Arc::ptr_eq(&info.stream, &stream)
|
||||||
|
&& info.monitor_activation_pending
|
||||||
|
&& !info.ready
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
info.monitor_activation_pending = false;
|
||||||
|
info.ready = true;
|
||||||
|
info.catch_up_target = None;
|
||||||
|
ConnectionType::from_bytes(&key.connection_type).unwrap_or(ConnectionType::Incoming)
|
||||||
|
};
|
||||||
|
|
||||||
|
Connection::client_checkup(stream, connection_type, ip, port, command_map);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
fn spawn_monitor_removal(ip: String, monitored_address: String, port: u16) {
|
||||||
|
let Some(context) = node_runtime_context() else {
|
||||||
|
warn!("[network_map] node runtime context is not initialized");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !Wallet::short_address_validation(&monitored_address) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let monitoring_address = context.wallet.saved.short_address.clone();
|
||||||
|
if monitored_address == monitoring_address {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timestamp = crate::Utc::now().timestamp_millis() as u64;
|
||||||
|
let modified_block =
|
||||||
|
crate::records::block_height::get_block_height::get_height(&context.db).saturating_add(1);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let signature = NodeInfo::monitor_signature(
|
||||||
|
MONITOR_ACTION_REMOVE,
|
||||||
|
&monitored_address,
|
||||||
|
&monitoring_address,
|
||||||
|
&ip,
|
||||||
|
timestamp,
|
||||||
|
modified_block,
|
||||||
|
&context.wallet,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let edit = SignedMonitorEdit {
|
||||||
|
action: MONITOR_ACTION_REMOVE,
|
||||||
|
monitored_address: monitored_address.clone(),
|
||||||
|
monitoring_address,
|
||||||
|
target_ip: ip,
|
||||||
|
modified_timestamp: timestamp,
|
||||||
|
modified_block,
|
||||||
|
modified_signature: signature,
|
||||||
|
};
|
||||||
|
let params = MonitorAddressParams {
|
||||||
|
map: context.map.clone(),
|
||||||
|
edit,
|
||||||
|
remote_ip: String::new(),
|
||||||
|
db: context.db.clone(),
|
||||||
|
wallet: context.wallet.clone(),
|
||||||
|
connections_key: format!("disconnect:{port}"),
|
||||||
|
};
|
||||||
let RpcResponse::Binary(response) = NodeInfo::remove_monitor(params.clone()).await;
|
let RpcResponse::Binary(response) = NodeInfo::remove_monitor(params.clone()).await;
|
||||||
if response.as_slice() != b"Success" {
|
if response.as_slice() != b"Success" {
|
||||||
warn!(
|
warn!(
|
||||||
|
|
@ -1019,7 +1132,6 @@ fn spawn_monitor_update(ip: String, action: u8, monitored_address: String, port:
|
||||||
String::from_utf8_lossy(&response)
|
String::from_utf8_lossy(&response)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,24 @@ impl NodeInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn revive_deleted_membership(node: &mut NodeInfo, edit: &SignedNodeEdit) -> bool {
|
||||||
|
let accepted_ip_change = node.ip != edit.ip;
|
||||||
|
node.ip = edit.ip.clone();
|
||||||
|
node.port = edit.port;
|
||||||
|
node.added_by = edit.modified_by.clone();
|
||||||
|
node.added_timestamp = edit.modified_timestamp;
|
||||||
|
node.added_signature = edit.modified_signature.clone();
|
||||||
|
|
||||||
|
// Keep the new lifecycle inactive until a newer signed monitor-add
|
||||||
|
// proves that connection setup reached the operational state.
|
||||||
|
node.deleted_timestamp = edit.modified_timestamp;
|
||||||
|
// Zero is the deterministic pending marker: no historical block has
|
||||||
|
// yet activated this lifecycle, and every peer derives the same map.
|
||||||
|
node.deleted_block = 0;
|
||||||
|
node.monitoring.clear();
|
||||||
|
accepted_ip_change
|
||||||
|
}
|
||||||
|
|
||||||
fn mark_conflict_loser(
|
fn mark_conflict_loser(
|
||||||
address_map: &mut HashMap<String, NodeInfo>,
|
address_map: &mut HashMap<String, NodeInfo>,
|
||||||
loser_address: &str,
|
loser_address: &str,
|
||||||
|
|
@ -497,12 +515,7 @@ impl NodeInfo {
|
||||||
existing_node.added_timestamp <= existing_node.deleted_timestamp;
|
existing_node.added_timestamp <= existing_node.deleted_timestamp;
|
||||||
let mut accepted_ip_change = false;
|
let mut accepted_ip_change = false;
|
||||||
if current_is_old_cycle || Self::membership_is_preferred(&edit, existing_node) {
|
if current_is_old_cycle || Self::membership_is_preferred(&edit, existing_node) {
|
||||||
accepted_ip_change = existing_node.ip != edit.ip;
|
accepted_ip_change = Self::revive_deleted_membership(existing_node, &edit);
|
||||||
existing_node.ip = edit.ip.clone();
|
|
||||||
existing_node.port = edit.port;
|
|
||||||
existing_node.added_by = edit.modified_by;
|
|
||||||
existing_node.added_timestamp = edit.modified_timestamp;
|
|
||||||
existing_node.added_signature = edit.modified_signature;
|
|
||||||
}
|
}
|
||||||
if accepted_ip_change {
|
if accepted_ip_change {
|
||||||
let mut monitor_state = MONITOR_EVENT_STATE.lock().await;
|
let mut monitor_state = MONITOR_EVENT_STATE.lock().await;
|
||||||
|
|
@ -989,12 +1002,8 @@ impl NodeInfo {
|
||||||
if current_is_old_cycle
|
if current_is_old_cycle
|
||||||
|| Self::membership_is_preferred(&edit, existing_node)
|
|| Self::membership_is_preferred(&edit, existing_node)
|
||||||
{
|
{
|
||||||
let accepted_ip_change = existing_node.ip != edit.ip;
|
let accepted_ip_change =
|
||||||
existing_node.ip = edit.ip.clone();
|
Self::revive_deleted_membership(existing_node, &edit);
|
||||||
existing_node.port = edit.port;
|
|
||||||
existing_node.added_by = edit.modified_by.clone();
|
|
||||||
existing_node.added_timestamp = edit.modified_timestamp;
|
|
||||||
existing_node.added_signature = edit.modified_signature.clone();
|
|
||||||
if accepted_ip_change {
|
if accepted_ip_change {
|
||||||
let mut monitor_state = MONITOR_EVENT_STATE.lock().await;
|
let mut monitor_state = MONITOR_EVENT_STATE.lock().await;
|
||||||
Self::discard_old_target_ip_events_from(
|
Self::discard_old_target_ip_events_from(
|
||||||
|
|
@ -1146,6 +1155,34 @@ mod tests {
|
||||||
assert!(NodeInfo::validate_snapshot_structure(&map).is_err());
|
assert!(NodeInfo::validate_snapshot_structure(&map).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconnect_revives_membership_before_snapshot_is_exposed() {
|
||||||
|
let mut node = snapshot_node("1.2.3.5", 10, &["first.cltc"]);
|
||||||
|
node.blocks_mined = 95;
|
||||||
|
node.deleted_timestamp = 20;
|
||||||
|
node.deleted_block = 100;
|
||||||
|
|
||||||
|
let edit = SignedNodeEdit {
|
||||||
|
address: "node.cltc".to_string(),
|
||||||
|
ip: "1.2.3.5".to_string(),
|
||||||
|
port: 50051,
|
||||||
|
modified_by: "new-sponsor.cltc".to_string(),
|
||||||
|
modified_timestamp: 30,
|
||||||
|
modified_signature: "11".repeat(Wallet::SIGNATURE_LENGTH),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!NodeInfo::revive_deleted_membership(&mut node, &edit));
|
||||||
|
assert_eq!(node.added_timestamp, 30);
|
||||||
|
assert_eq!(node.deleted_timestamp, 30);
|
||||||
|
assert_eq!(node.deleted_block, 0);
|
||||||
|
assert!(node.monitoring.is_empty());
|
||||||
|
assert_eq!(node.blocks_mined, 95);
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("node.cltc".to_string(), node);
|
||||||
|
assert!(NodeInfo::validate_snapshot_structure(&map).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn complete_snapshot_rejects_deleted_or_duplicate_active_monitors() {
|
fn complete_snapshot_rejects_deleted_or_duplicate_active_monitors() {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,30 @@ mod tests {
|
||||||
assert!(NodeInfo::monitor_event_order(&remove) > NodeInfo::monitor_event_order(&add));
|
assert!(NodeInfo::monitor_event_order(&remove) > NodeInfo::monitor_event_order(&add));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_a_newer_monitor_add_activates_pending_membership() {
|
||||||
|
let mut pending = node(&[]);
|
||||||
|
pending.added_timestamp = 300;
|
||||||
|
pending.deleted_timestamp = 300;
|
||||||
|
|
||||||
|
let mut edit = SignedMonitorEdit {
|
||||||
|
action: MONITOR_ACTION_ADD,
|
||||||
|
monitored_address: "target".to_string(),
|
||||||
|
monitoring_address: "monitor".to_string(),
|
||||||
|
target_ip: "1.2.3.4".to_string(),
|
||||||
|
modified_timestamp: 300,
|
||||||
|
modified_block: 50,
|
||||||
|
modified_signature: "signature".to_string(),
|
||||||
|
};
|
||||||
|
assert!(!NodeInfo::can_activate_pending_membership(&edit, &pending));
|
||||||
|
|
||||||
|
edit.modified_timestamp = 301;
|
||||||
|
assert!(NodeInfo::can_activate_pending_membership(&edit, &pending));
|
||||||
|
|
||||||
|
edit.action = MONITOR_ACTION_REMOVE;
|
||||||
|
assert!(!NodeInfo::can_activate_pending_membership(&edit, &pending));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn signed_monitor_wire_record_round_trips() {
|
fn signed_monitor_wire_record_round_trips() {
|
||||||
let monitored = "ab13318c26250b048db92920a80a86127c933b0c.cltc".to_string();
|
let monitored = "ab13318c26250b048db92920a80a86127c933b0c.cltc".to_string();
|
||||||
|
|
@ -259,14 +283,17 @@ impl NodeInfo {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn can_activate_pending_membership(edit: &SignedMonitorEdit, node: &NodeInfo) -> bool {
|
||||||
|
edit.action == MONITOR_ACTION_ADD && edit.modified_timestamp > node.deleted_timestamp
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn discard_old_target_ip_events_from(
|
pub(super) fn discard_old_target_ip_events_from(
|
||||||
state: &mut HashMap<String, SignedMonitorEdit>,
|
state: &mut HashMap<String, SignedMonitorEdit>,
|
||||||
address: &str,
|
address: &str,
|
||||||
current_ip: &str,
|
current_ip: &str,
|
||||||
) {
|
) {
|
||||||
state.retain(|_, event| {
|
state
|
||||||
event.monitored_address != address || event.target_ip == current_ip
|
.retain(|_, event| event.monitored_address != address || event.target_ip == current_ip);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn broadcast_monitor_event(
|
async fn broadcast_monitor_event(
|
||||||
|
|
@ -356,6 +383,7 @@ impl NodeInfo {
|
||||||
}
|
}
|
||||||
if monitored.deleted_timestamp > 0
|
if monitored.deleted_timestamp > 0
|
||||||
&& monitored.added_timestamp <= monitored.deleted_timestamp
|
&& monitored.added_timestamp <= monitored.deleted_timestamp
|
||||||
|
&& !Self::can_activate_pending_membership(edit, monitored)
|
||||||
{
|
{
|
||||||
return Err(
|
return Err(
|
||||||
"monitor target must have a newer membership record before reconnection"
|
"monitor target must have a newer membership record before reconnection"
|
||||||
|
|
@ -449,7 +477,10 @@ impl NodeInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
let local_short = wallet.saved.short_address.clone();
|
let local_short = wallet.saved.short_address.clone();
|
||||||
if remote_ip.is_empty() && edit.monitoring_address == local_short {
|
if remote_ip.is_empty()
|
||||||
|
&& edit.monitoring_address == local_short
|
||||||
|
&& edit.modified_signature.is_empty()
|
||||||
|
{
|
||||||
edit.modified_timestamp = Utc::now().timestamp_millis() as u64;
|
edit.modified_timestamp = Utc::now().timestamp_millis() as u64;
|
||||||
edit.modified_block = get_height(&db).saturating_add(1);
|
edit.modified_block = get_height(&db).saturating_add(1);
|
||||||
edit.modified_signature = Self::monitor_signature(
|
edit.modified_signature = Self::monitor_signature(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ use crate::sled::Db;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
impl NodeInfo {
|
impl NodeInfo {
|
||||||
|
fn eligible_at_block(node_info: &NodeInfo, block_number: u32) -> bool {
|
||||||
|
node_info.deleted_timestamp == 0
|
||||||
|
|| (node_info.deleted_block > 0 && block_number < node_info.deleted_block)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn has_reciprocal_sponsorship(
|
pub async fn has_reciprocal_sponsorship(
|
||||||
local_address: &str,
|
local_address: &str,
|
||||||
operational_peer_wallets: &[String],
|
operational_peer_wallets: &[String],
|
||||||
|
|
@ -33,8 +38,16 @@ impl NodeInfo {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn signed_mapping_state(
|
pub(crate) async fn is_active_address(address: &str) -> bool {
|
||||||
) -> (Vec<SyncedNodeState>, Vec<SignedMonitorEdit>) {
|
ADDRESS_MAP
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get(address)
|
||||||
|
.map(|node| node.deleted_timestamp == 0)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn signed_mapping_state() -> (Vec<SyncedNodeState>, Vec<SignedMonitorEdit>) {
|
||||||
let map = ADDRESS_MAP.lock().await;
|
let map = ADDRESS_MAP.lock().await;
|
||||||
let monitor_events = MONITOR_EVENT_STATE.lock().await;
|
let monitor_events = MONITOR_EVENT_STATE.lock().await;
|
||||||
|
|
||||||
|
|
@ -282,8 +295,9 @@ impl NodeInfo {
|
||||||
let map = ADDRESS_MAP.lock().await;
|
let map = ADDRESS_MAP.lock().await;
|
||||||
if let Some(node_info) = map.get(address) {
|
if let Some(node_info) = map.get(address) {
|
||||||
// Deleted nodes remain valid for blocks before their recorded
|
// Deleted nodes remain valid for blocks before their recorded
|
||||||
// deletion height, which keeps historical validation deterministic.
|
// deletion height. A deleted record with block zero is a pending
|
||||||
return node_info.deleted_block == 0 || block_number < node_info.deleted_block;
|
// or rejected lifecycle and is never eligible to mine.
|
||||||
|
return Self::eligible_at_block(node_info, block_number);
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -448,6 +462,26 @@ mod tests {
|
||||||
NODE_RECORD_FIXED_BYTES,
|
NODE_RECORD_FIXED_BYTES,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pending_membership_is_not_mining_eligible() {
|
||||||
|
let mut pending = NodeInfo::new(
|
||||||
|
"198.51.100.10".to_string(),
|
||||||
|
50050,
|
||||||
|
0,
|
||||||
|
"sponsor.cltc".to_string(),
|
||||||
|
300,
|
||||||
|
"signature".to_string(),
|
||||||
|
);
|
||||||
|
pending.deleted_timestamp = 300;
|
||||||
|
pending.deleted_block = 0;
|
||||||
|
|
||||||
|
assert!(!NodeInfo::eligible_at_block(&pending, 0));
|
||||||
|
assert!(!NodeInfo::eligible_at_block(&pending, 10_000));
|
||||||
|
|
||||||
|
pending.deleted_timestamp = 0;
|
||||||
|
assert!(NodeInfo::eligible_at_block(&pending, 10_000));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn bootstrap_snapshot_keeps_deletion_and_monitor_state() {
|
async fn bootstrap_snapshot_keeps_deletion_and_monitor_state() {
|
||||||
let address = "1111111111111111111111111111111111111111.cltc";
|
let address = "1111111111111111111111111111111111111111.cltc";
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,24 @@ pub async fn delete_entry(map: Arc<Mutex<Command>>, key: Byte3) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn retire_peer_entries(map: Arc<Mutex<Command>>, peer: &str) {
|
||||||
|
let mut map = map.lock().await;
|
||||||
|
let expires_at = Instant::now() + Duration::from_secs(30);
|
||||||
|
|
||||||
|
for channel_pair in map.values_mut() {
|
||||||
|
if channel_pair.peer.as_deref() != Some(peer) || channel_pair.expires_at.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the original waiter immediately while retaining the UID long
|
||||||
|
// enough to reject a late reply from the disconnected socket.
|
||||||
|
let (retired_tx, retired_rx) = mpsc::channel(1);
|
||||||
|
drop(retired_rx);
|
||||||
|
channel_pair.tx = retired_tx;
|
||||||
|
channel_pair.expires_at = Some(expires_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -212,4 +230,29 @@ mod tests {
|
||||||
|
|
||||||
assert!(is_retired_entry(map, uid).await);
|
assert!(is_retired_entry(map, uid).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn retiring_peer_entries_wakes_only_that_peers_waiters() {
|
||||||
|
let map = Arc::new(Mutex::new(Command::new()));
|
||||||
|
let (dead_uid, dead_tx, dead_rx) = reserve_entry_with_context(
|
||||||
|
map.clone(),
|
||||||
|
Some(2),
|
||||||
|
Some("203.0.113.10:50050".to_string()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let (live_uid, _live_tx, live_rx) = reserve_entry_with_context(
|
||||||
|
map.clone(),
|
||||||
|
Some(2),
|
||||||
|
Some("203.0.113.11:50050".to_string()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
drop(dead_tx);
|
||||||
|
|
||||||
|
retire_peer_entries(map.clone(), "203.0.113.10:50050").await;
|
||||||
|
|
||||||
|
assert!(dead_rx.lock().await.recv().await.is_none());
|
||||||
|
assert!(is_retired_entry(map.clone(), dead_uid).await);
|
||||||
|
assert!(!is_retired_entry(map.clone(), live_uid).await);
|
||||||
|
assert!(live_rx.lock().await.try_recv().is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ pub struct ConnectionInfo {
|
||||||
pub local_setup_complete: bool,
|
pub local_setup_complete: bool,
|
||||||
pub remote_setup_complete: bool,
|
pub remote_setup_complete: bool,
|
||||||
pub local_setup_acknowledged: bool,
|
pub local_setup_acknowledged: bool,
|
||||||
|
pub monitor_activation_pending: bool,
|
||||||
// Incoming peers that connect behind the tip retain the height they
|
// Incoming peers that connect behind the tip retain the height they
|
||||||
// originally need to reach. This avoids chasing a moving local tip.
|
// originally need to reach. This avoids chasing a moving local tip.
|
||||||
pub catch_up_target: Option<u32>,
|
pub catch_up_target: Option<u32>,
|
||||||
|
|
@ -231,6 +232,7 @@ impl ConnectionInfo {
|
||||||
local_setup_complete: false,
|
local_setup_complete: false,
|
||||||
remote_setup_complete: false,
|
remote_setup_complete: false,
|
||||||
local_setup_acknowledged: false,
|
local_setup_acknowledged: false,
|
||||||
|
monitor_activation_pending: false,
|
||||||
catch_up_target: None,
|
catch_up_target: None,
|
||||||
ready: false,
|
ready: false,
|
||||||
health: ConnectionHealth::Connected,
|
health: ConnectionHealth::Connected,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use crate::common::binary_conversions::{binary_to_ip, ip_to_binary};
|
use crate::common::binary_conversions::{binary_to_ip, ip_to_binary};
|
||||||
use crate::log::warn;
|
use crate::log::warn;
|
||||||
use crate::records::memory::connections::{
|
use crate::records::memory::connections::{
|
||||||
set_stream_health, spawn_retry_dropped_outgoing, CONNECTIONS,
|
miner_connection_count, retire_peer_requests_from_runtime, set_stream_health,
|
||||||
|
spawn_outage_recovery_from_runtime, spawn_retry_dropped_outgoing, CONNECTIONS,
|
||||||
};
|
};
|
||||||
use crate::records::memory::enums::{ClientType, ConnectionType};
|
use crate::records::memory::enums::{ClientType, ConnectionType};
|
||||||
use crate::records::memory::response_channels::{
|
use crate::records::memory::response_channels::{
|
||||||
delete_entry, reserve_entry_with_context, Command,
|
delete_entry, reserve_entry_with_context, retire_peer_entries, Command,
|
||||||
};
|
};
|
||||||
use crate::records::memory::structs::{ConnectionHealth, StoreConnectionParams};
|
use crate::records::memory::structs::{ConnectionHealth, StoreConnectionParams};
|
||||||
use crate::rpc::command_maps::RPC_BLOCK_HEIGHT;
|
use crate::rpc::command_maps::RPC_BLOCK_HEIGHT;
|
||||||
|
|
@ -127,8 +128,12 @@ async fn purge_stale_duplicate_miner(ip: &str, command_map: Arc<Mutex<Command>>)
|
||||||
let duplicate_key = format!("{duplicate_ip}:{duplicate_port}");
|
let duplicate_key = format!("{duplicate_ip}:{duplicate_port}");
|
||||||
match recorded_health {
|
match recorded_health {
|
||||||
ConnectionHealth::Connected | ConnectionHealth::Busy => {
|
ConnectionHealth::Connected | ConnectionHealth::Busy => {
|
||||||
let health =
|
let health = miner_stream_health(
|
||||||
miner_stream_health(&duplicate_key, duplicate_stream.clone(), command_map).await;
|
&duplicate_key,
|
||||||
|
duplicate_stream.clone(),
|
||||||
|
command_map.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
if matches!(health, ConnectionHealth::Connected | ConnectionHealth::Busy) {
|
if matches!(health, ConnectionHealth::Connected | ConnectionHealth::Busy) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -161,6 +166,8 @@ async fn purge_stale_duplicate_miner(ip: &str, command_map: Arc<Mutex<Command>>)
|
||||||
"[connection_manager] removing stale duplicate miner stream before accepting reconnect: peer={duplicate_key}"
|
"[connection_manager] removing stale duplicate miner stream before accepting reconnect: peer={duplicate_key}"
|
||||||
);
|
);
|
||||||
connection.drop_connection(connection_type, duplicate_ip, duplicate_port);
|
connection.drop_connection(connection_type, duplicate_ip, duplicate_port);
|
||||||
|
drop(guard);
|
||||||
|
retire_peer_entries(command_map, &duplicate_key).await;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,13 +336,18 @@ pub async fn remove_stream_from_memory(stream: &Arc<Mutex<TcpStream>>) {
|
||||||
if let Some((connection_type, ip_bytes, port)) = matching_connection {
|
if let Some((connection_type, ip_bytes, port)) = matching_connection {
|
||||||
let ip = crate::common::binary_conversions::binary_to_ip(ip_bytes);
|
let ip = crate::common::binary_conversions::binary_to_ip(ip_bytes);
|
||||||
let dropped = connection.drop_connection(connection_type, ip.clone(), port);
|
let dropped = connection.drop_connection(connection_type, ip.clone(), port);
|
||||||
|
drop(connection_instance);
|
||||||
|
retire_peer_requests_from_runtime(&format!("{ip}:{port}")).await;
|
||||||
if connection_type == ConnectionType::Outgoing
|
if connection_type == ConnectionType::Outgoing
|
||||||
&& dropped
|
&& dropped
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|connection_info| ClientType::from_bytes(&connection_info.client_type))
|
.and_then(|connection_info| ClientType::from_bytes(&connection_info.client_type))
|
||||||
== Some(ClientType::Miner)
|
== Some(ClientType::Miner)
|
||||||
{
|
{
|
||||||
spawn_retry_dropped_outgoing(ip, port);
|
spawn_retry_dropped_outgoing(ip.clone(), port);
|
||||||
|
}
|
||||||
|
if miner_connection_count().await == 0 {
|
||||||
|
spawn_outage_recovery_from_runtime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::common::network_startup::{get_ip_and_port, get_node_connections};
|
||||||
use crate::log::{error, info, warn};
|
use crate::log::{error, info, warn};
|
||||||
use crate::miner::flag::{is_mining_stop_requested, is_normal_mode};
|
use crate::miner::flag::{is_mining_stop_requested, is_normal_mode};
|
||||||
use crate::records::memory::connections::{
|
use crate::records::memory::connections::{
|
||||||
operational_peer_wallets, peer_connection_count, ready_outgoing_connection_count,
|
miner_connection_count, operational_peer_wallets, ready_outgoing_connection_count,
|
||||||
refill_outgoing_connections_once,
|
refill_outgoing_connections_once,
|
||||||
};
|
};
|
||||||
use crate::records::memory::network_mapping::NodeInfo;
|
use crate::records::memory::network_mapping::NodeInfo;
|
||||||
|
|
@ -15,6 +15,24 @@ use crate::wallets::structures::Wallet;
|
||||||
use crate::Arc;
|
use crate::Arc;
|
||||||
use crate::Duration;
|
use crate::Duration;
|
||||||
use crate::Mutex;
|
use crate::Mutex;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
static OUTAGE_RECOVERY_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
struct OutageRecoveryGuard;
|
||||||
|
|
||||||
|
impl Drop for OutageRecoveryGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
OUTAGE_RECOVERY_ACTIVE.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_start_outage_recovery() -> Option<OutageRecoveryGuard> {
|
||||||
|
OUTAGE_RECOVERY_ACTIVE
|
||||||
|
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||||
|
.ok()
|
||||||
|
.map(|_| OutageRecoveryGuard)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_connections(
|
pub async fn handle_connections(
|
||||||
db: Db,
|
db: Db,
|
||||||
|
|
@ -148,6 +166,29 @@ async fn attempt_bootstrap_connections(
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn spawn_outage_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<Mutex<Command>>) {
|
||||||
|
let Some(recovery_guard) = try_start_outage_recovery() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _recovery_guard = recovery_guard;
|
||||||
|
|
||||||
|
// A miner socket still completing setup owns recovery even though it
|
||||||
|
// is not ready for normal traffic yet.
|
||||||
|
if miner_connection_count().await != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("[reconnect] all miner peers are offline; starting coordinated outage recovery");
|
||||||
|
match attempt_bootstrap_connections(db, wallet, map, "reconnect").await {
|
||||||
|
Ok(true) => {}
|
||||||
|
Ok(false) => warn!("[reconnect] coordinated outage recovery found no available peer"),
|
||||||
|
Err(err) => warn!("[reconnect] coordinated outage recovery aborted: {err}"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn spawn_isolated_bootstrap_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<Mutex<Command>>) {
|
pub fn spawn_isolated_bootstrap_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<Mutex<Command>>) {
|
||||||
let outgoing_connections = crate::Settings::load()
|
let outgoing_connections = crate::Settings::load()
|
||||||
.map(|settings| settings.outgoing_connections)
|
.map(|settings| settings.outgoing_connections)
|
||||||
|
|
@ -164,20 +205,8 @@ pub fn spawn_isolated_bootstrap_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<M
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if peer_connection_count().await == 0 {
|
if miner_connection_count().await == 0 {
|
||||||
info!("[reconnect] no operational peers remain; retrying bootstrap recovery");
|
spawn_outage_recovery(db.clone(), wallet.clone(), map.clone());
|
||||||
match attempt_bootstrap_connections(
|
|
||||||
db.clone(),
|
|
||||||
wallet.clone(),
|
|
||||||
map.clone(),
|
|
||||||
"reconnect",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(true) => {}
|
|
||||||
Ok(false) => {}
|
|
||||||
Err(err) => warn!("[reconnect] bootstrap recovery aborted: {err}"),
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,3 +219,17 @@ pub fn spawn_isolated_bootstrap_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<M
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn outage_recovery_has_one_owner() {
|
||||||
|
OUTAGE_RECOVERY_ACTIVE.store(false, Ordering::SeqCst);
|
||||||
|
let owner = try_start_outage_recovery().expect("first recovery should acquire ownership");
|
||||||
|
assert!(try_start_outage_recovery().is_none());
|
||||||
|
drop(owner);
|
||||||
|
assert!(try_start_outage_recovery().is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue