disconnection fix

This commit is contained in:
contractless 2026-09-12 10:05:41 -06:00
parent 291fbd5755
commit ca24d25f68
6 changed files with 98 additions and 24 deletions

View File

@ -1060,6 +1060,23 @@ impl NodeInfo {
if !candidate_lost_collision { if !candidate_lost_collision {
if let Some(existing_node) = address_map.get_mut(&edit.address) { if let Some(existing_node) = address_map.get_mut(&edit.address) {
if existing_node.deleted_timestamp > 0 { if existing_node.deleted_timestamp > 0 {
// A revived membership remains pending with its own add
// timestamp as the deletion marker until monitor activation.
// Redundant delivery of that exact signed record is already
// represented locally; acknowledge it without reviving it or
// changing any mapping state. Actual later deletions and
// different records must still pass the ordering check.
if existing_node.deleted_timestamp == existing_node.added_timestamp
&& existing_node.deleted_block == 0
&& existing_node.monitoring.is_empty()
&& existing_node.ip == edit.ip
&& 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
{
return RpcResponse::Binary(b"Success".to_vec());
}
if edit.modified_timestamp <= existing_node.deleted_timestamp { if edit.modified_timestamp <= existing_node.deleted_timestamp {
return RpcResponse::Binary( return RpcResponse::Binary(
b"Error: Reconnection record did not follow deletion".to_vec(), b"Error: Reconnection record did not follow deletion".to_vec(),
@ -1140,6 +1157,70 @@ impl NodeInfo {
mod tests { mod tests {
use super::*; use super::*;
#[tokio::test]
async fn repeated_signed_rejoin_preserves_pending_membership() {
use crate::wallets::structures::SavedWallet;
let db = sled::Config::new().temporary(true).open().unwrap();
let (public, private) = Wallet::generate_keypair();
let address_bytes = Wallet::public_key_bytes_to_short_address_bytes(&public).unwrap();
let address = Wallet::bytes_to_short_address(&address_bytes).unwrap();
crate::records::wallet_registry::register_short_address(&db, &address_bytes, &public).unwrap();
let wallet = Arc::new(Wallet {
saved: SavedWallet {
short_address: address.clone(), vanity_address: None,
public_key: crate::encode(public), private_key: private,
},
encryption_key: String::new(),
});
let mut old = NodeInfo::new("8.8.8.8".into(), 50050, 0, address.clone(), 50, String::new());
old.deleted_timestamp = 100;
old.deleted_block = 10;
let previous = ADDRESS_MAP.lock().await.insert(address.clone(), old);
let edit = SignedNodeEdit {
address: address.clone(), ip: "8.8.8.8".into(), port: 50050,
modified_by: address.clone(), modified_timestamp: 200,
modified_signature: NodeInfo::added_signature(&address, "8.8.8.8", 50050, 200, &wallet).await,
};
let params = || AddAddressParams {
map: Arc::new(Mutex::new(Command::new())), edit: edit.clone(),
monitors: Vec::new(), blocks_mined: 0, remote_ip: String::new(),
db: db.clone(), wallet: wallet.clone(), connections_key: "8.8.4.4:50050".into(),
};
let RpcResponse::Binary(first) = NodeInfo::add_address_now(params()).await;
let RpcResponse::Binary(repeated) = NodeInfo::add_address_now(params()).await;
let mut different = params();
different.edit.port = 50051;
different.edit.modified_signature = NodeInfo::added_signature(
&address, "8.8.8.8", 50051, 200, &wallet,
).await;
let RpcResponse::Binary(conflicting) = NodeInfo::add_address_now(different).await;
let mut mapping = ADDRESS_MAP.lock().await;
let actual = mapping.remove(&address).unwrap();
let mut deleted = NodeInfo::new(
edit.ip.clone(), edit.port, 0, edit.modified_by.clone(),
edit.modified_timestamp, edit.modified_signature.clone(),
);
deleted.deleted_timestamp = 250;
deleted.deleted_block = 20;
mapping.insert(address.clone(), deleted);
drop(mapping);
let RpcResponse::Binary(stale) = NodeInfo::add_address_now(params()).await;
let mut mapping = ADDRESS_MAP.lock().await;
let after_stale = mapping.remove(&address).unwrap();
if let Some(previous) = previous { mapping.insert(address, previous); }
drop(mapping);
assert_eq!(String::from_utf8(first).unwrap(), "Success");
assert_eq!(String::from_utf8(repeated).unwrap(), "Success");
assert_eq!(String::from_utf8(conflicting).unwrap(), "Error: Reconnection record did not follow deletion");
assert_eq!(String::from_utf8(stale).unwrap(), "Error: Reconnection record did not follow deletion");
assert_eq!(after_stale.deleted_timestamp, 250);
assert_eq!(after_stale.deleted_block, 20);
assert_eq!(actual.added_timestamp, 200);
assert_eq!(actual.deleted_timestamp, 200);
assert_eq!(actual.deleted_block, 0);
assert!(actual.monitoring.is_empty());
}
#[test] #[test]
fn full_admission_window_allows_known_wallet_but_not_a_new_wallet_on_same_ip() { fn full_admission_window_allows_known_wallet_but_not_a_new_wallet_on_same_ip() {
let mut map = HashMap::new(); let mut map = HashMap::new();

View File

@ -11,7 +11,7 @@ use crate::{timeout, AsyncReadExt, AsyncWriteExt, Duration};
const STREAM_READ_TIMEOUT_SECONDS: u64 = 30; const STREAM_READ_TIMEOUT_SECONDS: u64 = 30;
async fn read_exact_from_stream( async fn read_exact_from_stream(
_key: &str, key: &str,
stream_locked: &Arc<Mutex<TcpStream>>, stream_locked: &Arc<Mutex<TcpStream>>,
stream: &mut TcpStream, stream: &mut TcpStream,
buffer: &mut [u8], buffer: &mut [u8],
@ -26,6 +26,7 @@ async fn read_exact_from_stream(
{ {
Ok(Ok(_)) => Ok(()), Ok(Ok(_)) => Ok(()),
Ok(Err(err)) => { Ok(Err(err)) => {
warn!("[rpc] removing connection after payload read failure: peer={key} requested_bytes={} error={err}", buffer.len());
if let Err(e) = stream.shutdown().await { if let Err(e) = stream.shutdown().await {
warn!("Error shutting down stream: {e}"); warn!("Error shutting down stream: {e}");
} }
@ -33,6 +34,7 @@ async fn read_exact_from_stream(
Err(err.to_string()) Err(err.to_string())
} }
Err(_) => { Err(_) => {
warn!("[rpc] removing connection after payload read timeout: peer={key} requested_bytes={} timeout_seconds={STREAM_READ_TIMEOUT_SECONDS}", buffer.len());
if let Err(e) = stream.shutdown().await { if let Err(e) = stream.shutdown().await {
warn!("Error shutting down stream: {e}"); warn!("Error shutting down stream: {e}");
} }

View File

@ -10,7 +10,6 @@ use crate::Duration;
use crate::Mutex; use crate::Mutex;
use crate::Serialize; use crate::Serialize;
use crate::TcpStream; use crate::TcpStream;
use std::io::ErrorKind;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use tokio::task::JoinSet; use tokio::task::JoinSet;
@ -23,17 +22,6 @@ pub enum RpcResponse {
} }
impl RpcResponse { impl RpcResponse {
fn is_expected_disconnect(err: &std::io::Error) -> bool {
matches!(
err.kind(),
ErrorKind::BrokenPipe
| ErrorKind::ConnectionReset
| ErrorKind::ConnectionAborted
| ErrorKind::NotConnected
| ErrorKind::UnexpectedEof
)
}
async fn write_bytes( async fn write_bytes(
stream: &Arc<Mutex<TcpStream>>, stream: &Arc<Mutex<TcpStream>>,
connections_key: Option<&str>, connections_key: Option<&str>,
@ -62,9 +50,7 @@ impl RpcResponse {
true true
} }
Ok(Err(err)) => { Ok(Err(err)) => {
if !Self::is_expected_disconnect(&err) { warn!("[rpc] removing connection after socket write failure: peer={peer} socket_peer={:?} error={err:?}", stream_guard.peer_addr());
warn!("Error sending binary response to {peer}: {err:?}");
}
let _ = stream_guard.shutdown().await; let _ = stream_guard.shutdown().await;
drop(stream_guard); drop(stream_guard);
set_stream_health(stream, ConnectionHealth::Closed).await; set_stream_health(stream, ConnectionHealth::Closed).await;

View File

@ -35,6 +35,7 @@ async fn read_next_command_byte(
match stream.try_read(&mut buffer) { match stream.try_read(&mut buffer) {
Ok(1) => return Ok(Some(buffer[0])), Ok(1) => return Ok(Some(buffer[0])),
Ok(0) => { Ok(0) => {
warn!("[rpc] command stream reached EOF: socket_peer={:?}", stream.peer_addr());
drop(stream); drop(stream);
remove_stream_from_memory(stream_locked).await; remove_stream_from_memory(stream_locked).await;
return Ok(None); return Ok(None);
@ -45,7 +46,7 @@ async fn read_next_command_byte(
sleep(timeout_duration).await; sleep(timeout_duration).await;
} }
Err(err) => { Err(err) => {
warn!("Dropped stream: {err:?}"); warn!("[rpc] command read failed: socket_peer={:?} error={err:?}", stream.peer_addr());
drop(stream); drop(stream);
remove_stream_from_memory(stream_locked).await; remove_stream_from_memory(stream_locked).await;
return Ok(None); return Ok(None);

View File

@ -182,13 +182,13 @@ pub async fn combine_and_send_data(params: CombineAndSendDataParams) -> Result<(
} }
// start the rpc loop // start the rpc loop
tokio::spawn(start_loop( let loop_stream = stream.clone();
stream.clone(), let loop_key = connections_key.to_string();
db, tokio::spawn(async move {
connections_key.to_string(), if let Err(err) = start_loop(loop_stream, db, loop_key.clone(), wallet, map).await {
wallet, error!("[rpc] command loop exited with error: peer={loop_key} error={err}");
map, }
)); });
Ok(()) Ok(())
} }

View File

@ -555,6 +555,10 @@ pub async fn start_loop(
.await?; .await?;
let should_drop_rejected_miner = client_type == ClientType::Miner let should_drop_rejected_miner = client_type == ClientType::Miner
&& matches!(&result, responses::RpcResponse::Binary(bytes) if String::from_utf8_lossy(bytes).starts_with("Error:")); && matches!(&result, responses::RpcResponse::Binary(bytes) if String::from_utf8_lossy(bytes).starts_with("Error:"));
if should_drop_rejected_miner {
let responses::RpcResponse::Binary(bytes) = &result;
crate::log::warn!("[rpc] removing miner after membership announcement rejection: peer={} reason={}", connections_key, String::from_utf8_lossy(bytes));
}
result result
.send(&stream_locked, Some(&connections_key), uid) .send(&stream_locked, Some(&connections_key), uid)
.await; .await;