restartup fixes on lost internet connection

This commit is contained in:
viraladmin 2026-07-01 09:18:12 -06:00
parent 12916c0b11
commit 650c079288
8 changed files with 198 additions and 67 deletions

View File

@ -6,6 +6,7 @@ For the protocol design, mining rules, balance-sheet model and orphan correction
## Table of Contents
- [System Requirements](#system-requirements)
- [Network Requirements](#network-requirements)
- [Build Instructions](#build-instructions)
- [PostgreSQL Setup](#postgresql-setup)
@ -14,6 +15,13 @@ For the protocol design, mining rules, balance-sheet model and orphan correction
- [Startup](#startup)
- [Additional Documentation](#additional-documentation)
## System Requirements:
- Dedicated Internet
- PC / Laptop / Hosted VPS or cloud hostingg
- Minimum 16GB memory 24 - 32 GB recommended
- i5 or higher CPU (or AMD equivalent CPU)
## Network Requirements
A public node needs to be reachable by other peers.
@ -26,6 +34,7 @@ A public node needs to be reachable by other peers.
Loopback and private addresses are useful for local testing, but they should not be announced by a public node.
## Build Instructions
Install Rust and build from the repository root.
@ -84,7 +93,7 @@ The Windows installer downloads and installs PostgreSQL when needed, then create
### Manual PostgreSQL Setup
Manual PostgreSQL instructions should live in [docs/POSTGRES.md](docs/POSTGRES.md). Until that file is fully written, use the CLI installer unless you already know how to create the database, user and permissions manually.
Manual PostgreSQL instructions should live in [docs/POSTGRES.md](src/branch/main/docs/POSTGRES.md). Until that file is fully written, use the CLI installer unless you already know how to create the database, user and permissions manually.
## Settings Configuration
@ -158,7 +167,7 @@ dbname = contractless_db
- `error` shows only error logs.
- `off` disables log output.
Detailed settings notes should live in [docs/SETTINGS.md](docs/SETTINGS.md).
Detailed settings notes should live in [docs/SETTINGS.md](src/branch/main/docs/SETTINGS.md).
## Runtime Paths
@ -303,8 +312,8 @@ Node flags:
## Additional Documentation
- [Whitepaper](https://contractless.community/contractless_whitepaper.pdf)
- [Manual PostgreSQL setup](docs/POSTGRES.md)
- [Settings reference](docs/SETTINGS.md)
- [CLI tools reference](docs/CLI_TOOLS.md)
- [Transaction reference](docs/TRANSACTIONS.md)
- [Developer guide](docs/DEV.md)
- [Manual PostgreSQL setup](src/branch/main/docs/POSTGRES.md)
- [Settings reference](src/branch/main/docs/SETTINGS.md)
- [CLI tools reference](src/branch/main/docs/CLI_TOOLS.md)
- [Transaction reference](src/branch/main/docs/TRANSACTIONS.md)
- [Developer guide](src/branch/main/docs/DEV.md)

View File

@ -108,7 +108,7 @@ Typical uses:
- inspect transaction fields
- apply or undo balance-sheet effects
For user-facing transaction behavior, see [TRANSACTIONS.md](TRANSACTIONS.md).
For user-facing transaction behavior, see [TRANSACTIONS.md](src/branch/main/docs/TRANSACTIONS.md).
### Common Hashing and Types
@ -148,7 +148,7 @@ Typical uses:
- inspect client request helpers
- inspect response structures
For raw client handshake notes, see [DEV_CLIENTS.md](DEV_CLIENTS.md).
For raw client handshake notes, see [DEV_CLIENTS.md](src/branch/main/docs/DEV_CLIENTS.md).
### Standalone Tool Connections
@ -225,7 +225,7 @@ When adding a tool:
- use `standalone_tools::connections` for RPC client requests
- keep RPC payloads binary across the TCP stream
- print human-readable or JSON-decoded output only after the binary response is received
- add the tool to [CLI_TOOLS.md](CLI_TOOLS.md)
- add the tool to [CLI_TOOLS.md](src/branch/main/docs/CLI_TOOLS.md)
## Adding New RPC Commands
@ -242,9 +242,9 @@ When adding a command:
## Documentation Map
- [README.md](../README.md): build, configure and start a node
- [CLI_TOOLS.md](CLI_TOOLS.md): command-line tool reference
- [TRANSACTIONS.md](TRANSACTIONS.md): transaction behavior reference
- [POSTGRES.md](POSTGRES.md): manual PostgreSQL setup
- [SETTINGS.md](SETTINGS.md): `settings.ini` field reference
- [DEV_CLIENTS.md](DEV_CLIENTS.md): low-level client handshake and request notes
- [README.md](src/branch/main/README.md): build, configure and start a node
- [CLI_TOOLS.md](src/branch/main/docs/CLI_TOOLS.md): command-line tool reference
- [TRANSACTIONS.md](src/branch/main/docs/TRANSACTIONS.md): transaction behavior reference
- [POSTGRES.md](src/branch/main/docs/POSTGRES.md): manual PostgreSQL setup
- [SETTINGS.md](src/branch/main/docs/SETTINGS.md): `settings.ini` field reference
- [DEV_CLIENTS.md](src/branch/main/docs/DEV_CLIENTS.md): low-level client handshake and request notes

View File

@ -1,5 +1,6 @@
use blockchain::common::cli_prompts::prompt_visible;
use blockchain::env;
use blockchain::standalone_tools::vanity_resolver::canonical_vanity_address_input;
use blockchain::wallets::structures::Wallet;
#[tokio::main]
@ -21,5 +22,9 @@ async fn main() {
.to_string()
};
println!("{}", Wallet::short_address_validation(&address));
let is_valid = Wallet::short_address_validation(&address)
|| canonical_vanity_address_input(&address)
.is_some_and(|vanity| Wallet::short_address_validation(&vanity));
println!("{is_valid}");
}

View File

@ -537,6 +537,13 @@ impl Connection {
.count()
}
pub fn count_miner_connections(&self) -> usize {
self.connection_map
.values()
.filter(|info| ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner))
.count()
}
// Return ready peer streams so broadcast-style paths do not send
// network-wide traffic to peers still completing startup sync.
pub fn get_all_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> {
@ -860,6 +867,17 @@ pub async fn peer_connection_count() -> usize {
.unwrap_or(0)
}
pub async fn miner_connection_count() -> usize {
// Recovery logic uses raw miner sockets to avoid starting another
// bootstrap attempt while an accepted peer is still finishing setup.
CONNECTIONS
.read()
.await
.as_ref()
.map(|connection| connection.count_miner_connections())
.unwrap_or(0)
}
pub async fn mark_peer_wallet_registry_synced(key: &str) -> bool {
CONNECTIONS
.write()

View File

@ -14,6 +14,7 @@ use crate::rpc::command_maps::{
RPC_WALLET_REGISTRY_SYNC,
};
use crate::standalone_tools::transaction_creator::create_transaction_request;
use crate::standalone_tools::vanity_resolver::canonical_vanity_address_input;
use crate::timeout;
use crate::wallets::structures::Wallet;
use crate::Duration;
@ -553,7 +554,7 @@ async fn build_request_bytes(
// Lookup canonical short owner address by registered vanity address.
45 => {
let command_number: u8 = RPC_VANITY_OWNER_LOOKUP;
let address_bytes = Wallet::normalize_to_short_address(&command_input)
let address_bytes = canonical_vanity_address_input(&command_input)
.and_then(|vanity| Wallet::vanity_address_to_bytes(&vanity))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid vanity address")

View File

@ -5,22 +5,45 @@ use crate::rpc::command_maps::RPC_VANITY_OWNER_LOOKUP;
use crate::standalone_tools::connections::handshake::{self, HandshakeWallet};
use crate::wallets::structures::Wallet;
pub fn canonical_vanity_address_input(address: &str) -> Option<String> {
let trimmed = address.trim();
let (payload, network_suffix) = trimmed.rsplit_once('.')?;
let network_suffix = network_suffix.trim().to_ascii_lowercase();
if Wallet::map_wallet_to_byte(&network_suffix) == 0 {
return None;
}
let payload = payload.trim();
if payload.is_empty() || payload.len() > Wallet::SHORT_ADDRESS_HASH_BYTES_LENGTH {
return None;
}
if !payload.chars().all(|ch| ch.is_ascii_alphabetic()) {
return None;
}
Some(format!(
"{:>width$}.{network_suffix}",
payload.to_ascii_lowercase(),
width = Wallet::SHORT_ADDRESS_HASH_BYTES_LENGTH
))
}
pub async fn resolve_wallet_address_input(
address: &str,
wallet: &Wallet,
) -> Result<String, String> {
let normalized = Wallet::normalize_to_short_address(address.trim())
.ok_or_else(|| "Invalid wallet address.".to_string())?;
let trimmed = address.trim();
if let Some(normalized) = Wallet::normalize_to_short_address(trimmed) {
if Wallet::short_address_to_bytes(&normalized).is_some() {
return Ok(normalized);
}
if Wallet::vanity_address_to_bytes(&normalized).is_none() {
return Err("Invalid wallet address.".to_string());
}
lookup_vanity_owner(&normalized, wallet).await
let canonical_vanity = canonical_vanity_address_input(trimmed)
.ok_or_else(|| "Invalid wallet address.".to_string())?;
lookup_vanity_owner(&canonical_vanity, wallet).await
}
async fn lookup_vanity_owner(vanity_address: &str, wallet: &Wallet) -> Result<String, String> {

View File

@ -1,8 +1,10 @@
use crate::common::network_startup::get_connections;
use crate::log::{error, info};
use crate::log::{error, info, warn};
use crate::miner::flag::{
clear_mining_stop_request, set_mining_state, set_node_mode, MiningState, NodeMode,
clear_mining_stop_request, is_normal_mode, set_mining_state, set_node_mode, MiningState,
NodeMode,
};
use crate::records::memory::connections::{miner_connection_count, peer_connection_count};
use crate::records::memory::response_channels::Command;
use crate::rpc::client::handshake::connect_and_handshake;
use crate::rpc::client::structs::Connect;
@ -31,8 +33,26 @@ pub async fn handle_connections(
return Ok(());
}
// try the configured bootstrap peers one by one until a
// handshake succeeds or the list is exhausted
let connected = attempt_bootstrap_connections(db, wallet, map, "startup").await?;
if connected {
return Ok(());
}
// Startup can continue as a standalone node even if no bootstrap peer is reachable.
set_node_mode(NodeMode::Normal);
clear_mining_stop_request();
set_mining_state(MiningState::Idle);
Ok(())
}
async fn attempt_bootstrap_connections(
db: Db,
wallet: Arc<Wallet>,
map: Arc<Mutex<Command>>,
context: &str,
) -> Result<bool, String> {
// Try the configured bootstrap peers one by one until a
// handshake succeeds or the list is exhausted.
let filtered_servers = get_connections().await;
let mut last_error: Option<String> = None;
@ -60,8 +80,12 @@ pub async fn handle_connections(
let err_string = match connect_and_handshake(connect_params).await {
Ok(()) => {
if context == "startup" {
info!("Connected to {server}");
return Ok(());
} else {
info!("[reconnect] bootstrap recovery connected to {server}");
}
return Ok(true);
}
Err(err) => err.to_string(),
};
@ -71,19 +95,69 @@ pub async fn handle_connections(
if err_string.contains("The connection is already in the connection manager Please wait 10 minutes and try again") {
return Err(err_string);
}
if context == "startup" {
error!("Error connecting to {server}: {err_string}");
} else {
warn!("[reconnect] bootstrap recovery failed to connect to {server}: {err_string}");
}
last_error = Some(err_string.clone());
sleep(Duration::from_secs(5)).await;
}
if let Some(err) = last_error {
if context == "startup" {
info!("No bootstrap peers connected during startup: {err}");
} else {
info!("No bootstrap peers connected during startup.");
warn!("[reconnect] bootstrap recovery found no peers: {err}");
}
// Startup can continue as a standalone node even if no bootstrap peer is reachable.
set_node_mode(NodeMode::Normal);
clear_mining_stop_request();
set_mining_state(MiningState::Idle);
Ok(())
} else {
if context == "startup" {
info!("No bootstrap peers connected during startup.");
} else {
warn!("[reconnect] bootstrap recovery has no configured peers to try");
}
}
Ok(false)
}
pub fn spawn_isolated_bootstrap_recovery(
db: Db,
wallet: Arc<Wallet>,
map: Arc<Mutex<Command>>,
) {
let outgoing_connections = crate::Settings::load()
.map(|settings| settings.outgoing_connections)
.unwrap_or(0);
if outgoing_connections == 0 {
return;
}
tokio::spawn(async move {
loop {
sleep(Duration::from_secs(60)).await;
if !is_normal_mode() || peer_connection_count().await > 0 {
continue;
}
if miner_connection_count().await > 0 {
continue;
}
info!("[reconnect] no operational peers remain; retrying bootstrap recovery");
match attempt_bootstrap_connections(
db.clone(),
wallet.clone(),
map.clone(),
"reconnect",
)
.await
{
Ok(true) => {}
Ok(false) => {}
Err(err) => warn!("[reconnect] bootstrap recovery aborted: {err}"),
}
}
});
}

View File

@ -16,7 +16,7 @@ use crate::records::memory::response_channels::Command;
use crate::records::record_chain::rewards_tx::rebuild_immature_reward_window;
use crate::rpc::server::start_rpc::start_rpc;
use crate::sled::Db;
use crate::startup::connections::handle_connections;
use crate::startup::connections::{handle_connections, spawn_isolated_bootstrap_recovery};
use crate::startup::daemonize::{install_shutdown_cleanup, remove_registered_pid_file};
use crate::startup::initialize_startup::open_chain_state;
use crate::verifications::verification_service::initialize_global_verification_service;
@ -165,6 +165,7 @@ pub async fn run_unlocked_node(wallet: Arc<Wallet>, install_shutdown: bool) -> R
handle_connections(db.clone(), wallet.clone(), map.clone())
.await
.map_err(|e| format!("Startup connection error: {e}"))?;
spawn_isolated_bootstrap_recovery(db.clone(), wallet.clone(), map.clone());
create_genesis_transaction(
&db,