recovery fixes
This commit is contained in:
parent
7ca01b9e69
commit
291fbd5755
|
|
@ -159,6 +159,33 @@ pub async fn begin_chain_sync() -> ChainOperationGuard {
|
||||||
ChainOperationGuard { active: true }
|
ChainOperationGuard { active: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Incoming setup belongs to one socket, not to an endpoint that may have
|
||||||
|
// reconnected. Check its identity while holding the connection read lock so
|
||||||
|
// removal cannot race with admission to the chain gate and the stop request.
|
||||||
|
pub async fn begin_connected_peer_sync(
|
||||||
|
stream: &crate::Arc<crate::Mutex<crate::TcpStream>>,
|
||||||
|
) -> Option<ChainOperationGuard> {
|
||||||
|
loop {
|
||||||
|
let connections = crate::records::memory::connections::CONNECTIONS.read().await;
|
||||||
|
let current = connections.as_ref().is_some_and(|connection| {
|
||||||
|
connection.connection_map.values().any(|info| crate::Arc::ptr_eq(&info.stream, stream))
|
||||||
|
});
|
||||||
|
if !current {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if try_acquire_chain_operation_gate() {
|
||||||
|
let guard = ChainOperationGuard { active: true };
|
||||||
|
request_mining_stop();
|
||||||
|
drop(connections);
|
||||||
|
wait_for_mining_idle().await;
|
||||||
|
set_node_mode(NodeMode::Syncing);
|
||||||
|
return Some(guard);
|
||||||
|
}
|
||||||
|
drop(connections);
|
||||||
|
sleep(Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn begin_reorg_lock() -> ChainOperationGuard {
|
pub async fn begin_reorg_lock() -> ChainOperationGuard {
|
||||||
// Orphan correction shares the canonical chain gate with synchronization,
|
// Orphan correction shares the canonical chain gate with synchronization,
|
||||||
// so rollback/replay cannot overlap a startup or live catch-up save.
|
// so rollback/replay cannot overlap a startup or live catch-up save.
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@ 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_created_before, 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;
|
||||||
use crate::rpc::client::handshake_processing::{bootstrap_peer_discovery, BootstrapParams};
|
use crate::rpc::client::handshake_processing::{
|
||||||
|
bootstrap_peer_discovery, startup_discovery_active, BootstrapParams,
|
||||||
|
};
|
||||||
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;
|
||||||
|
|
@ -124,6 +126,17 @@ fn node_runtime_context() -> Option<NodeRuntimeContext> {
|
||||||
.and_then(|context| context.clone())
|
.and_then(|context| context.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn preferred_miner_connection_type(
|
||||||
|
peer_wallet_short_address: &str,
|
||||||
|
) -> Option<ConnectionType> {
|
||||||
|
let local_wallet = node_runtime_context()?.wallet.saved.short_address.clone();
|
||||||
|
if local_wallet.as_str() < peer_wallet_short_address {
|
||||||
|
Some(ConnectionType::Outgoing)
|
||||||
|
} else {
|
||||||
|
Some(ConnectionType::Incoming)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn spawn_outage_recovery_from_runtime() {
|
pub(crate) fn spawn_outage_recovery_from_runtime() {
|
||||||
let Some(context) = node_runtime_context() else {
|
let Some(context) = node_runtime_context() else {
|
||||||
return;
|
return;
|
||||||
|
|
@ -185,6 +198,7 @@ async fn reconnect_replacement_inner(excluded_ip: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)] // Retained while direct dropped-peer retries are disabled below.
|
||||||
async fn retry_dropped_outgoing(ip: String, port: u16) {
|
async fn retry_dropped_outgoing(ip: String, port: u16) {
|
||||||
let recovery_key = format!("peer:{ip}");
|
let recovery_key = format!("peer:{ip}");
|
||||||
let Some(_recovery_guard) = try_start_recovery(recovery_key) else {
|
let Some(_recovery_guard) = try_start_recovery(recovery_key) else {
|
||||||
|
|
@ -199,7 +213,7 @@ async fn retry_dropped_outgoing(ip: String, port: u16) {
|
||||||
|
|
||||||
// Losing every miner peer is process-level outage recovery, not an
|
// Losing every miner peer is process-level outage recovery, not an
|
||||||
// independent retry of each socket that happened to time out.
|
// independent retry of each socket that happened to time out.
|
||||||
if miner_connection_count().await == 0 {
|
if sync_capable_miner_connection_count().await == 0 {
|
||||||
spawn_outage_recovery(context.db, context.wallet, context.map);
|
spawn_outage_recovery(context.db, context.wallet, context.map);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -236,10 +250,7 @@ async fn retry_dropped_outgoing(ip: String, port: u16) {
|
||||||
wallet: context.wallet.clone(),
|
wallet: context.wallet.clone(),
|
||||||
db: context.db.clone(),
|
db: context.db.clone(),
|
||||||
map: context.map.clone(),
|
map: context.map.clone(),
|
||||||
// A miner stream that is still completing setup already has a
|
first: sync_capable_miner_connection_count().await == 0 && !startup_discovery_active(),
|
||||||
// startup owner. Counting only ready peers could launch a second
|
|
||||||
// full startup synchronization while the first one is active.
|
|
||||||
first: miner_connection_count().await == 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match connect_and_handshake(connect).await {
|
match connect_and_handshake(connect).await {
|
||||||
|
|
@ -268,9 +279,13 @@ async fn retry_dropped_outgoing(ip: String, port: u16) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn_retry_dropped_outgoing(ip: String, port: u16) {
|
pub fn spawn_retry_dropped_outgoing(ip: String, port: u16) {
|
||||||
tokio::spawn(async move {
|
// Temporarily disabled: let the disconnected node initiate recovery.
|
||||||
retry_dropped_outgoing(ip, port).await;
|
// Keep this common entry point so all drop paths (including penalties)
|
||||||
});
|
// follow the same policy, and restoring retries only requires uncommenting.
|
||||||
|
let _ = (ip, port);
|
||||||
|
// tokio::spawn(async move {
|
||||||
|
// retry_dropped_outgoing(ip, port).await;
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn_reconnect_bootstrap(params: BootstrapParams) {
|
pub fn spawn_reconnect_bootstrap(params: BootstrapParams) {
|
||||||
|
|
@ -387,7 +402,13 @@ impl Connection {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let stream = Arc::clone(&connection_info.stream);
|
let stream = Arc::clone(&connection_info.stream);
|
||||||
|
let peer = format!("{ip}:{port}");
|
||||||
|
let command_map = node_runtime_context().map(|context| context.map);
|
||||||
|
let retirement_cutoff = crate::Instant::now();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
if let Some(command_map) = command_map {
|
||||||
|
retire_peer_entries_created_before(command_map, &peer, retirement_cutoff).await;
|
||||||
|
}
|
||||||
let mut stream_guard = stream.lock().await;
|
let mut stream_guard = stream.lock().await;
|
||||||
let _ = stream_guard.shutdown().await;
|
let _ = stream_guard.shutdown().await;
|
||||||
});
|
});
|
||||||
|
|
@ -528,7 +549,7 @@ impl Connection {
|
||||||
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 {
|
if sync_capable_miner_connection_count().await == 0 {
|
||||||
spawn_outage_recovery_from_runtime();
|
spawn_outage_recovery_from_runtime();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -757,6 +778,20 @@ impl Connection {
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn count_sync_capable_miner_connections(&self) -> usize {
|
||||||
|
self.connection_map
|
||||||
|
.values()
|
||||||
|
.filter(|info| {
|
||||||
|
ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
|
||||||
|
&& (info.ready || (info.wallet_registry_synced && info.network_map_synced))
|
||||||
|
&& matches!(
|
||||||
|
info.health,
|
||||||
|
ConnectionHealth::Connected | ConnectionHealth::Busy
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
// Return ready peer streams so broadcast-style paths do not send
|
// Return ready peer streams so broadcast-style paths do not send
|
||||||
// network-wide traffic to peers still completing startup sync.
|
// network-wide traffic to peers still completing startup sync.
|
||||||
pub fn get_all_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> {
|
pub fn get_all_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> {
|
||||||
|
|
@ -835,6 +870,30 @@ impl Connection {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_sync_capable_peer_streams_with_keys(&self) -> Vec<(String, Arc<Mutex<TcpStream>>)> {
|
||||||
|
self.connection_map
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(key, connection_info)| {
|
||||||
|
if ClientType::from_bytes(&connection_info.client_type) != Some(ClientType::Miner)
|
||||||
|
|| (!connection_info.ready
|
||||||
|
&& !(connection_info.wallet_registry_synced
|
||||||
|
&& connection_info.network_map_synced))
|
||||||
|
|| !matches!(
|
||||||
|
connection_info.health,
|
||||||
|
ConnectionHealth::Connected | ConnectionHealth::Busy
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ip = binary_to_ip(key.ip.clone());
|
||||||
|
Some((
|
||||||
|
format!("{}:{}", ip, key.port),
|
||||||
|
Arc::clone(&connection_info.stream),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve a stored outgoing node connection back to its live stream.
|
// Resolve a stored outgoing node connection back to its live stream.
|
||||||
pub fn get_stream_for_outgoing(&self, ip: &str, port: u16) -> Option<Arc<Mutex<TcpStream>>> {
|
pub fn get_stream_for_outgoing(&self, ip: &str, port: u16) -> Option<Arc<Mutex<TcpStream>>> {
|
||||||
let ip_bytes = ip_to_binary(ip);
|
let ip_bytes = ip_to_binary(ip);
|
||||||
|
|
@ -1114,6 +1173,12 @@ fn spawn_monitor_activation(
|
||||||
ConnectionType::from_bytes(&key.connection_type).unwrap_or(ConnectionType::Incoming)
|
ConnectionType::from_bytes(&key.connection_type).unwrap_or(ConnectionType::Incoming)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Err(err) = crate::records::peer_endpoints::remember_peer_endpoints(
|
||||||
|
&context.db,
|
||||||
|
&[format!("{ip}:{port}")],
|
||||||
|
) {
|
||||||
|
warn!("[reconnect] could not cache operational peer endpoint: {err}");
|
||||||
|
}
|
||||||
Connection::client_checkup(stream, connection_type, ip, port, command_map);
|
Connection::client_checkup(stream, connection_type, ip, port, command_map);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1268,6 +1333,15 @@ pub async fn miner_connection_count() -> usize {
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn sync_capable_miner_connection_count() -> usize {
|
||||||
|
CONNECTIONS
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.map(|connection| connection.count_sync_capable_miner_connections())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn mark_peer_wallet_registry_synced(key: &str, stream: &Arc<Mutex<TcpStream>>) -> bool {
|
pub async fn mark_peer_wallet_registry_synced(key: &str, stream: &Arc<Mutex<TcpStream>>) -> bool {
|
||||||
CONNECTIONS
|
CONNECTIONS
|
||||||
.write()
|
.write()
|
||||||
|
|
@ -1616,6 +1690,15 @@ pub async fn startup_synced_peer_streams() -> Vec<(String, Arc<Mutex<TcpStream>>
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn sync_capable_peer_streams() -> Vec<(String, Arc<Mutex<TcpStream>>)> {
|
||||||
|
CONNECTIONS
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.map(|connection| connection.get_sync_capable_peer_streams_with_keys())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_client_type_from_memory(key: &str) -> Option<ClientType> {
|
pub async fn get_client_type_from_memory(key: &str) -> Option<ClientType> {
|
||||||
// Recover the stored client role from the serialized connection key
|
// Recover the stored client role from the serialized connection key
|
||||||
// used throughout the RPC layer.
|
// used throughout the RPC layer.
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,27 @@ fn signature_is_empty(signature: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeInfo {
|
impl NodeInfo {
|
||||||
|
fn new_node_admission_limit_reached(
|
||||||
|
address_map: &HashMap<String, NodeInfo>,
|
||||||
|
edit: &SignedNodeEdit,
|
||||||
|
current_timestamp: u64,
|
||||||
|
) -> bool {
|
||||||
|
// Reconnecting a known wallet does not grow the membership map. All
|
||||||
|
// signature, sponsor, deletion-order and IP-ownership checks still apply.
|
||||||
|
!address_map.contains_key(&edit.address)
|
||||||
|
&& address_map
|
||||||
|
.values()
|
||||||
|
.filter(|node| {
|
||||||
|
node.added_by == edit.modified_by
|
||||||
|
&& current_timestamp.saturating_sub(node.added_timestamp) <= ONE_HOUR_MILLIS
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
>= 10
|
||||||
|
}
|
||||||
|
|
||||||
fn eligible_sponsor_endpoint_from_map(
|
fn eligible_sponsor_endpoint_from_map(
|
||||||
address_map: &HashMap<String, NodeInfo>,
|
address_map: &HashMap<String, NodeInfo>,
|
||||||
operational_addresses: &HashSet<String>,
|
operational_addresses: &HashSet<String>,
|
||||||
current_timestamp: u64,
|
|
||||||
excluded_address: &str,
|
excluded_address: &str,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let mut candidates: Vec<(&String, &NodeInfo)> = address_map
|
let mut candidates: Vec<(&String, &NodeInfo)> = address_map
|
||||||
|
|
@ -33,7 +50,6 @@ impl NodeInfo {
|
||||||
address.as_str() != excluded_address
|
address.as_str() != excluded_address
|
||||||
&& operational_addresses.contains(address.as_str())
|
&& operational_addresses.contains(address.as_str())
|
||||||
&& node.deleted_timestamp == 0
|
&& node.deleted_timestamp == 0
|
||||||
&& current_timestamp.saturating_sub(node.added_timestamp) >= ONE_HOUR_MILLIS
|
|
||||||
&& node.blocks_mined >= 100
|
&& node.blocks_mined >= 100
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -50,13 +66,11 @@ impl NodeInfo {
|
||||||
fn sponsor_redirect_suffix(
|
fn sponsor_redirect_suffix(
|
||||||
address_map: &HashMap<String, NodeInfo>,
|
address_map: &HashMap<String, NodeInfo>,
|
||||||
operational_addresses: &HashSet<String>,
|
operational_addresses: &HashSet<String>,
|
||||||
current_timestamp: u64,
|
|
||||||
excluded_address: &str,
|
excluded_address: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
Self::eligible_sponsor_endpoint_from_map(
|
Self::eligible_sponsor_endpoint_from_map(
|
||||||
address_map,
|
address_map,
|
||||||
operational_addresses,
|
operational_addresses,
|
||||||
current_timestamp,
|
|
||||||
excluded_address,
|
excluded_address,
|
||||||
)
|
)
|
||||||
.map(|endpoint| format!(" sponsor={endpoint}"))
|
.map(|endpoint| format!(" sponsor={endpoint}"))
|
||||||
|
|
@ -921,8 +935,8 @@ impl NodeInfo {
|
||||||
{
|
{
|
||||||
let mut address_map = ADDRESS_MAP.lock().await;
|
let mut address_map = ADDRESS_MAP.lock().await;
|
||||||
|
|
||||||
// Once the chain is mature, adding nodes is restricted to older
|
// Once the chain is mature, adding nodes is restricted to active
|
||||||
// active participants with sufficient mined history.
|
// participants with sufficient mined history.
|
||||||
let local_height = get_height(&db);
|
let local_height = get_height(&db);
|
||||||
if !Self::self_add_allowed_at_height(local_height) {
|
if !Self::self_add_allowed_at_height(local_height) {
|
||||||
let signer_key = Wallet::normalize_to_short_address(&edit.modified_by)
|
let signer_key = Wallet::normalize_to_short_address(&edit.modified_by)
|
||||||
|
|
@ -930,30 +944,21 @@ impl NodeInfo {
|
||||||
let signer_node = address_map.get(&signer_key);
|
let signer_node = address_map.get(&signer_key);
|
||||||
let signer_is_local = signer_key == wallet.saved.short_address;
|
let signer_is_local = signer_key == wallet.saved.short_address;
|
||||||
let valid_added_by = signer_node
|
let valid_added_by = signer_node
|
||||||
.map(|node| {
|
.map(|node| node.deleted_timestamp == 0)
|
||||||
current_timestamp.saturating_sub(node.added_timestamp) >= ONE_HOUR_MILLIS
|
|
||||||
&& node.deleted_timestamp == 0
|
|
||||||
&& (!signer_is_local
|
|
||||||
|| Self::local_runtime_is_mature(current_timestamp))
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if !valid_added_by {
|
if !valid_added_by {
|
||||||
let signer_exists = signer_node.is_some();
|
let signer_exists = signer_node.is_some();
|
||||||
let signer_deleted_timestamp =
|
let signer_deleted_timestamp =
|
||||||
signer_node.map(|node| node.deleted_timestamp).unwrap_or(0);
|
signer_node.map(|node| node.deleted_timestamp).unwrap_or(0);
|
||||||
let signer_age_ms = signer_node
|
|
||||||
.map(|node| current_timestamp.saturating_sub(node.added_timestamp))
|
|
||||||
.unwrap_or(0);
|
|
||||||
let signer_blocks_mined =
|
let signer_blocks_mined =
|
||||||
signer_node.map(|node| node.blocks_mined).unwrap_or(0);
|
signer_node.map(|node| node.blocks_mined).unwrap_or(0);
|
||||||
let redirect = Self::sponsor_redirect_suffix(
|
let redirect = Self::sponsor_redirect_suffix(
|
||||||
&address_map,
|
&address_map,
|
||||||
&operational_addresses,
|
&operational_addresses,
|
||||||
current_timestamp,
|
|
||||||
&signer_key,
|
&signer_key,
|
||||||
);
|
);
|
||||||
return RpcResponse::Binary(format!(
|
return RpcResponse::Binary(format!(
|
||||||
"Error: This address cannot add nodes. It must exist for at least 60 minutes and not be marked for deletion local_height={local_height} self_add_block={} self_add_limit={} signer={signer_key} signer_exists={signer_exists} signer_is_local={signer_is_local} signer_deleted_timestamp={signer_deleted_timestamp} signer_age_ms={signer_age_ms} signer_blocks_mined={signer_blocks_mined}{redirect}",
|
"Error: This address cannot add nodes because it is missing or marked for deletion local_height={local_height} self_add_block={} self_add_limit={} signer={signer_key} signer_exists={signer_exists} signer_is_local={signer_is_local} signer_deleted_timestamp={signer_deleted_timestamp} signer_blocks_mined={signer_blocks_mined}{redirect}",
|
||||||
super::SELF_ADD_BLOCK,
|
super::SELF_ADD_BLOCK,
|
||||||
Self::self_add_limit_height(),
|
Self::self_add_limit_height(),
|
||||||
)
|
)
|
||||||
|
|
@ -964,7 +969,6 @@ impl NodeInfo {
|
||||||
let redirect = Self::sponsor_redirect_suffix(
|
let redirect = Self::sponsor_redirect_suffix(
|
||||||
&address_map,
|
&address_map,
|
||||||
&operational_addresses,
|
&operational_addresses,
|
||||||
current_timestamp,
|
|
||||||
&signer_key,
|
&signer_key,
|
||||||
);
|
);
|
||||||
return RpcResponse::Binary(format!(
|
return RpcResponse::Binary(format!(
|
||||||
|
|
@ -973,15 +977,7 @@ impl NodeInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let added_by_count_in_last_hour = address_map
|
if Self::new_node_admission_limit_reached(&address_map, &edit, current_timestamp) {
|
||||||
.values()
|
|
||||||
.filter(|node| {
|
|
||||||
node.added_by == edit.modified_by
|
|
||||||
&& current_timestamp.saturating_sub(node.added_timestamp) <= ONE_HOUR_MILLIS
|
|
||||||
})
|
|
||||||
.count();
|
|
||||||
|
|
||||||
if added_by_count_in_last_hour >= 10 {
|
|
||||||
return RpcResponse::Binary(
|
return RpcResponse::Binary(
|
||||||
b"Error: Cannot add more than 10 nodes in 60 minutes".to_vec(),
|
b"Error: Cannot add more than 10 nodes in 60 minutes".to_vec(),
|
||||||
);
|
);
|
||||||
|
|
@ -1144,6 +1140,53 @@ impl NodeInfo {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_admission_window_allows_known_wallet_but_not_a_new_wallet_on_same_ip() {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for index in 0..10 {
|
||||||
|
map.insert(
|
||||||
|
format!("node-{index}"),
|
||||||
|
NodeInfo::new(
|
||||||
|
format!("8.8.8.{}", index + 1),
|
||||||
|
50050,
|
||||||
|
0,
|
||||||
|
"sponsor".into(),
|
||||||
|
100,
|
||||||
|
"signature".into(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut edit = SignedNodeEdit {
|
||||||
|
address: "node-0".into(),
|
||||||
|
ip: "8.8.8.1".into(),
|
||||||
|
port: 50050,
|
||||||
|
modified_by: "sponsor".into(),
|
||||||
|
modified_timestamp: 200,
|
||||||
|
modified_signature: "signature".into(),
|
||||||
|
};
|
||||||
|
map.get_mut("node-0").unwrap().deleted_timestamp = 150;
|
||||||
|
map.get_mut("node-0").unwrap().deleted_block = 50;
|
||||||
|
assert!(!NodeInfo::new_node_admission_limit_reached(
|
||||||
|
&map, &edit, 200
|
||||||
|
));
|
||||||
|
edit.address = "replacement-wallet".into();
|
||||||
|
assert!(NodeInfo::new_node_admission_limit_reached(&map, &edit, 200));
|
||||||
|
assert!(NodeInfo::new_node_admission_limit_reached(
|
||||||
|
&map,
|
||||||
|
&edit,
|
||||||
|
100 + ONE_HOUR_MILLIS
|
||||||
|
));
|
||||||
|
assert!(!NodeInfo::new_node_admission_limit_reached(
|
||||||
|
&map,
|
||||||
|
&edit,
|
||||||
|
101 + ONE_HOUR_MILLIS
|
||||||
|
));
|
||||||
|
map.remove("node-9");
|
||||||
|
assert!(!NodeInfo::new_node_admission_limit_reached(
|
||||||
|
&map, &edit, 200
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
fn snapshot_node(ip: &str, timestamp: u64, monitors: &[&str]) -> NodeInfo {
|
fn snapshot_node(ip: &str, timestamp: u64, monitors: &[&str]) -> NodeInfo {
|
||||||
let mut node = NodeInfo::new(
|
let mut node = NodeInfo::new(
|
||||||
ip.to_string(),
|
ip.to_string(),
|
||||||
|
|
@ -1181,7 +1224,6 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sponsor_redirect_selects_an_eligible_different_node() {
|
fn sponsor_redirect_selects_an_eligible_different_node() {
|
||||||
let now = ONE_HOUR_MILLIS * 3;
|
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
let mut rejected = snapshot_node("1.2.3.4", 1, &[]);
|
let mut rejected = snapshot_node("1.2.3.4", 1, &[]);
|
||||||
|
|
@ -1202,7 +1244,6 @@ mod tests {
|
||||||
NodeInfo::eligible_sponsor_endpoint_from_map(
|
NodeInfo::eligible_sponsor_endpoint_from_map(
|
||||||
&map,
|
&map,
|
||||||
&HashSet::from(["newer.cltc".to_string(), "older.cltc".to_string()]),
|
&HashSet::from(["newer.cltc".to_string(), "older.cltc".to_string()]),
|
||||||
now,
|
|
||||||
"rejected.cltc",
|
"rejected.cltc",
|
||||||
),
|
),
|
||||||
Some("1.2.3.5:50051".to_string())
|
Some("1.2.3.5:50051".to_string())
|
||||||
|
|
@ -1210,7 +1251,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sponsor_redirect_ignores_deleted_young_and_under_mined_nodes() {
|
fn sponsor_redirect_allows_recent_active_mined_node() {
|
||||||
let now = ONE_HOUR_MILLIS * 3;
|
let now = ONE_HOUR_MILLIS * 3;
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
|
@ -1219,9 +1260,9 @@ mod tests {
|
||||||
deleted.deleted_timestamp = now;
|
deleted.deleted_timestamp = now;
|
||||||
map.insert("deleted.cltc".to_string(), deleted);
|
map.insert("deleted.cltc".to_string(), deleted);
|
||||||
|
|
||||||
let mut young = snapshot_node("1.2.3.5", now - 1_000, &[]);
|
let mut recent = snapshot_node("1.2.3.5", now - 1_000, &[]);
|
||||||
young.blocks_mined = 100;
|
recent.blocks_mined = 100;
|
||||||
map.insert("young.cltc".to_string(), young);
|
map.insert("recent.cltc".to_string(), recent);
|
||||||
|
|
||||||
let mut under_mined = snapshot_node("1.2.3.6", 1, &[]);
|
let mut under_mined = snapshot_node("1.2.3.6", 1, &[]);
|
||||||
under_mined.blocks_mined = 99;
|
under_mined.blocks_mined = 99;
|
||||||
|
|
@ -1232,13 +1273,12 @@ mod tests {
|
||||||
&map,
|
&map,
|
||||||
&HashSet::from([
|
&HashSet::from([
|
||||||
"deleted.cltc".to_string(),
|
"deleted.cltc".to_string(),
|
||||||
"young.cltc".to_string(),
|
"recent.cltc".to_string(),
|
||||||
"under-mined.cltc".to_string(),
|
"under-mined.cltc".to_string(),
|
||||||
]),
|
]),
|
||||||
now,
|
|
||||||
"none.cltc",
|
"none.cltc",
|
||||||
),
|
),
|
||||||
None
|
Some("1.2.3.5:50050".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ use crate::wallets::structures::Wallet;
|
||||||
use crate::Arc;
|
use crate::Arc;
|
||||||
use crate::HashMap;
|
use crate::HashMap;
|
||||||
use crate::Mutex;
|
use crate::Mutex;
|
||||||
use crate::OnceLock;
|
|
||||||
use crate::Utc;
|
use crate::Utc;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
|
@ -47,8 +46,6 @@ lazy_static! {
|
||||||
pub const SELF_ADD_BLOCK: u32 = 0;
|
pub const SELF_ADD_BLOCK: u32 = 0;
|
||||||
pub const SELF_ADD_WINDOW_BLOCKS: u32 = 10_000;
|
pub const SELF_ADD_WINDOW_BLOCKS: u32 = 10_000;
|
||||||
|
|
||||||
static NODE_RUNTIME_STARTED_MILLIS: OnceLock<u64> = OnceLock::new();
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NodeInfo {
|
pub struct NodeInfo {
|
||||||
ip: String,
|
ip: String,
|
||||||
|
|
@ -63,18 +60,6 @@ pub struct NodeInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeInfo {
|
impl NodeInfo {
|
||||||
pub fn initialize_runtime_start() {
|
|
||||||
let _ = NODE_RUNTIME_STARTED_MILLIS.set(Utc::now().timestamp_millis() as u64);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn runtime_started_millis() -> u64 {
|
|
||||||
*NODE_RUNTIME_STARTED_MILLIS.get_or_init(|| Utc::now().timestamp_millis() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn local_runtime_is_mature(current_timestamp: u64) -> bool {
|
|
||||||
current_timestamp.saturating_sub(Self::runtime_started_millis()) >= 3_600_000
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn self_add_allowed_at_height(height: u32) -> bool {
|
pub fn self_add_allowed_at_height(height: u32) -> bool {
|
||||||
height <= SELF_ADD_BLOCK.saturating_add(SELF_ADD_WINDOW_BLOCKS)
|
height <= SELF_ADD_BLOCK.saturating_add(SELF_ADD_WINDOW_BLOCKS)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ use crate::sled::Db;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
impl NodeInfo {
|
impl NodeInfo {
|
||||||
|
pub(crate) async fn mapping_sync_record_count() -> usize {
|
||||||
|
let memberships = ADDRESS_MAP.lock().await.len();
|
||||||
|
let monitor_events = MONITOR_EVENT_STATE.lock().await.len();
|
||||||
|
memberships.saturating_add(monitor_events)
|
||||||
|
}
|
||||||
|
|
||||||
fn eligible_at_block(node_info: &NodeInfo, block_number: u32) -> bool {
|
fn eligible_at_block(node_info: &NodeInfo, block_number: u32) -> bool {
|
||||||
node_info.deleted_timestamp == 0
|
node_info.deleted_timestamp == 0
|
||||||
|| (node_info.deleted_block > 0 && block_number < node_info.deleted_block)
|
|| (node_info.deleted_block > 0 && block_number < node_info.deleted_block)
|
||||||
|
|
|
||||||
|
|
@ -198,11 +198,22 @@ pub async fn delete_entry(map: Arc<Mutex<Command>>, key: Byte3) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn retire_peer_entries(map: Arc<Mutex<Command>>, peer: &str) {
|
pub async fn retire_peer_entries(map: Arc<Mutex<Command>>, peer: &str) {
|
||||||
|
retire_peer_entries_created_before(map, peer, Instant::now()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn retire_peer_entries_created_before(
|
||||||
|
map: Arc<Mutex<Command>>,
|
||||||
|
peer: &str,
|
||||||
|
cutoff: Instant,
|
||||||
|
) {
|
||||||
let mut map = map.lock().await;
|
let mut map = map.lock().await;
|
||||||
let expires_at = Instant::now() + Duration::from_secs(30);
|
let expires_at = Instant::now() + Duration::from_secs(30);
|
||||||
|
|
||||||
for channel_pair in map.values_mut() {
|
for channel_pair in map.values_mut() {
|
||||||
if channel_pair.peer.as_deref() != Some(peer) || channel_pair.expires_at.is_some() {
|
if channel_pair.peer.as_deref() != Some(peer)
|
||||||
|
|| channel_pair.expires_at.is_some()
|
||||||
|
|| channel_pair.created_at > cutoff
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod balance_sheet;
|
||||||
pub mod block_height;
|
pub mod block_height;
|
||||||
pub mod ip_score;
|
pub mod ip_score;
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
|
pub mod peer_endpoints;
|
||||||
pub mod record_chain;
|
pub mod record_chain;
|
||||||
pub mod unpack_block;
|
pub mod unpack_block;
|
||||||
pub mod wallet_registry;
|
pub mod wallet_registry;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
//! Durable dial hints only. These records never establish membership or mining eligibility.
|
||||||
|
use crate::common::network_startup::canonical_miner_endpoint;
|
||||||
|
use crate::sled::Db;
|
||||||
|
|
||||||
|
const TREE: &str = "peer_endpoint_cache";
|
||||||
|
const KEY: &[u8] = b"endpoints_v1";
|
||||||
|
const MAX_ENDPOINTS: usize = 512;
|
||||||
|
|
||||||
|
fn merge_endpoints(preferred: &[String], previous: &[String]) -> Vec<String> {
|
||||||
|
let mut endpoints = Vec::new();
|
||||||
|
for value in preferred.iter().chain(previous) {
|
||||||
|
if let Some(endpoint) = canonical_miner_endpoint(value) {
|
||||||
|
if !endpoints.contains(&endpoint) {
|
||||||
|
endpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
if endpoints.len() == MAX_ENDPOINTS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remember_peer_endpoints(db: &Db, endpoints: &[String]) -> sled::Result<()> {
|
||||||
|
let preferred = merge_endpoints(endpoints, &[]);
|
||||||
|
if preferred.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// Atomic read/modify/write preserves discoveries from concurrent peer setups.
|
||||||
|
db.open_tree(TREE)?.update_and_fetch(KEY, |previous| {
|
||||||
|
let previous: Vec<String> = previous
|
||||||
|
.and_then(|bytes| serde_json::from_slice(bytes).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
Some(serde_json::to_vec(&merge_endpoints(&preferred, &previous)).unwrap())
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remembered_peer_endpoints(db: &Db) -> sled::Result<Vec<String>> {
|
||||||
|
let previous: Vec<String> = db
|
||||||
|
.open_tree(TREE)?
|
||||||
|
.get(KEY)?
|
||||||
|
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
// Revalidate disk hints and tolerate malformed cache data independently of chain state.
|
||||||
|
Ok(merge_endpoints(&previous, &[]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hints_survive_reopening_without_restoring_membership() {
|
||||||
|
let path =
|
||||||
|
std::env::temp_dir().join(format!("contractless-peers-{}", rand::random::<u128>()));
|
||||||
|
{
|
||||||
|
let db = sled::open(&path).unwrap();
|
||||||
|
remember_peer_endpoints(&db, &["8.8.8.8:50050".into()]).unwrap();
|
||||||
|
db.flush().unwrap();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let db = sled::open(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
remembered_peer_endpoints(&db).unwrap(),
|
||||||
|
vec!["8.8.8.8:50050"]
|
||||||
|
);
|
||||||
|
assert!(db.get(b"height").unwrap().is_none());
|
||||||
|
}
|
||||||
|
std::fs::remove_dir_all(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_is_bounded_deduplicated_and_filters_invalid_endpoints() {
|
||||||
|
let db = sled::Config::new().temporary(true).open().unwrap();
|
||||||
|
let endpoints = (1..=600)
|
||||||
|
.map(|port| format!("8.8.8.8:{port}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
remember_peer_endpoints(&db, &endpoints).unwrap();
|
||||||
|
remember_peer_endpoints(
|
||||||
|
&db,
|
||||||
|
&[
|
||||||
|
"1.1.1.1:50050".into(),
|
||||||
|
"1.1.1.1:50050".into(),
|
||||||
|
"127.0.0.1:50050".into(),
|
||||||
|
"10.0.0.1:50050".into(),
|
||||||
|
"8.8.4.4:0".into(),
|
||||||
|
"bad".into(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let peers = remembered_peer_endpoints(&db).unwrap();
|
||||||
|
assert_eq!(peers.len(), MAX_ENDPOINTS);
|
||||||
|
assert_eq!(peers[0], "1.1.1.1:50050");
|
||||||
|
assert_eq!(peers[1], "8.8.8.8:1");
|
||||||
|
assert!(!peers
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.starts_with("127.") || p.starts_with("10.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn concurrent_discoveries_do_not_overwrite_each_other() {
|
||||||
|
let db = sled::Config::new().temporary(true).open().unwrap();
|
||||||
|
let workers = (1..=16)
|
||||||
|
.map(|port| {
|
||||||
|
let db = db.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
remember_peer_endpoints(&db, &[format!("8.8.8.8:{port}")]).unwrap();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for worker in workers {
|
||||||
|
worker.join().unwrap();
|
||||||
|
}
|
||||||
|
let peers = remembered_peer_endpoints(&db).unwrap();
|
||||||
|
assert_eq!(peers.len(), 16);
|
||||||
|
for port in 1..=16 {
|
||||||
|
assert!(peers.contains(&format!("8.8.8.8:{port}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn corrupt_cache_does_not_block_startup_or_future_discovery() {
|
||||||
|
let db = sled::Config::new().temporary(true).open().unwrap();
|
||||||
|
db.open_tree(TREE).unwrap().insert(KEY, b"broken").unwrap();
|
||||||
|
assert!(remembered_peer_endpoints(&db).unwrap().is_empty());
|
||||||
|
remember_peer_endpoints(&db, &["8.8.8.8:50050".into()]).unwrap();
|
||||||
|
assert_eq!(remembered_peer_endpoints(&db).unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,8 @@ use crate::orphans::torrent_candidates::hydrate_torrent_candidates;
|
||||||
use crate::records::block_height::get_block_height::get_height;
|
use crate::records::block_height::get_block_height::get_height;
|
||||||
use crate::records::memory::connections::{
|
use crate::records::memory::connections::{
|
||||||
mark_peer_network_map_synced, mark_peer_operational, mark_peer_wallet_registry_synced,
|
mark_peer_network_map_synced, mark_peer_operational, mark_peer_wallet_registry_synced,
|
||||||
spawn_peer_setup_retry, startup_synced_peer_streams, CONNECTIONS,
|
spawn_outage_recovery_from_runtime, spawn_peer_setup_retry, startup_synced_peer_streams,
|
||||||
|
sync_capable_peer_streams, CONNECTIONS,
|
||||||
};
|
};
|
||||||
use crate::records::memory::network_mapping::NodeInfo;
|
use crate::records::memory::network_mapping::NodeInfo;
|
||||||
use crate::records::memory::response_channels::Command;
|
use crate::records::memory::response_channels::Command;
|
||||||
|
|
@ -50,6 +51,10 @@ use tokio::time::Instant as TokioInstant;
|
||||||
|
|
||||||
static STARTUP_DISCOVERY_ACTIVE: AtomicBool = AtomicBool::new(false);
|
static STARTUP_DISCOVERY_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
pub fn startup_discovery_active() -> bool {
|
||||||
|
STARTUP_DISCOVERY_ACTIVE.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
struct StartupDiscoveryGuard;
|
struct StartupDiscoveryGuard;
|
||||||
|
|
||||||
impl Drop for StartupDiscoveryGuard {
|
impl Drop for StartupDiscoveryGuard {
|
||||||
|
|
@ -107,6 +112,38 @@ pub fn spawn_bootstrap_peer_discovery(params: BootstrapParams) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn wait_for_replacement_sync_peer(
|
||||||
|
failed_stream: &Arc<Mutex<TcpStream>>,
|
||||||
|
) -> (String, Arc<Mutex<TcpStream>>) {
|
||||||
|
let mut recovery_ticks = 0_u16;
|
||||||
|
loop {
|
||||||
|
if let Some(peer) = sync_capable_peer_streams()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.find(|(_, stream)| !Arc::ptr_eq(stream, failed_stream))
|
||||||
|
{
|
||||||
|
return peer;
|
||||||
|
}
|
||||||
|
if recovery_ticks % 20 == 0 {
|
||||||
|
spawn_outage_recovery_from_runtime();
|
||||||
|
}
|
||||||
|
recovery_ticks = recovery_ticks.wrapping_add(1);
|
||||||
|
sleep(Duration::from_millis(250)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_peer_transport_failure(err: &str) -> bool {
|
||||||
|
err.contains("Timed out")
|
||||||
|
|| err.contains("timed out")
|
||||||
|
|| err.contains("Connection reset")
|
||||||
|
|| err.contains("Connection aborted")
|
||||||
|
|| err.contains("Broken pipe")
|
||||||
|
|| err.contains("connection closed")
|
||||||
|
|| err.contains("No response received")
|
||||||
|
|| err.contains("response channel closed")
|
||||||
|
|| err.contains("Failed to send")
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn bootstrap_peer_discovery(params: BootstrapParams) -> Result<(), String> {
|
pub async fn bootstrap_peer_discovery(params: BootstrapParams) -> Result<(), String> {
|
||||||
let chain_sync_guard = if params.run_startup_sync {
|
let chain_sync_guard = if params.run_startup_sync {
|
||||||
Some(begin_chain_sync().await)
|
Some(begin_chain_sync().await)
|
||||||
|
|
@ -115,8 +152,8 @@ pub async fn bootstrap_peer_discovery(params: BootstrapParams) -> Result<(), Str
|
||||||
};
|
};
|
||||||
let (_, _, local_endpoint) = get_ip_and_port().await;
|
let (_, _, local_endpoint) = get_ip_and_port().await;
|
||||||
let max = SETTINGS.outgoing_connections;
|
let max = SETTINGS.outgoing_connections;
|
||||||
let current_key = params.connections_key.clone();
|
let mut current_key = params.connections_key.clone();
|
||||||
let stream = params.stream;
|
let mut stream = params.stream;
|
||||||
let mut imported_chain = false;
|
let mut imported_chain = false;
|
||||||
|
|
||||||
if params.run_startup_sync {
|
if params.run_startup_sync {
|
||||||
|
|
@ -179,13 +216,13 @@ pub async fn bootstrap_peer_discovery(params: BootstrapParams) -> Result<(), Str
|
||||||
}
|
}
|
||||||
|
|
||||||
if !setup_tasks.is_empty() {
|
if !setup_tasks.is_empty() {
|
||||||
let warmup_deadline = TokioInstant::now() + Duration::from_secs(10);
|
let warmup_deadline = TokioInstant::now() + Duration::from_secs(30);
|
||||||
while TokioInstant::now() < warmup_deadline
|
while TokioInstant::now() < warmup_deadline
|
||||||
&& setup_tasks.iter().any(|task| !task.is_finished())
|
&& setup_tasks.iter().any(|task| !task.is_finished())
|
||||||
{
|
{
|
||||||
sleep(Duration::from_millis(100)).await;
|
sleep(Duration::from_millis(100)).await;
|
||||||
}
|
}
|
||||||
let pool_size = startup_synced_peer_streams().await.len();
|
let pool_size = sync_capable_peer_streams().await.len();
|
||||||
info!(
|
info!(
|
||||||
"[sync] torrent peer pool warmed with {pool_size} synchronized peer(s); starting chain synchronization"
|
"[sync] torrent peer pool warmed with {pool_size} synchronized peer(s); starting chain synchronization"
|
||||||
);
|
);
|
||||||
|
|
@ -198,22 +235,50 @@ pub async fn bootstrap_peer_discovery(params: BootstrapParams) -> Result<(), Str
|
||||||
if params.run_startup_sync {
|
if params.run_startup_sync {
|
||||||
loop {
|
loop {
|
||||||
let local_height = get_height(¶ms.db);
|
let local_height = get_height(¶ms.db);
|
||||||
let remote_height =
|
let remote_height = match request_remote_height(
|
||||||
request_remote_height(stream.clone(), params.map.clone(), current_key.clone())
|
stream.clone(),
|
||||||
.await?;
|
params.map.clone(),
|
||||||
ensure_compatible_genesis(
|
current_key.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(height) => height,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"[sync] source peer {current_key} failed height request; selecting another synchronized peer: {err}"
|
||||||
|
);
|
||||||
|
let failed_stream = stream.clone();
|
||||||
|
remove_stream_from_memory(&failed_stream).await;
|
||||||
|
(current_key, stream) = wait_for_replacement_sync_peer(&failed_stream).await;
|
||||||
|
info!("[sync] continuing startup synchronization through {current_key}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = ensure_compatible_genesis(
|
||||||
stream.clone(),
|
stream.clone(),
|
||||||
params.map.clone(),
|
params.map.clone(),
|
||||||
current_key.clone(),
|
current_key.clone(),
|
||||||
remote_height,
|
remote_height,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
if !is_peer_transport_failure(&err) {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
warn!(
|
||||||
|
"[sync] source peer {current_key} failed genesis verification transport; selecting another synchronized peer: {err}"
|
||||||
|
);
|
||||||
|
let failed_stream = stream.clone();
|
||||||
|
remove_stream_from_memory(&failed_stream).await;
|
||||||
|
(current_key, stream) = wait_for_replacement_sync_peer(&failed_stream).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let local_genesis_exists = genesis_checkup().await;
|
let local_genesis_exists = genesis_checkup().await;
|
||||||
|
|
||||||
if !local_genesis_exists || remote_height > local_height + 10 {
|
if !local_genesis_exists || remote_height > local_height + 10 {
|
||||||
imported_chain = true;
|
imported_chain = true;
|
||||||
info!("[sync] Starting sync from {local_height} to {remote_height}");
|
info!("[sync] Starting sync from {local_height} to {remote_height}");
|
||||||
node_syncing(
|
if let Err(err) = node_syncing(
|
||||||
stream.clone(),
|
stream.clone(),
|
||||||
¶ms.db,
|
¶ms.db,
|
||||||
remote_height,
|
remote_height,
|
||||||
|
|
@ -223,7 +288,31 @@ pub async fn bootstrap_peer_discovery(params: BootstrapParams) -> Result<(), Str
|
||||||
current_key.clone(),
|
current_key.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Sync error: {e}"))?;
|
{
|
||||||
|
if err.kind() != io::ErrorKind::ConnectionAborted
|
||||||
|
&& err.kind() != io::ErrorKind::BrokenPipe
|
||||||
|
&& err.kind() != io::ErrorKind::ConnectionReset
|
||||||
|
&& err.kind() != io::ErrorKind::TimedOut
|
||||||
|
{
|
||||||
|
return Err(format!("Sync error: {err}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!(
|
||||||
|
"[sync] source peer {current_key} became unusable; preserving startup sync ownership and selecting another peer: {err}"
|
||||||
|
);
|
||||||
|
let failed_stream = stream.clone();
|
||||||
|
let current_stream = Connection::get_stream_from_memory(¤t_key).await;
|
||||||
|
if current_stream
|
||||||
|
.as_ref()
|
||||||
|
.map(|candidate| Arc::ptr_eq(candidate, &failed_stream))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
remove_stream_from_memory(&failed_stream).await;
|
||||||
|
}
|
||||||
|
(current_key, stream) = wait_for_replacement_sync_peer(&failed_stream).await;
|
||||||
|
info!("[sync] resuming startup synchronization through {current_key}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if !local_genesis_exists && !genesis_checkup().await && remote_height > 0 {
|
if !local_genesis_exists && !genesis_checkup().await && remote_height > 0 {
|
||||||
return Err("Sync completed without obtaining remote genesis".to_string());
|
return Err("Sync completed without obtaining remote genesis".to_string());
|
||||||
}
|
}
|
||||||
|
|
@ -235,57 +324,125 @@ pub async fn bootstrap_peer_discovery(params: BootstrapParams) -> Result<(), Str
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_sync_local_height = get_height(¶ms.db);
|
let (post_sync_local_height, post_sync_remote_height) = loop {
|
||||||
let post_sync_remote_height =
|
let local_height = get_height(¶ms.db);
|
||||||
request_remote_height(stream.clone(), params.map.clone(), current_key.clone()).await?;
|
match request_remote_height(stream.clone(), params.map.clone(), current_key.clone())
|
||||||
|
.await
|
||||||
let imported_candidates = match hydrate_torrent_candidates(
|
{
|
||||||
stream.clone(),
|
Ok(remote_height) => break (local_height, remote_height),
|
||||||
params.map.clone(),
|
Err(err) => {
|
||||||
current_key.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(imported) => {
|
|
||||||
if imported > 0 {
|
|
||||||
warn!(
|
warn!(
|
||||||
"[sync] hydrated {imported} torrent candidates before post-sync orphan check"
|
"[sync] source peer {current_key} failed post-sync height request; selecting another synchronized peer: {err}"
|
||||||
);
|
);
|
||||||
|
let failed_stream = stream.clone();
|
||||||
|
remove_stream_from_memory(&failed_stream).await;
|
||||||
|
(current_key, stream) = wait_for_replacement_sync_peer(&failed_stream).await;
|
||||||
}
|
}
|
||||||
imported
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("[sync] failed to hydrate torrent candidates: {err}");
|
|
||||||
0
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if post_sync_remote_height != post_sync_local_height || imported_candidates > 0 {
|
let imported_candidates = loop {
|
||||||
let orphan_checkup_params = OrphanCheckup2 {
|
match hydrate_torrent_candidates(
|
||||||
stream: stream.clone(),
|
|
||||||
db: params.db.clone(),
|
|
||||||
local_height: post_sync_local_height,
|
|
||||||
remote_height: post_sync_remote_height,
|
|
||||||
recheck_from_height: Some(post_sync_local_height.min(post_sync_remote_height)),
|
|
||||||
map: params.map.clone(),
|
|
||||||
node_syncing: true,
|
|
||||||
connections_key: current_key.clone(),
|
|
||||||
};
|
|
||||||
match sync_checkup(orphan_checkup_params, params.wallet.clone()).await {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(err) => return Err(format!("Post-sync orphan check error: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if imported_chain {
|
|
||||||
sync_governance_state(
|
|
||||||
stream.clone(),
|
stream.clone(),
|
||||||
¶ms.db,
|
|
||||||
params.map.clone(),
|
params.map.clone(),
|
||||||
current_key.clone(),
|
current_key.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Governance state sync error: {err}"))?;
|
{
|
||||||
|
Ok(imported) => {
|
||||||
|
if imported > 0 {
|
||||||
|
warn!(
|
||||||
|
"[sync] hydrated {imported} torrent candidates before post-sync orphan check"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break imported;
|
||||||
|
}
|
||||||
|
Err(err) if is_peer_transport_failure(&err) => {
|
||||||
|
warn!(
|
||||||
|
"[sync] source peer {current_key} failed candidate hydration; selecting another synchronized peer: {err}"
|
||||||
|
);
|
||||||
|
let failed_stream = stream.clone();
|
||||||
|
remove_stream_from_memory(&failed_stream).await;
|
||||||
|
(current_key, stream) = wait_for_replacement_sync_peer(&failed_stream).await;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("[sync] failed to hydrate torrent candidates: {err}");
|
||||||
|
break 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if post_sync_remote_height != post_sync_local_height || imported_candidates > 0 {
|
||||||
|
loop {
|
||||||
|
let local_height = get_height(¶ms.db);
|
||||||
|
let remote_height = match request_remote_height(
|
||||||
|
stream.clone(),
|
||||||
|
params.map.clone(),
|
||||||
|
current_key.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(height) => height,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"[sync] post-sync source {current_key} failed height refresh; selecting another synchronized peer: {err}"
|
||||||
|
);
|
||||||
|
let failed_stream = stream.clone();
|
||||||
|
remove_stream_from_memory(&failed_stream).await;
|
||||||
|
(current_key, stream) =
|
||||||
|
wait_for_replacement_sync_peer(&failed_stream).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let orphan_checkup_params = OrphanCheckup2 {
|
||||||
|
stream: stream.clone(),
|
||||||
|
db: params.db.clone(),
|
||||||
|
local_height,
|
||||||
|
remote_height,
|
||||||
|
recheck_from_height: Some(local_height.min(remote_height)),
|
||||||
|
map: params.map.clone(),
|
||||||
|
node_syncing: true,
|
||||||
|
connections_key: current_key.clone(),
|
||||||
|
};
|
||||||
|
match sync_checkup(orphan_checkup_params, params.wallet.clone()).await {
|
||||||
|
Ok(()) => break,
|
||||||
|
Err(err) if is_peer_transport_failure(&err) => {
|
||||||
|
warn!(
|
||||||
|
"[sync] post-sync orphan source {current_key} disconnected; selecting another synchronized peer: {err}"
|
||||||
|
);
|
||||||
|
let failed_stream = stream.clone();
|
||||||
|
remove_stream_from_memory(&failed_stream).await;
|
||||||
|
(current_key, stream) =
|
||||||
|
wait_for_replacement_sync_peer(&failed_stream).await;
|
||||||
|
}
|
||||||
|
Err(err) => return Err(format!("Post-sync orphan check error: {err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if imported_chain {
|
||||||
|
loop {
|
||||||
|
match sync_governance_state(
|
||||||
|
stream.clone(),
|
||||||
|
¶ms.db,
|
||||||
|
params.map.clone(),
|
||||||
|
current_key.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => break,
|
||||||
|
Err(err) if is_peer_transport_failure(&err) => {
|
||||||
|
warn!(
|
||||||
|
"[sync] source peer {current_key} failed governance synchronization; selecting another synchronized peer: {err}"
|
||||||
|
);
|
||||||
|
let failed_stream = stream.clone();
|
||||||
|
remove_stream_from_memory(&failed_stream).await;
|
||||||
|
(current_key, stream) =
|
||||||
|
wait_for_replacement_sync_peer(&failed_stream).await;
|
||||||
|
}
|
||||||
|
Err(err) => return Err(format!("Governance state sync error: {err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
info!("[governance] finalized state sync completed: peer={current_key}");
|
info!("[governance] finalized state sync completed: peer={current_key}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,6 +540,18 @@ mod tests {
|
||||||
drop(owner);
|
drop(owner);
|
||||||
assert!(try_start_startup_discovery().is_some());
|
assert!(try_start_startup_discovery().is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transport_failures_are_separate_from_consensus_failures() {
|
||||||
|
assert!(is_peer_transport_failure(
|
||||||
|
"Timed out waiting for block hash vote at height 0"
|
||||||
|
));
|
||||||
|
assert!(is_peer_transport_failure("Connection reset by peer"));
|
||||||
|
assert!(!is_peer_transport_failure(
|
||||||
|
"incompatible genesis from peer: local=a remote=b"
|
||||||
|
));
|
||||||
|
assert!(!is_peer_transport_failure("Incorrect previous_block_hash"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn process_handshake_response(
|
pub async fn process_handshake_response(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
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::{
|
||||||
miner_connection_count, set_stream_health, spawn_outage_recovery_from_runtime,
|
preferred_miner_connection_type, set_stream_health, spawn_outage_recovery_from_runtime,
|
||||||
spawn_retry_dropped_outgoing, CONNECTIONS,
|
spawn_retry_dropped_outgoing, sync_capable_miner_connection_count, 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::{
|
||||||
|
|
@ -90,7 +90,12 @@ async fn miner_stream_health(
|
||||||
health
|
health
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn purge_stale_duplicate_miner(ip: &str, command_map: Arc<Mutex<Command>>) -> bool {
|
async fn purge_stale_duplicate_miner(
|
||||||
|
ip: &str,
|
||||||
|
new_connection_type: ConnectionType,
|
||||||
|
peer_wallet_short_address: &str,
|
||||||
|
command_map: Arc<Mutex<Command>>,
|
||||||
|
) -> bool {
|
||||||
let ip_bytes = ip_to_binary(ip);
|
let ip_bytes = ip_to_binary(ip);
|
||||||
let duplicate = {
|
let duplicate = {
|
||||||
let guard = CONNECTIONS.read().await;
|
let guard = CONNECTIONS.read().await;
|
||||||
|
|
@ -114,13 +119,20 @@ async fn purge_stale_duplicate_miner(ip: &str, command_map: Arc<Mutex<Command>>)
|
||||||
connection_key.port,
|
connection_key.port,
|
||||||
Arc::clone(&connection_info.stream),
|
Arc::clone(&connection_info.stream),
|
||||||
connection_info.health,
|
connection_info.health,
|
||||||
|
connection_info.ready,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some((connection_type, duplicate_ip, duplicate_port, duplicate_stream, recorded_health)) =
|
let Some((
|
||||||
duplicate
|
connection_type,
|
||||||
|
duplicate_ip,
|
||||||
|
duplicate_port,
|
||||||
|
duplicate_stream,
|
||||||
|
recorded_health,
|
||||||
|
ready,
|
||||||
|
)) = duplicate
|
||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
@ -128,14 +140,30 @@ 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 = miner_stream_health(
|
if !ready && connection_type != new_connection_type {
|
||||||
&duplicate_key,
|
// Simultaneous cross-connections are resolved identically at
|
||||||
duplicate_stream.clone(),
|
// both ends: the lexicographically smaller wallet owns the
|
||||||
command_map.clone(),
|
// outgoing half of the one surviving TCP connection.
|
||||||
)
|
let Some(preferred_type) =
|
||||||
.await;
|
preferred_miner_connection_type(peer_wallet_short_address)
|
||||||
if matches!(health, ConnectionHealth::Connected | ConnectionHealth::Busy) {
|
else {
|
||||||
return false;
|
return false;
|
||||||
|
};
|
||||||
|
if connection_type == preferred_type || new_connection_type != preferred_type {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Same-direction duplicates and established connections keep
|
||||||
|
// the original stale-stream probe used by reconnect recovery.
|
||||||
|
let health = miner_stream_health(
|
||||||
|
&duplicate_key,
|
||||||
|
duplicate_stream.clone(),
|
||||||
|
command_map.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if matches!(health, ConnectionHealth::Connected | ConnectionHealth::Busy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConnectionHealth::Unresponsive | ConnectionHealth::Closed => {}
|
ConnectionHealth::Unresponsive | ConnectionHealth::Closed => {}
|
||||||
|
|
@ -201,7 +229,14 @@ pub async fn write_outgoing_miner_to_memory(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !purge_stale_duplicate_miner(&ip, command_map.clone()).await {
|
if !purge_stale_duplicate_miner(
|
||||||
|
&ip,
|
||||||
|
ConnectionType::Outgoing,
|
||||||
|
&wallet_short_address,
|
||||||
|
command_map.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,7 +298,13 @@ pub async fn write_to_memory(
|
||||||
drop(connection_instance);
|
drop(connection_instance);
|
||||||
if !added
|
if !added
|
||||||
&& client_type == ClientType::Miner
|
&& client_type == ClientType::Miner
|
||||||
&& purge_stale_duplicate_miner(&ip, command_map.clone()).await
|
&& purge_stale_duplicate_miner(
|
||||||
|
&ip,
|
||||||
|
ConnectionType::Incoming,
|
||||||
|
&wallet_short_address,
|
||||||
|
command_map.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
let mut retry_connection_instance = CONNECTIONS.write().await;
|
let mut retry_connection_instance = CONNECTIONS.write().await;
|
||||||
if let Some(mut retry_connection) = retry_connection_instance.take() {
|
if let Some(mut retry_connection) = retry_connection_instance.take() {
|
||||||
|
|
@ -344,7 +385,7 @@ pub async fn remove_stream_from_memory(stream: &Arc<Mutex<TcpStream>>) {
|
||||||
{
|
{
|
||||||
spawn_retry_dropped_outgoing(ip.clone(), port);
|
spawn_retry_dropped_outgoing(ip.clone(), port);
|
||||||
}
|
}
|
||||||
if miner_connection_count().await == 0 {
|
if sync_capable_miner_connection_count().await == 0 {
|
||||||
spawn_outage_recovery_from_runtime();
|
spawn_outage_recovery_from_runtime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,28 @@ use crate::wallets::structures::Wallet;
|
||||||
use crate::Arc;
|
use crate::Arc;
|
||||||
use crate::Utc;
|
use crate::Utc;
|
||||||
|
|
||||||
|
fn is_bulk_mapping_record(client_type: ClientType, command: u8) -> bool {
|
||||||
|
client_type == ClientType::Miner
|
||||||
|
&& matches!(
|
||||||
|
command,
|
||||||
|
RPC_NETWORK_MEMBERSHIP_RECONCILE | RPC_NETWORK_MONITOR_ADD | RPC_NETWORK_MONITOR_REMOVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_limit(client_type: ClientType, command: u8, mapping_records: usize) -> u32 {
|
||||||
|
if !is_bulk_mapping_record(client_type, command) {
|
||||||
|
return RPC_LONG_WINDOW_LIMIT;
|
||||||
|
}
|
||||||
|
// Allow an initial full reconciliation plus three setup retries, alongside
|
||||||
|
// the ordinary live-update allowance. Still score sustained repeated floods.
|
||||||
|
// Never sleep in the shared RPC reader: that would also stall block replies.
|
||||||
|
RPC_LONG_WINDOW_LIMIT.saturating_add(
|
||||||
|
u32::try_from(mapping_records)
|
||||||
|
.unwrap_or(u32::MAX)
|
||||||
|
.saturating_mul(4),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub const MAX_TORRENT_METADATA_BYTES: usize = 8192;
|
pub const MAX_TORRENT_METADATA_BYTES: usize = 8192;
|
||||||
pub const RPC_SHORT_WINDOW_SECS: i64 = 2;
|
pub const RPC_SHORT_WINDOW_SECS: i64 = 2;
|
||||||
pub const RPC_LONG_WINDOW_SECS: i64 = 60;
|
pub const RPC_LONG_WINDOW_SECS: i64 = 60;
|
||||||
|
|
@ -51,6 +73,12 @@ pub async fn check_request_frequency_with_client_type(
|
||||||
command: u8,
|
command: u8,
|
||||||
wallet: Arc<Wallet>,
|
wallet: Arc<Wallet>,
|
||||||
) {
|
) {
|
||||||
|
let mapping_records = if is_bulk_mapping_record(client_type, command) {
|
||||||
|
crate::records::memory::network_mapping::NodeInfo::mapping_sync_record_count().await
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let limit = request_limit(client_type, command, mapping_records);
|
||||||
// Keep one compact flood-tracker row per subject and decay the
|
// Keep one compact flood-tracker row per subject and decay the
|
||||||
// counters by elapsed time so stale request history expires
|
// counters by elapsed time so stale request history expires
|
||||||
// automatically without growing the tree unbounded.
|
// automatically without growing the tree unbounded.
|
||||||
|
|
@ -85,7 +113,7 @@ pub async fn check_request_frequency_with_client_type(
|
||||||
// Only sustained long-window flooding is scored; short-window state
|
// Only sustained long-window flooding is scored; short-window state
|
||||||
// exists so the serialized tracker can be extended without changing
|
// exists so the serialized tracker can be extended without changing
|
||||||
// the storage shape again.
|
// the storage shape again.
|
||||||
if state.is_flooding() {
|
if state.long_window_count > limit {
|
||||||
let _ = update_ip_score(
|
let _ = update_ip_score(
|
||||||
&ip,
|
&ip,
|
||||||
client_type.as_str(),
|
client_type.as_str(),
|
||||||
|
|
@ -108,6 +136,78 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::rpc::command_maps::{RPC_TIME, RPC_VALIDATE_ADDRESS};
|
use crate::rpc::command_maps::{RPC_TIME, RPC_VALIDATE_ADDRESS};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn large_mapping_reconciliations_and_retries_fit_but_sustained_floods_do_not() {
|
||||||
|
for command in [
|
||||||
|
RPC_NETWORK_MEMBERSHIP_RECONCILE,
|
||||||
|
RPC_NETWORK_MONITOR_ADD,
|
||||||
|
RPC_NETWORK_MONITOR_REMOVE,
|
||||||
|
] {
|
||||||
|
let limit = request_limit(ClientType::Miner, command, 1_000);
|
||||||
|
let mut state = RpcFloodState::new(0);
|
||||||
|
for _ in 0..4_000 {
|
||||||
|
state.record_request(0);
|
||||||
|
}
|
||||||
|
assert!(state.long_window_count <= limit);
|
||||||
|
for _ in 0..=RPC_LONG_WINDOW_LIMIT {
|
||||||
|
state.record_request(0);
|
||||||
|
}
|
||||||
|
assert!(state.long_window_count > limit);
|
||||||
|
state.record_request(RPC_LONG_WINDOW_SECS);
|
||||||
|
assert!(state.long_window_count <= limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn importing_a_map_from_empty_state_does_not_hit_the_old_250_record_limit() {
|
||||||
|
let mut state = RpcFloodState::new(0);
|
||||||
|
for already_imported in 0..1_000 {
|
||||||
|
state.record_request(0);
|
||||||
|
let limit = request_limit(
|
||||||
|
ClientType::Miner,
|
||||||
|
RPC_NETWORK_MEMBERSHIP_RECONCILE,
|
||||||
|
already_imported,
|
||||||
|
);
|
||||||
|
assert!(state.long_window_count <= limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_miner_bulk_records_get_scaled_limits() {
|
||||||
|
for command in [
|
||||||
|
RPC_NETWORK_MEMBERSHIP_RECONCILE,
|
||||||
|
RPC_NETWORK_MONITOR_ADD,
|
||||||
|
RPC_NETWORK_MONITOR_REMOVE,
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
request_limit(ClientType::Miner, command, 0),
|
||||||
|
RPC_LONG_WINDOW_LIMIT
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
request_limit(ClientType::Client, command, 100_000),
|
||||||
|
RPC_LONG_WINDOW_LIMIT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for command in [
|
||||||
|
RPC_TIME,
|
||||||
|
RPC_ADD_NETWORK_NODE,
|
||||||
|
RPC_SETUP_COMPLETE,
|
||||||
|
RPC_NETWORK_MAPPING_HASH,
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
request_limit(ClientType::Miner, command, 100_000),
|
||||||
|
RPC_LONG_WINDOW_LIMIT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut state = RpcFloodState::new(0);
|
||||||
|
for _ in 0..RPC_LONG_WINDOW_LIMIT {
|
||||||
|
state.record_request(0);
|
||||||
|
}
|
||||||
|
assert!(!state.is_flooding());
|
||||||
|
state.record_request(0);
|
||||||
|
assert!(state.is_flooding());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn miner_control_commands_have_independent_bounded_subjects() {
|
fn miner_control_commands_have_independent_bounded_subjects() {
|
||||||
let ip = "203.0.113.10";
|
let ip = "203.0.113.10";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::common::check_genesis::genesis_checkup;
|
use crate::common::check_genesis::genesis_checkup;
|
||||||
use crate::common::network_startup::canonical_miner_endpoint;
|
use crate::common::network_startup::canonical_miner_endpoint;
|
||||||
use crate::log::{error, warn};
|
use crate::log::{error, warn};
|
||||||
use crate::miner::flag::{begin_chain_sync, ChainOperationGuard};
|
use crate::miner::flag::{begin_connected_peer_sync, ChainOperationGuard};
|
||||||
use crate::orphans::structs::OrphanCheckup2;
|
use crate::orphans::structs::OrphanCheckup2;
|
||||||
use crate::orphans::sync_check::sync_checkup;
|
use crate::orphans::sync_check::sync_checkup;
|
||||||
use crate::orphans::torrent_candidates::hydrate_torrent_candidates;
|
use crate::orphans::torrent_candidates::hydrate_torrent_candidates;
|
||||||
|
|
@ -17,6 +17,7 @@ use crate::records::memory::structs::Connection;
|
||||||
use crate::rpc::client::genesis_compat::ensure_compatible_genesis;
|
use crate::rpc::client::genesis_compat::ensure_compatible_genesis;
|
||||||
use crate::rpc::client::governance_state_sync::sync_governance_state;
|
use crate::rpc::client::governance_state_sync::sync_governance_state;
|
||||||
use crate::rpc::client::handshake::{connect_and_handshake, sponsor_endpoint_from_error};
|
use crate::rpc::client::handshake::{connect_and_handshake, sponsor_endpoint_from_error};
|
||||||
|
use crate::rpc::client::handshake_processing::startup_discovery_active;
|
||||||
use crate::rpc::client::register_wallet::register_connected_wallet;
|
use crate::rpc::client::register_wallet::register_connected_wallet;
|
||||||
use crate::rpc::client::structs::Connect;
|
use crate::rpc::client::structs::Connect;
|
||||||
use crate::rpc::client::syncing::node_syncing;
|
use crate::rpc::client::syncing::node_syncing;
|
||||||
|
|
@ -42,6 +43,86 @@ use crate::Settings;
|
||||||
use crate::TcpStream;
|
use crate::TcpStream;
|
||||||
use crate::Utc;
|
use crate::Utc;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod stale_setup_reproduction {
|
||||||
|
use super::*;
|
||||||
|
use crate::miner::flag::{begin_chain_sync, is_mining_stop_requested, is_normal_mode, is_syncing_mode, set_mining_state, MiningState};
|
||||||
|
use crate::records::memory::connections::CONNECTIONS;
|
||||||
|
use crate::records::memory::enums::ConnectionType;
|
||||||
|
use crate::records::memory::structs::{ConnectionInfo, ConnectionKey};
|
||||||
|
use crate::rpc::command_maps::{RPC_BLOCK_HEIGHT, RPC_BLOCK_HASH_AT_HEIGHT};
|
||||||
|
use crate::wallets::structures::SavedWallet;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
// Regression for removed setup work taking the chain gate and stopping
|
||||||
|
// mining. No chain data is changed by this test.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn disconnected_queued_setup_exits_without_stopping_mining() {
|
||||||
|
set_mining_state(MiningState::Idle);
|
||||||
|
let owner = begin_chain_sync().await;
|
||||||
|
let db = sled::Config::new().temporary(true).open().unwrap();
|
||||||
|
let map = Arc::new(Mutex::new(Command::new()));
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let client = TcpStream::connect(listener.local_addr().unwrap()).await.unwrap();
|
||||||
|
let (mut peer, _) = listener.accept().await.unwrap();
|
||||||
|
let stream = Arc::new(Mutex::new(client));
|
||||||
|
let key = ConnectionKey { connection_type: ConnectionType::Incoming.as_bytes(), ip: crate::common::binary_conversions::ip_to_binary("127.0.0.1"), port: 50050 };
|
||||||
|
let info = ConnectionInfo::new(key.connection_type.clone(), key.ip.clone(), key.port, stream.clone(), ClientType::Miner.as_bytes(), "test-peer".into());
|
||||||
|
let previous_connections = CONNECTIONS.write().await.replace(Connection { connection_map: std::collections::HashMap::from([(key, info)]) });
|
||||||
|
let wallet = Arc::new(Wallet { saved: SavedWallet { short_address: String::new(), vanity_address: None, public_key: String::new(), private_key: String::new() }, encryption_key: String::new() });
|
||||||
|
let mut setup = Box::pin(sync_incoming_peer_before_operational(stream.clone(), &db, wallet, map.clone(), "127.0.0.1:50050"));
|
||||||
|
let has_genesis = genesis_checkup().await;
|
||||||
|
let responder = async {
|
||||||
|
let mut request = [0u8; 4];
|
||||||
|
peer.read_exact(&mut request).await.unwrap();
|
||||||
|
assert_eq!(request[0], RPC_BLOCK_HEIGHT);
|
||||||
|
let uid = [request[1], request[2], request[3]];
|
||||||
|
let tx = map.lock().await.get(&uid).unwrap().tx.clone();
|
||||||
|
tx.send(0u32.to_le_bytes().to_vec()).await.unwrap();
|
||||||
|
if has_genesis {
|
||||||
|
let mut request = [0u8; 8];
|
||||||
|
peer.read_exact(&mut request).await.unwrap();
|
||||||
|
assert_eq!(request[0], RPC_BLOCK_HASH_AT_HEIGHT);
|
||||||
|
let header = crate::records::unpack_block::unpack_header::load_block_header(0).await.unwrap();
|
||||||
|
let uid = [request[1], request[2], request[3]];
|
||||||
|
let tx = map.lock().await.get(&uid).unwrap().tx.clone();
|
||||||
|
tx.send(crate::decode(header.hash().await).unwrap()).await.unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut setup => panic!("setup exited before waiting for chain access"),
|
||||||
|
_ = responder => {}
|
||||||
|
}
|
||||||
|
// Poll through the initial checks into the occupied chain gate.
|
||||||
|
assert!(tokio::time::timeout(Duration::from_millis(50), &mut setup).await.is_err());
|
||||||
|
remove_stream_from_memory(&stream).await;
|
||||||
|
assert!(CONNECTIONS.read().await.as_ref().unwrap().connection_map.is_empty());
|
||||||
|
stream.lock().await.shutdown().await.unwrap();
|
||||||
|
// Removed work must exit even while another operation owns the gate.
|
||||||
|
let result = tokio::time::timeout(Duration::from_secs(1), &mut setup).await.unwrap();
|
||||||
|
assert!(matches!(result, Err(ref e) if e == "Incoming setup connection was removed while waiting for chain access"));
|
||||||
|
// Exiting stale work must not release the other operation's gate.
|
||||||
|
assert!(is_syncing_mode());
|
||||||
|
assert!(is_mining_stop_requested());
|
||||||
|
owner.finish();
|
||||||
|
assert!(is_normal_mode());
|
||||||
|
assert!(!is_mining_stop_requested());
|
||||||
|
// Reconnecting at the same endpoint cannot make the old task current.
|
||||||
|
let replacement = Arc::new(Mutex::new(TcpStream::connect(listener.local_addr().unwrap()).await.unwrap()));
|
||||||
|
let (_replacement_peer, _) = listener.accept().await.unwrap();
|
||||||
|
let key = ConnectionKey { connection_type: ConnectionType::Incoming.as_bytes(), ip: crate::common::binary_conversions::ip_to_binary("127.0.0.1"), port: 50050 };
|
||||||
|
let info = ConnectionInfo::new(key.connection_type.clone(), key.ip.clone(), key.port, replacement.clone(), ClientType::Miner.as_bytes(), "test-peer".into());
|
||||||
|
CONNECTIONS.write().await.as_mut().unwrap().connection_map.insert(key, info);
|
||||||
|
assert!(begin_connected_peer_sync(&stream).await.is_none());
|
||||||
|
assert!(is_normal_mode());
|
||||||
|
assert!(!is_mining_stop_requested());
|
||||||
|
let valid_guard = begin_connected_peer_sync(&replacement).await.expect("current replacement must still be able to sync");
|
||||||
|
assert!(is_syncing_mode());
|
||||||
|
valid_guard.finish();
|
||||||
|
*CONNECTIONS.write().await = previous_connections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const LIVE_ORPHAN_WINDOW: u32 = 10;
|
const LIVE_ORPHAN_WINDOW: u32 = 10;
|
||||||
|
|
||||||
fn peer_reached_catch_up_target(
|
fn peer_reached_catch_up_target(
|
||||||
|
|
@ -114,7 +195,12 @@ async fn sync_incoming_peer_before_operational(
|
||||||
|
|
||||||
// Wait for any existing startup, live catch-up, or orphan operation to
|
// Wait for any existing startup, live catch-up, or orphan operation to
|
||||||
// finish before this incoming peer can mutate the canonical chain.
|
// finish before this incoming peer can mutate the canonical chain.
|
||||||
let chain_sync_guard = begin_chain_sync().await;
|
let chain_sync_guard = begin_connected_peer_sync(&stream)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| "Incoming setup connection was removed while waiting for chain access".to_string())?;
|
||||||
|
if !incoming_setup_stream_is_current(connections_key, &stream).await {
|
||||||
|
return Err("Incoming setup connection was removed before chain synchronization".to_string());
|
||||||
|
}
|
||||||
let local_height = get_height(db);
|
let local_height = get_height(db);
|
||||||
let remote_height =
|
let remote_height =
|
||||||
request_remote_height(stream.clone(), map.clone(), connections_key.to_string()).await?;
|
request_remote_height(stream.clone(), map.clone(), connections_key.to_string()).await?;
|
||||||
|
|
@ -398,7 +484,7 @@ async fn complete_incoming_miner_setup(
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
wallet: wallet.clone(),
|
wallet: wallet.clone(),
|
||||||
map: map.clone(),
|
map: map.clone(),
|
||||||
first: true,
|
first: !startup_discovery_active(),
|
||||||
};
|
};
|
||||||
if let Err(sponsor_err) = connect_and_handshake(sponsor).await {
|
if let Err(sponsor_err) = connect_and_handshake(sponsor).await {
|
||||||
error!(
|
error!(
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ 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::{
|
||||||
miner_connection_count, operational_peer_wallets, peer_connection_count,
|
operational_peer_wallets, peer_connection_count, sync_capable_miner_connection_count,
|
||||||
ready_outgoing_connection_count, refill_outgoing_connections_once,
|
// ready_outgoing_connection_count, refill_outgoing_connections_once,
|
||||||
};
|
};
|
||||||
use crate::records::memory::network_mapping::NodeInfo;
|
use crate::records::memory::network_mapping::NodeInfo;
|
||||||
use crate::records::memory::response_channels::Command;
|
use crate::records::memory::response_channels::Command;
|
||||||
use crate::rpc::client::handshake::connect_and_handshake;
|
use crate::rpc::client::handshake::connect_and_handshake;
|
||||||
use crate::rpc::client::handshake::outbound_handshake_in_progress;
|
use crate::rpc::client::handshake::outbound_handshake_in_progress;
|
||||||
|
use crate::rpc::client::handshake_processing::startup_discovery_active;
|
||||||
use crate::rpc::client::structs::Connect;
|
use crate::rpc::client::structs::Connect;
|
||||||
use crate::sled::Db;
|
use crate::sled::Db;
|
||||||
use crate::sleep;
|
use crate::sleep;
|
||||||
|
|
@ -41,11 +42,6 @@ pub async fn handle_connections(
|
||||||
wallet: Arc<Wallet>,
|
wallet: Arc<Wallet>,
|
||||||
map: Arc<Mutex<Command>>,
|
map: Arc<Mutex<Command>>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Reciprocal sponsorship is only the bootstrap rule for the first two
|
|
||||||
// nodes that are establishing a chain. A node restarting an existing
|
|
||||||
// chain may connect to any valid operational peer.
|
|
||||||
let first_two_node_startup = !genesis_checkup().await;
|
|
||||||
|
|
||||||
// A zero outgoing limit means this node waits for an incoming sponsor. It
|
// A zero outgoing limit means this node waits for an incoming sponsor. It
|
||||||
// does not bypass the independent-peer requirement or permit solo mining.
|
// does not bypass the independent-peer requirement or permit solo mining.
|
||||||
let outgoing_connections = crate::Settings::load()
|
let outgoing_connections = crate::Settings::load()
|
||||||
|
|
@ -68,6 +64,9 @@ pub async fn handle_connections(
|
||||||
info!("No existing network peer is available. Waiting for connections.");
|
info!("No existing network peer is available. Waiting for connections.");
|
||||||
let mut retry_seconds = 0_u64;
|
let mut retry_seconds = 0_u64;
|
||||||
loop {
|
loop {
|
||||||
|
// A reset node also starts without local genesis, but it stops being a
|
||||||
|
// first-two-node bootstrap as soon as synchronization imports genesis.
|
||||||
|
let first_two_node_startup = !genesis_checkup().await;
|
||||||
let peer_wallets = operational_peer_wallets().await;
|
let peer_wallets = operational_peer_wallets().await;
|
||||||
let sponsorship_ready = if first_two_node_startup {
|
let sponsorship_ready = if first_two_node_startup {
|
||||||
NodeInfo::has_reciprocal_sponsorship(&wallet.saved.short_address, &peer_wallets).await
|
NodeInfo::has_reciprocal_sponsorship(&wallet.saved.short_address, &peer_wallets).await
|
||||||
|
|
@ -105,18 +104,15 @@ async fn attempt_bootstrap_connections(
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
// Try the configured bootstrap peers one by one until a
|
// Try the configured bootstrap peers one by one until a
|
||||||
// handshake succeeds or the list is exhausted.
|
// handshake succeeds or the list is exhausted.
|
||||||
let mut filtered_servers = get_node_connections().await;
|
let configured = get_node_connections().await;
|
||||||
if context != "startup" {
|
let mapped = NodeInfo::active_node_endpoints().await;
|
||||||
// During a live outage the synchronized in-memory mapping is still
|
let remembered =
|
||||||
// available. Use its active endpoints as recovery candidates so the
|
crate::records::peer_endpoints::remembered_peer_endpoints(&db).unwrap_or_else(|err| {
|
||||||
// configured bootstrap host is not a runtime single point of failure.
|
warn!("[reconnect] could not read cached peer endpoints: {err}");
|
||||||
for endpoint in NodeInfo::active_node_endpoints().await {
|
Vec::new()
|
||||||
if !filtered_servers.contains(&endpoint) {
|
});
|
||||||
filtered_servers.push(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let (_, _, local_endpoint) = get_ip_and_port().await;
|
let (_, _, local_endpoint) = get_ip_and_port().await;
|
||||||
|
let filtered_servers = recovery_candidates(configured, mapped, remembered, &local_endpoint);
|
||||||
let mut last_error: Option<String> = None;
|
let mut last_error: Option<String> = None;
|
||||||
|
|
||||||
for server in filtered_servers {
|
for server in filtered_servers {
|
||||||
|
|
@ -136,7 +132,9 @@ async fn attempt_bootstrap_connections(
|
||||||
// Clone the Arc for use in other async functions
|
// Clone the Arc for use in other async functions
|
||||||
let map_clone = Arc::clone(&map);
|
let map_clone = Arc::clone(&map);
|
||||||
|
|
||||||
let first: bool = true;
|
// An existing canonical startup owner will adopt this synchronized
|
||||||
|
// replacement stream. Do not queue a second full startup sync.
|
||||||
|
let first = !startup_discovery_active();
|
||||||
let connect_params = Connect {
|
let connect_params = Connect {
|
||||||
addr: socket_address,
|
addr: socket_address,
|
||||||
db: db_clone,
|
db: db_clone,
|
||||||
|
|
@ -200,6 +198,21 @@ async fn attempt_bootstrap_connections(
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn recovery_candidates(
|
||||||
|
configured: Vec<String>,
|
||||||
|
mapped: Vec<String>,
|
||||||
|
remembered: Vec<String>,
|
||||||
|
local_endpoint: &str,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
for endpoint in configured.into_iter().chain(mapped).chain(remembered) {
|
||||||
|
if endpoint != local_endpoint && !candidates.contains(&endpoint) {
|
||||||
|
candidates.push(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
pub fn spawn_outage_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<Mutex<Command>>) {
|
pub fn spawn_outage_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<Mutex<Command>>) {
|
||||||
let Some(recovery_guard) = try_start_outage_recovery() else {
|
let Some(recovery_guard) = try_start_outage_recovery() else {
|
||||||
return;
|
return;
|
||||||
|
|
@ -209,9 +222,9 @@ pub fn spawn_outage_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<Mutex<Command
|
||||||
let _recovery_guard = recovery_guard;
|
let _recovery_guard = recovery_guard;
|
||||||
|
|
||||||
// A raw socket that never completed setup is not network recovery.
|
// A raw socket that never completed setup is not network recovery.
|
||||||
// Only an operational miner peer should suppress configured-peer
|
// Only a ready peer or one that completed registry and map sync can
|
||||||
// outage recovery.
|
// suppress configured-peer outage recovery.
|
||||||
if outbound_handshake_in_progress() || miner_connection_count().await != 0 {
|
if outbound_handshake_in_progress() || sync_capable_miner_connection_count().await != 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,9 +261,12 @@ pub fn spawn_isolated_bootstrap_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<M
|
||||||
// A topology pass is bounded: it walks the current active map once,
|
// A topology pass is bounded: it walks the current active map once,
|
||||||
// tries each eligible endpoint at most once, then sleeps until the
|
// tries each eligible endpoint at most once, then sleeps until the
|
||||||
// next interval even when the configured limit exceeds network size.
|
// next interval even when the configured limit exceeds network size.
|
||||||
if ready_outgoing_connection_count().await < outgoing_connections as usize {
|
// Temporarily disabled with direct dropped-peer retries: while
|
||||||
refill_outgoing_connections_once().await;
|
// connected, do not rediscover and redial a peer that went away.
|
||||||
}
|
// Startup discovery and isolated recovery above remain enabled.
|
||||||
|
// if ready_outgoing_connection_count().await < outgoing_connections as usize {
|
||||||
|
// refill_outgoing_connections_once().await;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -259,6 +275,21 @@ pub fn spawn_isolated_bootstrap_recovery(db: Db, wallet: Arc<Wallet>, map: Arc<M
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restart_recovery_retains_cached_fallback_after_configured_peers() {
|
||||||
|
let peers = recovery_candidates(
|
||||||
|
vec!["8.8.8.8:50050".into()],
|
||||||
|
Vec::new(),
|
||||||
|
vec![
|
||||||
|
"8.8.8.8:50050".into(),
|
||||||
|
"1.1.1.1:50050".into(),
|
||||||
|
"9.9.9.9:50050".into(),
|
||||||
|
],
|
||||||
|
"9.9.9.9:50050",
|
||||||
|
);
|
||||||
|
assert_eq!(peers, vec!["8.8.8.8:50050", "1.1.1.1:50050"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn outage_recovery_has_one_owner() {
|
fn outage_recovery_has_one_owner() {
|
||||||
OUTAGE_RECOVERY_ACTIVE.store(false, Ordering::SeqCst);
|
OUTAGE_RECOVERY_ACTIVE.store(false, Ordering::SeqCst);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ use crate::miner::flag::{
|
||||||
request_mining_stop, set_mining_state, set_node_mode, MiningState, NodeMode,
|
request_mining_stop, set_mining_state, set_node_mode, MiningState, NodeMode,
|
||||||
};
|
};
|
||||||
use crate::records::memory::connections::initialize_connection;
|
use crate::records::memory::connections::initialize_connection;
|
||||||
use crate::records::memory::network_mapping::NodeInfo;
|
|
||||||
use crate::wallets::structures::Wallet;
|
use crate::wallets::structures::Wallet;
|
||||||
use crate::Path;
|
use crate::Path;
|
||||||
|
|
||||||
|
|
@ -69,7 +68,6 @@ pub async fn prepare_pre_wallet_startup() {
|
||||||
set_node_mode(NodeMode::Startup);
|
set_node_mode(NodeMode::Startup);
|
||||||
request_mining_stop();
|
request_mining_stop();
|
||||||
set_mining_state(MiningState::Idle);
|
set_mining_state(MiningState::Idle);
|
||||||
NodeInfo::initialize_runtime_start();
|
|
||||||
initialize_connection().await;
|
initialize_connection().await;
|
||||||
create_file_paths().await;
|
create_file_paths().await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -515,6 +515,14 @@ async fn get_network_mapping_response(
|
||||||
NodeInfo::validate_synced_monitor_state(monitor_state, db, &expected_targets).await?;
|
NodeInfo::validate_synced_monitor_state(monitor_state, db, &expected_targets).await?;
|
||||||
NodeInfo::install_synced_snapshot(db, snapshot, validated_monitor_state).await?;
|
NodeInfo::install_synced_snapshot(db, snapshot, validated_monitor_state).await?;
|
||||||
NodeInfo::restore_or_rebuild_mined_counts(db).await?;
|
NodeInfo::restore_or_rebuild_mined_counts(db).await?;
|
||||||
|
// The snapshot has been validated. Persist only its active dial hints;
|
||||||
|
// startup still requires the complete normal handshake and join exchange.
|
||||||
|
if let Err(err) = crate::records::peer_endpoints::remember_peer_endpoints(
|
||||||
|
db,
|
||||||
|
&NodeInfo::active_node_endpoints().await,
|
||||||
|
) {
|
||||||
|
warn!("[network_map] could not cache discovered peer endpoints: {err}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ use crate::Arc;
|
||||||
use crate::Duration;
|
use crate::Duration;
|
||||||
use crate::Mutex;
|
use crate::Mutex;
|
||||||
|
|
||||||
|
fn first_peer_index(block_number: u32, peer_count: usize) -> usize {
|
||||||
|
block_number as usize % peer_count
|
||||||
|
}
|
||||||
|
|
||||||
fn expected_piece_hash(
|
fn expected_piece_hash(
|
||||||
torrent: &crate::torrent::structs::Torrent,
|
torrent: &crate::torrent::structs::Torrent,
|
||||||
piece: u8,
|
piece: u8,
|
||||||
|
|
@ -233,8 +237,12 @@ pub async fn download_block_pieces(params: DownloadSave) -> Result<(), String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut reserved_any = false;
|
let mut reserved_any = false;
|
||||||
|
let peer_count = connected_nodes.len();
|
||||||
|
let first_peer = first_peer_index(params.block_number, peer_count);
|
||||||
for piece in &pieces {
|
for piece in &pieces {
|
||||||
for (connections_key, stream) in &connected_nodes {
|
for peer_offset in 0..peer_count {
|
||||||
|
let peer_index = (first_peer + peer_offset) % peer_count;
|
||||||
|
let (connections_key, stream) = &connected_nodes[peer_index];
|
||||||
// Use the full connection key for scheduling and failure
|
// Use the full connection key for scheduling and failure
|
||||||
// tracking so localhost peers on different ports remain distinct.
|
// tracking so localhost peers on different ports remain distinct.
|
||||||
let peer_key = connections_key.to_string();
|
let peer_key = connections_key.to_string();
|
||||||
|
|
@ -327,3 +335,15 @@ pub async fn download_block_pieces(params: DownloadSave) -> Result<(), String> {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::first_peer_index;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn consecutive_blocks_rotate_across_available_peers() {
|
||||||
|
assert_eq!(first_peer_index(100, 3), 1);
|
||||||
|
assert_eq!(first_peer_index(101, 3), 2);
|
||||||
|
assert_eq!(first_peer_index(102, 3), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::common::binary_conversions::binary_to_ip;
|
use crate::common::binary_conversions::binary_to_ip;
|
||||||
use crate::records::memory::connections::CONNECTIONS;
|
use crate::records::memory::connections::CONNECTIONS;
|
||||||
use crate::records::memory::enums::ClientType;
|
use crate::records::memory::enums::ClientType;
|
||||||
|
use crate::records::memory::structs::ConnectionHealth;
|
||||||
use crate::Arc;
|
use crate::Arc;
|
||||||
use crate::Mutex;
|
use crate::Mutex;
|
||||||
use crate::TcpStream;
|
use crate::TcpStream;
|
||||||
|
|
@ -18,6 +19,12 @@ pub async fn get_nodes_from_memory() -> Vec<(String, Arc<Mutex<TcpStream>>)> {
|
||||||
if !connection_info.ready {
|
if !connection_info.ready {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !matches!(
|
||||||
|
connection_info.health,
|
||||||
|
ConnectionHealth::Connected | ConnectionHealth::Busy
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Use ip:port as the scheduler key and clone the shared stream handle for requests.
|
// Use ip:port as the scheduler key and clone the shared stream handle for requests.
|
||||||
let ip = binary_to_ip(connection_info.ip.clone());
|
let ip = binary_to_ip(connection_info.ip.clone());
|
||||||
let port = connection_info.port;
|
let port = connection_info.port;
|
||||||
|
|
@ -46,6 +53,12 @@ pub async fn get_sync_nodes_from_memory() -> Vec<(String, Arc<Mutex<TcpStream>>)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !matches!(
|
||||||
|
connection_info.health,
|
||||||
|
ConnectionHealth::Connected | ConnectionHealth::Busy
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let ip = binary_to_ip(connection_info.ip.clone());
|
let ip = binary_to_ip(connection_info.ip.clone());
|
||||||
let port = connection_info.port;
|
let port = connection_info.port;
|
||||||
let key = format!("{ip}:{port}");
|
let key = format!("{ip}:{port}");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue