diff --git a/README.md b/README.md index 89cfc68..7866b04 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/DEV.md b/docs/DEV.md index d137ce6..ea7a76a 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -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 diff --git a/src/bin/verify_address.rs b/src/bin/verify_address.rs index d533667..a649774 100644 --- a/src/bin/verify_address.rs +++ b/src/bin/verify_address.rs @@ -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}"); } diff --git a/src/records/memory/connections.rs b/src/records/memory/connections.rs index 028003c..e8b983b 100644 --- a/src/records/memory/connections.rs +++ b/src/records/memory/connections.rs @@ -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>> { @@ -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() diff --git a/src/standalone_tools/connections/sending_request.rs b/src/standalone_tools/connections/sending_request.rs index e7b832e..eb56b0d 100644 --- a/src/standalone_tools/connections/sending_request.rs +++ b/src/standalone_tools/connections/sending_request.rs @@ -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") diff --git a/src/standalone_tools/vanity_resolver.rs b/src/standalone_tools/vanity_resolver.rs index 8993ee7..50c02d7 100644 --- a/src/standalone_tools/vanity_resolver.rs +++ b/src/standalone_tools/vanity_resolver.rs @@ -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 { + 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 { - let normalized = Wallet::normalize_to_short_address(address.trim()) + 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); + } + } + + let canonical_vanity = canonical_vanity_address_input(trimmed) .ok_or_else(|| "Invalid wallet address.".to_string())?; - 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 + lookup_vanity_owner(&canonical_vanity, wallet).await } async fn lookup_vanity_owner(vanity_address: &str, wallet: &Wallet) -> Result { diff --git a/src/startup/connections.rs b/src/startup/connections.rs index 179c6d4..2d690dc 100644 --- a/src/startup/connections.rs +++ b/src/startup/connections.rs @@ -1,11 +1,13 @@ -use crate::common::network_startup::get_connections; -use crate::log::{error, info}; -use crate::miner::flag::{ - clear_mining_stop_request, set_mining_state, set_node_mode, MiningState, NodeMode, -}; -use crate::records::memory::response_channels::Command; -use crate::rpc::client::handshake::connect_and_handshake; -use crate::rpc::client::structs::Connect; +use crate::common::network_startup::get_connections; +use crate::log::{error, info, warn}; +use crate::miner::flag::{ + 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; use crate::sled::Db; use crate::sleep; use crate::wallets::structures::Wallet; @@ -31,12 +33,30 @@ 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, + map: Arc>, + context: &str, +) -> Result { + // 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 = None; - - for server in filtered_servers { + let mut last_error: Option = None; + + for server in filtered_servers { // build the outbound handshake request using cloned // shared state so each attempt can run independently let db_clone = db.clone(); @@ -58,32 +78,86 @@ pub async fn handle_connections( first, }; - let err_string = match connect_and_handshake(connect_params).await { - Ok(()) => { - info!("Connected to {server}"); - return Ok(()); - } - Err(err) => err.to_string(), - }; + let err_string = match connect_and_handshake(connect_params).await { + Ok(()) => { + if context == "startup" { + info!("Connected to {server}"); + } else { + info!("[reconnect] bootstrap recovery connected to {server}"); + } + return Ok(true); + } + Err(err) => err.to_string(), + }; // A peer can reject us because it already has this connection recorded. // In that case retrying other bootstrap peers would not fix the local duplicate state. - if err_string.contains("The connection is already in the connection manager Please wait 10 minutes and try again") { - return Err(err_string); - } - error!("Error connecting to {server}: {err_string}"); + 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 { - info!("No bootstrap peers connected during startup: {err}"); - } else { - info!("No bootstrap peers connected during startup."); - } - // 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(()) -} + sleep(Duration::from_secs(5)).await; + } + + if let Some(err) = last_error { + if context == "startup" { + info!("No bootstrap peers connected during startup: {err}"); + } else { + warn!("[reconnect] bootstrap recovery found no peers: {err}"); + } + } 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, + map: Arc>, +) { + 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}"), + } + } + }); +} diff --git a/src/startup/node_runtime.rs b/src/startup/node_runtime.rs index d3661e5..4d6233b 100644 --- a/src/startup/node_runtime.rs +++ b/src/startup/node_runtime.rs @@ -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, 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,