From 13ae207739554e27b357c2ca46eaff393a2965d0 Mon Sep 17 00:00:00 2001 From: viraladmin <00purple@gmail.com> Date: Fri, 5 Jun 2026 21:33:37 -0600 Subject: [PATCH] sybil detection fixes --- src/common/check_genesis.rs | 48 +- src/orphans/get_path_names.rs | 56 +-- src/orphans/save_blocks.rs | 10 +- src/orphans/torrent_candidates.rs | 9 +- .../undo_transactions/undo_borrower.rs | 200 ++++---- .../undo_transactions/undo_collateral.rs | 206 ++++---- src/records/balance_sheet/pathing.rs | 184 +++---- src/records/memory/connections.rs | 20 +- src/records/memory/network_mapping/add.rs | 85 +++- src/records/memory/network_mapping/enums.rs | 8 +- src/records/memory/network_mapping/monitor.rs | 66 +-- src/records/memory/network_mapping/structs.rs | 1 + src/records/memory/response_channels.rs | 46 +- .../unpack_block/load_by_block_number.rs | 438 ++++++++--------- src/records/unpack_block/unpack_header.rs | 78 +-- src/records/wallet_registry/helpers.rs | 4 +- src/records/wallet_registry/mappings.rs | 334 ++++++------- src/records/wallet_registry/storage.rs | 174 +++---- src/records/wallet_registry/structs.rs | 30 +- src/rpc/client/block_hash_vote.rs | 9 +- src/rpc/client/syncing.rs | 10 +- src/rpc/command_maps.rs | 2 - src/rpc/commands/add_network_node.rs | 15 + src/rpc/commands/mod.rs | 2 - src/rpc/commands/network_info.rs | 194 ++++---- src/rpc/commands/route_reply.rs | 25 +- src/rpc/commands/torrent_candidates.rs | 128 ++--- src/rpc/commands/tx_count.rs | 300 ++++++------ src/rpc/server/rpc_command_loop.rs | 38 +- src/startup/connections.rs | 108 ++-- src/startup/daemonize.rs | 460 +++++++++--------- src/startup/network_broadcast.rs | 83 +--- src/startup/remote_height.rs | 6 +- src/startup/unlock_pipe.rs | 406 ++++++++-------- .../torrenting_system/request_piece.rs | 9 +- src/torrent/torrenting_system/save_torrent.rs | 308 ++++++------ src/torrent/unpack_local_torrent.rs | 136 +++--- src/wallets/load_wallets.rs | 180 +++---- 38 files changed, 2223 insertions(+), 2193 deletions(-) diff --git a/src/common/check_genesis.rs b/src/common/check_genesis.rs index 2e55f4a..bca9e18 100644 --- a/src/common/check_genesis.rs +++ b/src/common/check_genesis.rs @@ -1,24 +1,24 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::metadata; -use crate::PathBuf; - -// Check whether the local chain already has the active network's genesis block. -pub async fn genesis_checkup() -> bool { - // Resolve the active network suffix and block directory from the - // shared path helper so mainnet/testnet never duplicate path logic. - let ( - _network_name, - _padded_base_coin, - suffix, - _torrent_path, - _wallet_path, - block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - - // Genesis is always block zero using the current network's block suffix. - let genesis_location = PathBuf::from(block_path).join(format!("0.{suffix}")); - (metadata(genesis_location).await).is_ok() -} +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::metadata; +use crate::PathBuf; + +// Check whether the local chain already has the active network's genesis block. +pub async fn genesis_checkup() -> bool { + // Resolve the active network suffix and block directory from the + // shared path helper so mainnet/testnet never duplicate path logic. + let ( + _network_name, + _padded_base_coin, + suffix, + _torrent_path, + _wallet_path, + block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + + // Genesis is always block zero using the current network's block suffix. + let genesis_location = PathBuf::from(block_path).join(format!("0.{suffix}")); + (metadata(genesis_location).await).is_ok() +} diff --git a/src/orphans/get_path_names.rs b/src/orphans/get_path_names.rs index a2c111c..4880f98 100644 --- a/src/orphans/get_path_names.rs +++ b/src/orphans/get_path_names.rs @@ -1,28 +1,28 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::PathBuf; - -pub async fn get_file_names(start_height: u32) -> (String, String) { - // build the canonical block and torrent filenames - // for the height currently being undone - let ( - _network_name, - _padded_base_coin, - block_ext, - torrent_path, - _wallet_path, - block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - - let torrent_name = PathBuf::from(torrent_path) - .join(format!("{start_height}.torrent")) - .to_string_lossy() - .into_owned(); - let block_name = PathBuf::from(block_path) - .join(format!("{start_height}.{block_ext}")) - .to_string_lossy() - .into_owned(); - (torrent_name, block_name) -} +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::PathBuf; + +pub async fn get_file_names(start_height: u32) -> (String, String) { + // build the canonical block and torrent filenames + // for the height currently being undone + let ( + _network_name, + _padded_base_coin, + block_ext, + torrent_path, + _wallet_path, + block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + + let torrent_name = PathBuf::from(torrent_path) + .join(format!("{start_height}.torrent")) + .to_string_lossy() + .into_owned(); + let block_name = PathBuf::from(block_path) + .join(format!("{start_height}.{block_ext}")) + .to_string_lossy() + .into_owned(); + (torrent_name, block_name) +} diff --git a/src/orphans/save_blocks.rs b/src/orphans/save_blocks.rs index 039cc6b..c628013 100644 --- a/src/orphans/save_blocks.rs +++ b/src/orphans/save_blocks.rs @@ -2,7 +2,8 @@ use crate::orphans::replay_errors::staged_candidate_status_for_error; use crate::orphans::structs::UndoTransactions; use crate::orphans::torrent_candidates::hydrate_torrent_candidates; use crate::records::block_height::get_block_height::get_height; -use crate::records::memory::response_channels::reserve_entry; +use crate::records::memory::response_channels::reserve_entry_with_context; +use crate::rpc::command_maps::RPC_TORRENT_BY_HEIGHT; use crate::records::memory::torrent_status::{ get_torrent_status, set_torrent_status, TorrentStatus, }; @@ -137,7 +138,12 @@ pub async fn save_new_blocks( // No staged candidate worked, so request the replacement torrent // directly from the connected peer. - let (hashmap_key, _save_tx, save_rx) = reserve_entry(params.map.clone()).await; + let (hashmap_key, _save_tx, save_rx) = reserve_entry_with_context( + params.map.clone(), + Some(RPC_TORRENT_BY_HEIGHT), + Some(params.connections_key.clone()), + ) + .await; send_request_torrent_message( params.stream.clone(), diff --git a/src/orphans/torrent_candidates.rs b/src/orphans/torrent_candidates.rs index 620e6cc..a4532c3 100644 --- a/src/orphans/torrent_candidates.rs +++ b/src/orphans/torrent_candidates.rs @@ -1,4 +1,4 @@ -use crate::records::memory::response_channels::{reserve_entry, Command}; +use crate::records::memory::response_channels::{reserve_entry_with_context, Command}; use crate::rpc::command_maps::RPC_TORRENT_CANDIDATES; use crate::rpc::responses::RpcResponse; use crate::torrent::torrenting_system::save_torrent::save_staged_torrent; @@ -11,7 +11,12 @@ pub async fn hydrate_torrent_candidates( ) -> Result { // Reserve a reply slot and send a small request packet asking the peer for // its staged/local torrent candidates. - let (hashmap_key, _tx, rx) = reserve_entry(map.clone()).await; + let (hashmap_key, _tx, rx) = reserve_entry_with_context( + map.clone(), + Some(RPC_TORRENT_CANDIDATES), + Some(connections_key.clone()), + ) + .await; let mut message = Vec::with_capacity(4); message.push(RPC_TORRENT_CANDIDATES); message.extend_from_slice(&hashmap_key); diff --git a/src/orphans/undo_transactions/undo_borrower.rs b/src/orphans/undo_transactions/undo_borrower.rs index eec5f0f..b7538b4 100644 --- a/src/orphans/undo_transactions/undo_borrower.rs +++ b/src/orphans/undo_transactions/undo_borrower.rs @@ -1,105 +1,105 @@ -use crate::blocks::loan_payment::ContractPaymentTransaction; -use crate::blocks::loans::LoanContractTransaction; +use crate::blocks::loan_payment::ContractPaymentTransaction; +use crate::blocks::loans::LoanContractTransaction; use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::records::record_chain::add_payments_db::remove_payment; -use crate::records::record_chain::nft_provenance::remove_nft_history_entry; -use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; -use crate::rpc::responses::RpcResponse; -use crate::sled::Db; - -pub async fn undo_borrower_transaction( - transaction: ContractPaymentTransaction, - mining_receiver: &str, - db: &Db, -) -> Result<(), String> { - // restore balances and database state for a contract payment - // that is being removed during orphan rollback - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - - // reload the original loan contract so the rollback uses the - // same lender and asset information the payment was based on - let contract_hash = decode(&transaction.unsigned_contract_payment.contract_hash) - .map_err(|e| format!("Error decoding contract hash: {e}"))?; - let contract = request_transaction_by_txid(db, contract_hash.clone()).await; - - let loan_txtype = 7; - let loan_tx = match contract { - RpcResponse::Binary(contract_bytes) => { - if contract_bytes.is_empty() { - return Err("Invalid loan contract: empty transaction bytes".to_string()); - } - if contract_bytes[0] != loan_txtype { - return Err( - "Invalid loan contract: referenced transaction is not a loan contract" - .to_string(), - ); - } - LoanContractTransaction::from_bytes(loan_txtype, &contract_bytes[1..]) - .await - .map_err(|e| e.to_string())? - } - }; - - let lender = loan_tx.unsigned_loan_contract.lender; - let loan_coin = loan_tx.unsigned_loan_contract.loan_coin; - let borrower = &transaction.unsigned_contract_payment.address; - let payback_amount = transaction.unsigned_contract_payment.payback_amount; - let tip = transaction.unsigned_contract_payment.tip; - let txfee = transaction.unsigned_contract_payment.txfee; - - // reverse the fee, tip, and repayment movements that were - // applied when the borrower payment was originally saved - let _ = - balance_sheet_operation_with_db(db, mining_receiver, txfee, &type_str, operand_subtraction); - let _ = balance_sheet_operation_with_db(db, borrower, txfee, &type_str, operand_addition); - let _ = - balance_sheet_operation_with_db(db, mining_receiver, tip, &loan_coin, operand_subtraction); - let _ = balance_sheet_operation_with_db(db, borrower, tip, &loan_coin, operand_addition); - let _ = balance_sheet_operation_with_db( - db, - &lender, - payback_amount, - &loan_coin, - operand_subtraction, - ); - let _ = - balance_sheet_operation_with_db(db, borrower, payback_amount, &loan_coin, operand_addition); - - // Remove the payment transaction lookup from the txid tree. - let txid_tree = db - .open_tree("txid") - .map_err(|e| format!("Failed to open txid tree: {e}"))?; - let tx_hash = transaction.unsigned_contract_payment.hash().await; - txid_tree - .remove(decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?) - .map_err(|e| format!("Failed to remove borrower txid: {e}"))?; - - // Loan payments involving NFTs also add a provenance entry for the loan - // asset, so remove it if this loan coin is tracked as an NFT. - let nft_tree = db - .open_tree("nfts") - .map_err(|e| format!("Failed to open nfts tree: {e}"))?; - let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?; - if nft_tree.contains_key(loan_coin.as_bytes()).unwrap_or(false) { - let _ = remove_nft_history_entry(db, &loan_coin, &tx_hash_bytes); - } - - // The aggregate payment record is reduced by the payment being undone. - let _ = remove_payment(db, contract_hash, payback_amount); - +use crate::records::record_chain::add_payments_db::remove_payment; +use crate::records::record_chain::nft_provenance::remove_nft_history_entry; +use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; + +pub async fn undo_borrower_transaction( + transaction: ContractPaymentTransaction, + mining_receiver: &str, + db: &Db, +) -> Result<(), String> { + // restore balances and database state for a contract payment + // that is being removed during orphan rollback + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + + // reload the original loan contract so the rollback uses the + // same lender and asset information the payment was based on + let contract_hash = decode(&transaction.unsigned_contract_payment.contract_hash) + .map_err(|e| format!("Error decoding contract hash: {e}"))?; + let contract = request_transaction_by_txid(db, contract_hash.clone()).await; + + let loan_txtype = 7; + let loan_tx = match contract { + RpcResponse::Binary(contract_bytes) => { + if contract_bytes.is_empty() { + return Err("Invalid loan contract: empty transaction bytes".to_string()); + } + if contract_bytes[0] != loan_txtype { + return Err( + "Invalid loan contract: referenced transaction is not a loan contract" + .to_string(), + ); + } + LoanContractTransaction::from_bytes(loan_txtype, &contract_bytes[1..]) + .await + .map_err(|e| e.to_string())? + } + }; + + let lender = loan_tx.unsigned_loan_contract.lender; + let loan_coin = loan_tx.unsigned_loan_contract.loan_coin; + let borrower = &transaction.unsigned_contract_payment.address; + let payback_amount = transaction.unsigned_contract_payment.payback_amount; + let tip = transaction.unsigned_contract_payment.tip; + let txfee = transaction.unsigned_contract_payment.txfee; + + // reverse the fee, tip, and repayment movements that were + // applied when the borrower payment was originally saved + let _ = + balance_sheet_operation_with_db(db, mining_receiver, txfee, &type_str, operand_subtraction); + let _ = balance_sheet_operation_with_db(db, borrower, txfee, &type_str, operand_addition); + let _ = + balance_sheet_operation_with_db(db, mining_receiver, tip, &loan_coin, operand_subtraction); + let _ = balance_sheet_operation_with_db(db, borrower, tip, &loan_coin, operand_addition); + let _ = balance_sheet_operation_with_db( + db, + &lender, + payback_amount, + &loan_coin, + operand_subtraction, + ); + let _ = + balance_sheet_operation_with_db(db, borrower, payback_amount, &loan_coin, operand_addition); + + // Remove the payment transaction lookup from the txid tree. + let txid_tree = db + .open_tree("txid") + .map_err(|e| format!("Failed to open txid tree: {e}"))?; + let tx_hash = transaction.unsigned_contract_payment.hash().await; + txid_tree + .remove(decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?) + .map_err(|e| format!("Failed to remove borrower txid: {e}"))?; + + // Loan payments involving NFTs also add a provenance entry for the loan + // asset, so remove it if this loan coin is tracked as an NFT. + let nft_tree = db + .open_tree("nfts") + .map_err(|e| format!("Failed to open nfts tree: {e}"))?; + let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?; + if nft_tree.contains_key(loan_coin.as_bytes()).unwrap_or(false) { + let _ = remove_nft_history_entry(db, &loan_coin, &tx_hash_bytes); + } + + // The aggregate payment record is reduced by the payment being undone. + let _ = remove_payment(db, contract_hash, payback_amount); + Ok(()) } diff --git a/src/orphans/undo_transactions/undo_collateral.rs b/src/orphans/undo_transactions/undo_collateral.rs index bb727ae..7e11707 100644 --- a/src/orphans/undo_transactions/undo_collateral.rs +++ b/src/orphans/undo_transactions/undo_collateral.rs @@ -1,108 +1,108 @@ -use crate::blocks::collateral::CollateralClaimTransaction; -use crate::blocks::loans::LoanContractTransaction; +use crate::blocks::collateral::CollateralClaimTransaction; +use crate::blocks::loans::LoanContractTransaction; use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::records::record_chain::nft_provenance::remove_nft_history_entry; -use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; -use crate::rpc::responses::RpcResponse; -use crate::sled::Db; - -pub async fn undo_collateral_transaction( - transaction: CollateralClaimTransaction, - mining_receiver: &str, - db: &Db, -) -> Result<(), String> { - // restore balances and contract state for a collateral - // claim that is being removed during orphan rollback - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - - // reload the original loan contract so the collateral - // asset and amount can be restored correctly - let contract_hash = decode(&transaction.unsigned_collateral_claim.contract_hash) - .map_err(|e| format!("Error decoding contract hash: {e}"))?; - let contract = request_transaction_by_txid(db, contract_hash.clone()).await; - - let loan_txtype = 7; - let loan_tx = match contract { - RpcResponse::Binary(contract_bytes) => { - if contract_bytes.is_empty() { - return Err("Invalid loan contract: empty transaction bytes".to_string()); - } - if contract_bytes[0] != loan_txtype { - return Err( - "Invalid loan contract: referenced transaction is not a loan contract" - .to_string(), - ); - } - LoanContractTransaction::from_bytes(loan_txtype, &contract_bytes[1..]) - .await - .map_err(|e| e.to_string())? - } - }; - - let collateral = loan_tx.unsigned_loan_contract.collateral; - let collateral_amount = loan_tx.unsigned_loan_contract.collateral_amount; - let collateral_holding = format!( - "collateral_{}", - transaction.unsigned_collateral_claim.contract_hash - ); - let claimer = &transaction.unsigned_collateral_claim.address; - let txfee = transaction.unsigned_collateral_claim.txfee; - - // reverse the fee and move the collateral back into the - // contract holding wallet until the claim exists again - let _ = - balance_sheet_operation_with_db(db, mining_receiver, txfee, &type_str, operand_subtraction); - let _ = balance_sheet_operation_with_db(db, claimer, txfee, &type_str, operand_addition); - let _ = balance_sheet_operation_with_db( - db, - claimer, - collateral_amount, - &collateral, - operand_subtraction, - ); - let _ = balance_sheet_operation_with_db( - db, - &collateral_holding, - collateral_amount, - &collateral, - operand_addition, - ); - - // Remove the collateral-claim transaction lookup from the txid tree. - let txid_tree = db.open_tree("txid").unwrap(); - let tx_hash = transaction.unsigned_collateral_claim.hash().await; - txid_tree - .remove(decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?) - .unwrap(); - - // NFT collateral claims write provenance for the collateral asset. - let nft_tree = db.open_tree("nfts").unwrap(); - let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?; - if nft_tree - .contains_key(collateral.as_bytes()) - .unwrap_or(false) - { - let _ = remove_nft_history_entry(db, &collateral, &tx_hash_bytes); - } - - // Mark the loan contract active again because the collateral claim no - // longer exists after rollback. - let loan_tree = db.open_tree("loan").unwrap(); - loan_tree.insert(contract_hash, "true".as_bytes()).unwrap(); - +use crate::records::record_chain::nft_provenance::remove_nft_history_entry; +use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; + +pub async fn undo_collateral_transaction( + transaction: CollateralClaimTransaction, + mining_receiver: &str, + db: &Db, +) -> Result<(), String> { + // restore balances and contract state for a collateral + // claim that is being removed during orphan rollback + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + + // reload the original loan contract so the collateral + // asset and amount can be restored correctly + let contract_hash = decode(&transaction.unsigned_collateral_claim.contract_hash) + .map_err(|e| format!("Error decoding contract hash: {e}"))?; + let contract = request_transaction_by_txid(db, contract_hash.clone()).await; + + let loan_txtype = 7; + let loan_tx = match contract { + RpcResponse::Binary(contract_bytes) => { + if contract_bytes.is_empty() { + return Err("Invalid loan contract: empty transaction bytes".to_string()); + } + if contract_bytes[0] != loan_txtype { + return Err( + "Invalid loan contract: referenced transaction is not a loan contract" + .to_string(), + ); + } + LoanContractTransaction::from_bytes(loan_txtype, &contract_bytes[1..]) + .await + .map_err(|e| e.to_string())? + } + }; + + let collateral = loan_tx.unsigned_loan_contract.collateral; + let collateral_amount = loan_tx.unsigned_loan_contract.collateral_amount; + let collateral_holding = format!( + "collateral_{}", + transaction.unsigned_collateral_claim.contract_hash + ); + let claimer = &transaction.unsigned_collateral_claim.address; + let txfee = transaction.unsigned_collateral_claim.txfee; + + // reverse the fee and move the collateral back into the + // contract holding wallet until the claim exists again + let _ = + balance_sheet_operation_with_db(db, mining_receiver, txfee, &type_str, operand_subtraction); + let _ = balance_sheet_operation_with_db(db, claimer, txfee, &type_str, operand_addition); + let _ = balance_sheet_operation_with_db( + db, + claimer, + collateral_amount, + &collateral, + operand_subtraction, + ); + let _ = balance_sheet_operation_with_db( + db, + &collateral_holding, + collateral_amount, + &collateral, + operand_addition, + ); + + // Remove the collateral-claim transaction lookup from the txid tree. + let txid_tree = db.open_tree("txid").unwrap(); + let tx_hash = transaction.unsigned_collateral_claim.hash().await; + txid_tree + .remove(decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?) + .unwrap(); + + // NFT collateral claims write provenance for the collateral asset. + let nft_tree = db.open_tree("nfts").unwrap(); + let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?; + if nft_tree + .contains_key(collateral.as_bytes()) + .unwrap_or(false) + { + let _ = remove_nft_history_entry(db, &collateral, &tx_hash_bytes); + } + + // Mark the loan contract active again because the collateral claim no + // longer exists after rollback. + let loan_tree = db.open_tree("loan").unwrap(); + loan_tree.insert(contract_hash, "true".as_bytes()).unwrap(); + Ok(()) } diff --git a/src/records/balance_sheet/pathing.rs b/src/records/balance_sheet/pathing.rs index f53e07f..902b033 100644 --- a/src/records/balance_sheet/pathing.rs +++ b/src/records/balance_sheet/pathing.rs @@ -1,92 +1,92 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::records::balance_sheet::tokens_to_lower::strip_spaces_and_lowercase; -use crate::wallets::structures::Wallet; -use crate::PathBuf; - -pub fn network_name() -> &'static str { - let ( - network_name, - _padded_base_coin, - _suffix, - _torrent_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - network_name -} - -pub fn balance_root_path() -> PathBuf { - // The balance root is the configured balance-sheet directory scoped - // to the active network name. - let ( - _network_name, - _padded_base_coin, - _suffix, - _torrent_path, - _wallet_path, - _block_path, - _db_path, - balance_path, - _log_path, - ) = block_extension_and_paths(); - PathBuf::from(balance_path) -} - -pub fn canonical_balance_address(address: &str) -> Option { - // Balance storage is normalized to the deterministic short address, - // regardless of whether callers still pass a long or short address. - Wallet::normalize_to_short_address(address) -} - -pub fn address_root_path(address: &str) -> Option { - let canonical_address = canonical_balance_address(address)?; - Some(balance_root_path().join(canonical_address)) -} - -pub fn balance_asset_segments(coin_type: &str) -> Vec { - // NFT balances use a nested path of asset name plus item/series - // number, while normal coins and tokens stay as a single segment. - let coin = strip_spaces_and_lowercase(coin_type); - - if let Some((series_name, item_number)) = coin.rsplit_once('_') { - if !series_name.is_empty() - && !item_number.is_empty() - && item_number.chars().all(|c| c.is_ascii_digit()) - { - return vec![series_name.to_string(), item_number.to_string()]; - } - } - - vec![coin] -} - -pub fn balance_file_path(address: &str, coin_type: &str) -> PathBuf { - // Build the canonical wallet balance path for an address and asset - // using the current hierarchical balance-sheet layout. - let mut path = address_root_path(address).unwrap_or_else(balance_root_path); - for segment in balance_asset_segments(coin_type) { - path.push(segment); - } - path.push("wallet.bal"); - path -} - -pub fn asset_name_from_relative_path(relative_path: &std::path::Path) -> Option { - // Convert a relative balance-sheet file path back into the logical - // asset name used by wallet and balance queries. - let segments: Vec = relative_path - .iter() - .map(|part| part.to_string_lossy().into_owned()) - .collect(); - - match segments.as_slice() { - [token_dir, wallet_file] if wallet_file == "wallet.bal" => Some(token_dir.clone()), - [token_dir, item_number, wallet_file] if wallet_file == "wallet.bal" => { - Some(format!("{token_dir}_{item_number}")) - } - _ => None, - } -} +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::records::balance_sheet::tokens_to_lower::strip_spaces_and_lowercase; +use crate::wallets::structures::Wallet; +use crate::PathBuf; + +pub fn network_name() -> &'static str { + let ( + network_name, + _padded_base_coin, + _suffix, + _torrent_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + network_name +} + +pub fn balance_root_path() -> PathBuf { + // The balance root is the configured balance-sheet directory scoped + // to the active network name. + let ( + _network_name, + _padded_base_coin, + _suffix, + _torrent_path, + _wallet_path, + _block_path, + _db_path, + balance_path, + _log_path, + ) = block_extension_and_paths(); + PathBuf::from(balance_path) +} + +pub fn canonical_balance_address(address: &str) -> Option { + // Balance storage is normalized to the deterministic short address, + // regardless of whether callers still pass a long or short address. + Wallet::normalize_to_short_address(address) +} + +pub fn address_root_path(address: &str) -> Option { + let canonical_address = canonical_balance_address(address)?; + Some(balance_root_path().join(canonical_address)) +} + +pub fn balance_asset_segments(coin_type: &str) -> Vec { + // NFT balances use a nested path of asset name plus item/series + // number, while normal coins and tokens stay as a single segment. + let coin = strip_spaces_and_lowercase(coin_type); + + if let Some((series_name, item_number)) = coin.rsplit_once('_') { + if !series_name.is_empty() + && !item_number.is_empty() + && item_number.chars().all(|c| c.is_ascii_digit()) + { + return vec![series_name.to_string(), item_number.to_string()]; + } + } + + vec![coin] +} + +pub fn balance_file_path(address: &str, coin_type: &str) -> PathBuf { + // Build the canonical wallet balance path for an address and asset + // using the current hierarchical balance-sheet layout. + let mut path = address_root_path(address).unwrap_or_else(balance_root_path); + for segment in balance_asset_segments(coin_type) { + path.push(segment); + } + path.push("wallet.bal"); + path +} + +pub fn asset_name_from_relative_path(relative_path: &std::path::Path) -> Option { + // Convert a relative balance-sheet file path back into the logical + // asset name used by wallet and balance queries. + let segments: Vec = relative_path + .iter() + .map(|part| part.to_string_lossy().into_owned()) + .collect(); + + match segments.as_slice() { + [token_dir, wallet_file] if wallet_file == "wallet.bal" => Some(token_dir.clone()), + [token_dir, item_number, wallet_file] if wallet_file == "wallet.bal" => { + Some(format!("{token_dir}_{item_number}")) + } + _ => None, + } +} diff --git a/src/records/memory/connections.rs b/src/records/memory/connections.rs index df40d61..228818e 100644 --- a/src/records/memory/connections.rs +++ b/src/records/memory/connections.rs @@ -5,7 +5,9 @@ use crate::records::memory::enums::{ClientType, ConnectionType}; use crate::records::memory::network_mapping::monitor::{MONITOR_ACTION_ADD, MONITOR_ACTION_REMOVE}; use crate::records::memory::network_mapping::structs::{MonitorAddressParams, SignedMonitorEdit}; use crate::records::memory::network_mapping::NodeInfo; -use crate::records::memory::response_channels::{delete_entry, reserve_entry, Command}; +use crate::records::memory::response_channels::{ + delete_entry, reserve_entry_with_context, Command, +}; use crate::records::memory::structs::{Connection, StoreConnectionParams}; use crate::rpc::client::handshake::connect_and_handshake; use crate::rpc::client::handshake_processing::{bootstrap_peer_discovery, BootstrapParams}; @@ -350,7 +352,12 @@ impl Connection { let message_type = RPC_BLOCK_HEIGHT; // Block-height request used as a lightweight checkup ping. let (checkup_key, _checkup_tx, checkup_rx_mutex) = - reserve_entry(command_map.clone()).await; + reserve_entry_with_context( + command_map.clone(), + Some(RPC_BLOCK_HEIGHT), + Some(format!("{ip}:{port}")), + ) + .await; // Send a lightweight ping message and wait for the reply // routed back through the shared response hashmap. @@ -776,7 +783,7 @@ fn spawn_monitor_update(ip: String, action: u8, peer_public_key: Vec, port: .await; let edit = SignedMonitorEdit { action, - monitored_address, + monitored_address: monitored_address.clone(), monitoring_address, target_ip: ip.clone(), modified_timestamp: timestamp, @@ -795,6 +802,13 @@ fn spawn_monitor_update(ip: String, action: u8, peer_public_key: Vec, port: } else { NodeInfo::remove_monitor(params).await }; + NodeInfo::broadcast_address_state( + context.map.clone(), + &monitored_address, + "", + &format!("{ip}:{port}"), + ) + .await; }); } diff --git a/src/records/memory/network_mapping/add.rs b/src/records/memory/network_mapping/add.rs index 6fc5a72..64264fa 100644 --- a/src/records/memory/network_mapping/add.rs +++ b/src/records/memory/network_mapping/add.rs @@ -1,6 +1,28 @@ use super::*; impl NodeInfo { + pub async fn broadcast_address_state( + map: Arc>, + address: &str, + remote_ip: &str, + connections_key: &str, + ) { + let edit = { + let address_map = ADDRESS_MAP.lock().await; + let Some(node) = address_map.get(address) else { + return; + }; + SignedNodeEdit { + address: address.to_string(), + ip: node.ip.clone(), + modified_by: node.added_by.clone(), + modified_timestamp: node.added_timestamp, + modified_signature: node.added_signature.clone(), + } + }; + Self::broadcast_node(map, &edit, remote_ip, NodeEditType::Add, connections_key).await; + } + pub async fn broadcast_node( map: Arc>, edit: &SignedNodeEdit, @@ -28,6 +50,20 @@ impl NodeInfo { }; let modified_timestamp_bytes = edit.modified_timestamp.to_le_bytes(); let modified_signature_bytes = decode(&edit.modified_signature).unwrap(); + let monitor_bytes = { + let address_map = ADDRESS_MAP.lock().await; + address_map + .get(&edit.address) + .map(|node| { + node.monitoring + .iter() + .filter_map(|monitor| Wallet::short_address_to_bytes(monitor)) + .collect::>() + }) + .unwrap_or_default() + }; + let monitor_count = monitor_bytes.len().min(u16::MAX as usize) as u16; + let monitor_count_bytes = monitor_count.to_le_bytes(); let streams = { let connections_lock = CONNECTIONS.read().await; connections_lock @@ -47,6 +83,10 @@ impl NodeInfo { message.extend_from_slice(&modified_by_bytes); message.extend_from_slice(&modified_timestamp_bytes); message.extend_from_slice(&modified_signature_bytes); + message.extend_from_slice(&monitor_count_bytes); + for monitor in monitor_bytes.iter().take(monitor_count as usize) { + message.extend_from_slice(monitor); + } let Some((peer_ip, _)) = peer_key.rsplit_once(':') else { continue; }; @@ -68,6 +108,7 @@ impl NodeInfo { let AddAddressParams { map, mut edit, + monitors, mut blocks_mined, remote_ip, db, @@ -118,6 +159,7 @@ impl NodeInfo { } let mut penalize_duplicate_ip = false; + let mut accepted_existing_node = false; { let mut address_map = ADDRESS_MAP.lock().await; @@ -168,6 +210,7 @@ impl NodeInfo { // same IP are still rejected, but deleted records remain in place // for historical validation. if let Some(existing_node) = address_map.get_mut(&edit.address) { + existing_node.monitoring = monitors.clone(); if existing_node.deleted_timestamp > 0 { if existing_node.ip == edit.ip { existing_node.deleted_timestamp = 0_u64; @@ -186,29 +229,35 @@ impl NodeInfo { edit.modified_signature.clone(), ); } - return RpcResponse::Binary(b"Success".to_vec()); + accepted_existing_node = true; } } - if let Some(existing_node) = address_map.values_mut().find(|node| node.ip == edit.ip) { - if existing_node.deleted_timestamp == 0 && edit.ip != GENESIS_IP { - penalize_duplicate_ip = true; + if !accepted_existing_node { + if let Some(existing_node) = address_map.values_mut().find(|node| node.ip == edit.ip) { + if existing_node.deleted_timestamp == 0 && edit.ip != GENESIS_IP { + penalize_duplicate_ip = true; + } } - } - if !penalize_duplicate_ip { - // Persist the new node locally. Network-map entries are bare - // IP membership records, separate from live socket keys. - address_map.insert( - edit.address.clone(), - NodeInfo::new( - edit.ip.clone(), - blocks_mined, - edit.modified_by.clone(), - edit.modified_timestamp, - edit.modified_signature.clone(), - ), - ); + if !penalize_duplicate_ip { + // Persist the new node locally. Network-map entries are bare + // IP membership records, separate from live socket keys. + address_map.insert( + edit.address.clone(), + { + let mut node = NodeInfo::new( + edit.ip.clone(), + blocks_mined, + edit.modified_by.clone(), + edit.modified_timestamp, + edit.modified_signature.clone(), + ); + node.monitoring = monitors.clone(); + node + }, + ); + } } } diff --git a/src/records/memory/network_mapping/enums.rs b/src/records/memory/network_mapping/enums.rs index aa4cf67..56f43e6 100644 --- a/src/records/memory/network_mapping/enums.rs +++ b/src/records/memory/network_mapping/enums.rs @@ -1,13 +1,9 @@ -use crate::rpc::command_maps::{ - RPC_ADD_NETWORK_NODE, RPC_NETWORK_MONITOR_ADD, RPC_NETWORK_MONITOR_REMOVE, -}; +use crate::rpc::command_maps::RPC_ADD_NETWORK_NODE; // NodeEditType keeps the network membership update type explicit at the call sites. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NodeEditType { Add, - MonitorAdd, - MonitorRemove, } impl NodeEditType { @@ -15,8 +11,6 @@ impl NodeEditType { pub fn message_type(self) -> u8 { match self { NodeEditType::Add => RPC_ADD_NETWORK_NODE, - NodeEditType::MonitorAdd => RPC_NETWORK_MONITOR_ADD, - NodeEditType::MonitorRemove => RPC_NETWORK_MONITOR_REMOVE, } } } diff --git a/src/records/memory/network_mapping/monitor.rs b/src/records/memory/network_mapping/monitor.rs index e7b1a24..f324856 100644 --- a/src/records/memory/network_mapping/monitor.rs +++ b/src/records/memory/network_mapping/monitor.rs @@ -21,62 +21,6 @@ impl NodeInfo { Wallet::sign_transaction(&hashed_data, private_key).await } - pub async fn broadcast_monitor( - map: Arc>, - edit: &SignedMonitorEdit, - remote_ip: &str, - edittype: NodeEditType, - _connections_key: &str, - ) { - let monitored_bytes = match Wallet::short_address_to_bytes(&edit.monitored_address) { - Some(bytes) => bytes, - None => return, - }; - let monitoring_bytes = match Wallet::short_address_to_bytes(&edit.monitoring_address) { - Some(bytes) => bytes, - None => return, - }; - let target_ip_bytes = ip_to_binary(&edit.target_ip); - let timestamp_bytes = edit.modified_timestamp.to_le_bytes(); - let signature_bytes = match decode(&edit.modified_signature) { - Ok(bytes) => bytes, - Err(_) => return, - }; - - let streams = { - let connections_lock = CONNECTIONS.read().await; - connections_lock - .as_ref() - .map(|connection| connection.get_all_ready_peer_streams_with_keys()) - .unwrap_or_default() - }; - - if !streams.is_empty() { - for (peer_key, unlocked_stream) in streams { - let Some((peer_ip, _)) = peer_key.rsplit_once(':') else { - continue; - }; - if !remote_ip.is_empty() && peer_ip == remote_ip { - continue; - } - - let (hashmap_key, _hashmap_tx, hashmap_rx) = reserve_entry(map.clone()).await; - let mut message = Vec::new(); - message.push(edittype.message_type()); - message.extend_from_slice(&hashmap_key); - message.push(edit.action); - message.extend_from_slice(&monitored_bytes); - message.extend_from_slice(&monitoring_bytes); - message.extend_from_slice(&target_ip_bytes); - message.extend_from_slice(×tamp_bytes); - message.extend_from_slice(&signature_bytes); - RpcResponse::send_raw(&unlocked_stream, Some(&peer_key), &message).await; - let mut rx = hashmap_rx.lock().await; - let _ = timeout(Duration::from_secs(5), rx.recv()).await; - } - } - } - fn mark_deleted_and_cascade( address_map: &mut HashMap, deleted_address: &str, @@ -155,12 +99,11 @@ impl NodeInfo { async fn apply_monitor(params: MonitorAddressParams, action: u8) -> RpcResponse { let MonitorAddressParams { - map, mut edit, remote_ip, db, wallet, - connections_key, + .. } = params; let current_timestamp = Utc::now().timestamp_millis() as u64; @@ -221,13 +164,6 @@ impl NodeInfo { } } - let broadcast_type = if action == MONITOR_ACTION_ADD { - NodeEditType::MonitorAdd - } else { - NodeEditType::MonitorRemove - }; - Self::broadcast_monitor(map, &edit, &remote_ip, broadcast_type, &connections_key).await; - RpcResponse::Binary(b"Success".to_vec()) } diff --git a/src/records/memory/network_mapping/structs.rs b/src/records/memory/network_mapping/structs.rs index c587a5d..857dc71 100644 --- a/src/records/memory/network_mapping/structs.rs +++ b/src/records/memory/network_mapping/structs.rs @@ -48,6 +48,7 @@ pub struct SignedMonitorEdit { pub struct AddAddressParams { pub map: Arc>, pub edit: SignedNodeEdit, + pub monitors: Vec, pub blocks_mined: u8, pub remote_ip: String, pub db: Db, diff --git a/src/records/memory/response_channels.rs b/src/records/memory/response_channels.rs index 1331489..fa85bfb 100644 --- a/src/records/memory/response_channels.rs +++ b/src/records/memory/response_channels.rs @@ -1,3 +1,4 @@ +use crate::log::warn; use crate::mpsc; use crate::Arc; use crate::Duration; @@ -10,19 +11,24 @@ pub struct ChannelPair { pub tx: mpsc::Sender>, pub rx: Arc>>>, pub expires_at: Option, + pub created_at: Instant, + pub command: Option, + pub peer: Option, } pub type Byte3 = [u8; 3]; pub type Command = HashMap; +const SLOW_RPC_TRACE_MS: u128 = 1_000; + fn random_3_byte_number() -> [u8; 3] { let mut rng = rand::thread_rng(); let num: u32 = rng.gen_range(0..=0xFFFFFF); // The protocol UID is three bytes on the wire, so the random u32 is sliced // down to the same fixed-width little-endian layout used by requests. - num.to_le_bytes()[1..4].try_into().unwrap() + num.to_le_bytes()[0..3].try_into().unwrap() } // Generate an untracked three-byte UID for fire-and-forget messages @@ -46,6 +52,18 @@ pub async fn reserve_entry( Byte3, mpsc::Sender>, Arc>>>, +) { + reserve_entry_with_context(map, None, None).await +} + +pub async fn reserve_entry_with_context( + map: Arc>, + command: Option, + peer: Option, +) -> ( + Byte3, + mpsc::Sender>, + Arc>>>, ) { loop { let key = random_3_byte_number(); @@ -74,6 +92,9 @@ pub async fn reserve_entry( tx: tx.clone(), rx: rx.clone(), expires_at: None, + created_at: now, + command, + peer: peer.clone(), }, ); (tx, rx) @@ -82,6 +103,21 @@ pub async fn reserve_entry( } } +pub struct ReplyTrace { + pub age_ms: u128, + pub command: Option, + pub peer: Option, +} + +pub async fn trace_entry(map: Arc>, key: Byte3) -> Option { + let map = map.lock().await; + map.get(&key).map(|channel_pair| ReplyTrace { + age_ms: channel_pair.created_at.elapsed().as_millis(), + command: channel_pair.command, + peer: channel_pair.peer.clone(), + }) +} + pub async fn get_entry(map: Arc>, key: Byte3) -> Option>> { let map = map.lock().await; if let Some(channel_pair) = map.get(&key) { @@ -110,6 +146,14 @@ pub async fn is_retired_entry(map: Arc>, key: Byte3) -> bool { pub async fn delete_entry(map: Arc>, key: Byte3) { let mut map = map.lock().await; if let Some(channel_pair) = map.get_mut(&key) { + let age_ms = channel_pair.created_at.elapsed().as_millis(); + if age_ms >= SLOW_RPC_TRACE_MS && channel_pair.expires_at.is_none() { + warn!( + "[rpc_trace] retiring slow uid: uid={key:?} cmd={:?} peer={} age_ms={age_ms}", + channel_pair.command, + channel_pair.peer.as_deref().unwrap_or("unknown") + ); + } // Keep the UID reserved briefly after completion so a late duplicate // reply cannot be mistaken for a fresh request. channel_pair.expires_at = Some(Instant::now() + Duration::from_secs(30)); diff --git a/src/records/unpack_block/load_by_block_number.rs b/src/records/unpack_block/load_by_block_number.rs index ea15820..1bd6239 100644 --- a/src/records/unpack_block/load_by_block_number.rs +++ b/src/records/unpack_block/load_by_block_number.rs @@ -1,225 +1,225 @@ -use crate::blocks::block::{Block, VrfBlock, VRF_BLOCK_BYTES}; -use crate::blocks::burn::BurnTransaction; -use crate::blocks::collateral::CollateralClaimTransaction; -use crate::blocks::genesis::GenesisTransaction; -use crate::blocks::issue_token::IssueTokenTransaction; -use crate::blocks::loan_payment::ContractPaymentTransaction; -use crate::blocks::loans::LoanContractTransaction; -use crate::blocks::marketing::MarketingTransaction; -use crate::blocks::nft::CreateNftTransaction; -use crate::blocks::rewards::RewardsTransaction; -use crate::blocks::swap::SwapTransaction; -use crate::blocks::token::CreateTokenTransaction; -use crate::blocks::transfer::TransferTransaction; -use crate::blocks::vanity::VanityAddressTransaction; -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::common::types::{ - Transaction, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, - GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, SWAP_TYPE, - TRANSFER_TYPE, VANITY_ADDRESS_TYPE, -}; -use crate::fs; -use crate::rpc::command_maps::get_bytes; -use crate::PathBuf; - -// The transaction body helpers keep the block parser aligned with the command map sizes. -fn transaction_body_len(txtype: u8) -> Result { - let total_len = get_bytes(txtype); - if total_len <= 1 { - return Err(format!("Unknown transaction type: {txtype}")); - } - - // get_bytes includes the transaction type byte; parser bodies start after - // that byte has already been consumed. - Ok(total_len - 1) -} - -fn transaction_body_slice( - binary_data: &[u8], - start: usize, - body_len: usize, -) -> Result<&[u8], String> { - // Slice with bounds checking so truncated block files fail cleanly instead - // of panicking during transaction parsing. - binary_data - .get(start..start + body_len) - .ok_or_else(|| format!("Truncated transaction body at offset {start}")) -} - -pub async fn load_block(block_number: u32) -> Result { - // Blocks are loaded from disk by height, then split back into the header and - // variable-length transaction payloads. - let ( - _network_name, - _padded_base_coin, - block_ext, - _torrent_path, - _wallet_path, - block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let file_name = PathBuf::from(block_path) - .join(format!("{block_number}.{block_ext}")) - .to_string_lossy() - .into_owned(); - - // Load the full block because this path reconstructs both the header and - // every transaction for validation or inspection. +use crate::blocks::block::{Block, VrfBlock, VRF_BLOCK_BYTES}; +use crate::blocks::burn::BurnTransaction; +use crate::blocks::collateral::CollateralClaimTransaction; +use crate::blocks::genesis::GenesisTransaction; +use crate::blocks::issue_token::IssueTokenTransaction; +use crate::blocks::loan_payment::ContractPaymentTransaction; +use crate::blocks::loans::LoanContractTransaction; +use crate::blocks::marketing::MarketingTransaction; +use crate::blocks::nft::CreateNftTransaction; +use crate::blocks::rewards::RewardsTransaction; +use crate::blocks::swap::SwapTransaction; +use crate::blocks::token::CreateTokenTransaction; +use crate::blocks::transfer::TransferTransaction; +use crate::blocks::vanity::VanityAddressTransaction; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::common::types::{ + Transaction, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, + GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, SWAP_TYPE, + TRANSFER_TYPE, VANITY_ADDRESS_TYPE, +}; +use crate::fs; +use crate::rpc::command_maps::get_bytes; +use crate::PathBuf; + +// The transaction body helpers keep the block parser aligned with the command map sizes. +fn transaction_body_len(txtype: u8) -> Result { + let total_len = get_bytes(txtype); + if total_len <= 1 { + return Err(format!("Unknown transaction type: {txtype}")); + } + + // get_bytes includes the transaction type byte; parser bodies start after + // that byte has already been consumed. + Ok(total_len - 1) +} + +fn transaction_body_slice( + binary_data: &[u8], + start: usize, + body_len: usize, +) -> Result<&[u8], String> { + // Slice with bounds checking so truncated block files fail cleanly instead + // of panicking during transaction parsing. + binary_data + .get(start..start + body_len) + .ok_or_else(|| format!("Truncated transaction body at offset {start}")) +} + +pub async fn load_block(block_number: u32) -> Result { + // Blocks are loaded from disk by height, then split back into the header and + // variable-length transaction payloads. + let ( + _network_name, + _padded_base_coin, + block_ext, + _torrent_path, + _wallet_path, + block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let file_name = PathBuf::from(block_path) + .join(format!("{block_number}.{block_ext}")) + .to_string_lossy() + .into_owned(); + + // Load the full block because this path reconstructs both the header and + // every transaction for validation or inspection. let binary_data = match fs::read(&file_name) { Ok(data) => data, Err(err) => { return Err(format!("Unable to read block {block_number}: {err:?}")); } }; - - if binary_data.len() < VRF_BLOCK_BYTES { - return Err("Unable to load block: binary data shorter than VrfBlock header".to_string()); - } - - let vrf_block = VrfBlock::from_bytes(&binary_data[0..VRF_BLOCK_BYTES]) - .await - .map_err(|e| e.to_string())?; - let mut i = VRF_BLOCK_BYTES; - let mut transactions: Vec = Vec::new(); - - while i < binary_data.len() { - // Each stored transaction begins with its type byte, followed by the fixed-size - // body for that transaction family. - let txtype = binary_data[i]; - i += 1; - let body_len = transaction_body_len(txtype)?; - let body = transaction_body_slice(&binary_data, i, body_len)?; - let transaction = match txtype { - GENESIS_TYPE => { - let genesis = Transaction::Genesis( - GenesisTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - genesis - } - REWARDS_TYPE => { - let rewards = Transaction::Rewards( - RewardsTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - rewards - } - TRANSFER_TYPE => { - let transfer = Transaction::Transfer( - TransferTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - transfer - } - BURN_TYPE => { - let burn = Transaction::Burn( - BurnTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - burn - } - CREATE_TOKEN_TYPE => { - let create_token = Transaction::Token( - CreateTokenTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - create_token - } - ISSUE_TOKEN_TYPE => { - let issue_token = Transaction::IssueToken( - IssueTokenTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - issue_token - } - CREATE_NFT_TYPE => { - let create_nft = Transaction::Nft( - CreateNftTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - create_nft - } - MARKETING_TYPE => { - let marketing = Transaction::Marketing( - MarketingTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - marketing - } - SWAP_TYPE => { - let swap = Transaction::Swap( - SwapTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - swap - } - LENDER_TYPE => { - let loan = Transaction::Lender( - LoanContractTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - loan - } - BORROWER_TYPE => { - let payment = Transaction::Borrower( - ContractPaymentTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - payment - } - COLLATERAL_TYPE => { - let collateral = Transaction::Collateral( - CollateralClaimTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - collateral - } - VANITY_ADDRESS_TYPE => { - let vanity = Transaction::Vanity( - VanityAddressTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - vanity - } - _ => { - return Err(format!("Unsupported transaction type: {txtype}")); - } - }; - transactions.push(transaction); - } - - let block = Block { - vrf_block, - transactions, - }; - - Ok(block) -} + + if binary_data.len() < VRF_BLOCK_BYTES { + return Err("Unable to load block: binary data shorter than VrfBlock header".to_string()); + } + + let vrf_block = VrfBlock::from_bytes(&binary_data[0..VRF_BLOCK_BYTES]) + .await + .map_err(|e| e.to_string())?; + let mut i = VRF_BLOCK_BYTES; + let mut transactions: Vec = Vec::new(); + + while i < binary_data.len() { + // Each stored transaction begins with its type byte, followed by the fixed-size + // body for that transaction family. + let txtype = binary_data[i]; + i += 1; + let body_len = transaction_body_len(txtype)?; + let body = transaction_body_slice(&binary_data, i, body_len)?; + let transaction = match txtype { + GENESIS_TYPE => { + let genesis = Transaction::Genesis( + GenesisTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + genesis + } + REWARDS_TYPE => { + let rewards = Transaction::Rewards( + RewardsTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + rewards + } + TRANSFER_TYPE => { + let transfer = Transaction::Transfer( + TransferTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + transfer + } + BURN_TYPE => { + let burn = Transaction::Burn( + BurnTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + burn + } + CREATE_TOKEN_TYPE => { + let create_token = Transaction::Token( + CreateTokenTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + create_token + } + ISSUE_TOKEN_TYPE => { + let issue_token = Transaction::IssueToken( + IssueTokenTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + issue_token + } + CREATE_NFT_TYPE => { + let create_nft = Transaction::Nft( + CreateNftTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + create_nft + } + MARKETING_TYPE => { + let marketing = Transaction::Marketing( + MarketingTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + marketing + } + SWAP_TYPE => { + let swap = Transaction::Swap( + SwapTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + swap + } + LENDER_TYPE => { + let loan = Transaction::Lender( + LoanContractTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + loan + } + BORROWER_TYPE => { + let payment = Transaction::Borrower( + ContractPaymentTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + payment + } + COLLATERAL_TYPE => { + let collateral = Transaction::Collateral( + CollateralClaimTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + collateral + } + VANITY_ADDRESS_TYPE => { + let vanity = Transaction::Vanity( + VanityAddressTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + vanity + } + _ => { + return Err(format!("Unsupported transaction type: {txtype}")); + } + }; + transactions.push(transaction); + } + + let block = Block { + vrf_block, + transactions, + }; + + Ok(block) +} diff --git a/src/records/unpack_block/unpack_header.rs b/src/records/unpack_block/unpack_header.rs index e4247f9..8aa2f35 100644 --- a/src/records/unpack_block/unpack_header.rs +++ b/src/records/unpack_block/unpack_header.rs @@ -1,27 +1,27 @@ -use crate::blocks::block::{VrfBlock, VRF_BLOCK_BYTES}; -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::AsyncReadExt; -use crate::File; -use crate::PathBuf; - -pub async fn load_block_header(block_number: u32) -> Result { - // Header-only loads avoid reading the full block when only chain metadata - // is needed. - let ( - _network_name, - _padded_base_coin, - block_ext, - _torrent_path, - _wallet_path, - block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let file_name = PathBuf::from(block_path) - .join(format!("{block_number}.{block_ext}")) - .to_string_lossy() - .into_owned(); +use crate::blocks::block::{VrfBlock, VRF_BLOCK_BYTES}; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::AsyncReadExt; +use crate::File; +use crate::PathBuf; + +pub async fn load_block_header(block_number: u32) -> Result { + // Header-only loads avoid reading the full block when only chain metadata + // is needed. + let ( + _network_name, + _padded_base_coin, + block_ext, + _torrent_path, + _wallet_path, + block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let file_name = PathBuf::from(block_path) + .join(format!("{block_number}.{block_ext}")) + .to_string_lossy() + .into_owned(); let file = match File::open(&file_name).await { Ok(file) => file, Err(err) => { @@ -30,23 +30,23 @@ pub async fn load_block_header(block_number: u32) -> Result { )); } }; - - let mut binary_data = Vec::with_capacity(VRF_BLOCK_BYTES); - // Only read the fixed VrfBlock prefix from the block file. - if let Err(err) = file - .take(VRF_BLOCK_BYTES as u64) - .read_to_end(&mut binary_data) - .await + + let mut binary_data = Vec::with_capacity(VRF_BLOCK_BYTES); + // Only read the fixed VrfBlock prefix from the block file. + if let Err(err) = file + .take(VRF_BLOCK_BYTES as u64) + .read_to_end(&mut binary_data) + .await { return Err(format!( "Error reading block header for height {block_number}: {err:?}" )); } - - // The stored header format is the same VrfBlock prefix used at the - // beginning of every block file. - match VrfBlock::from_bytes(&binary_data).await { - Ok(block) => Ok(block), - Err(err) => Err(format!("Error parsing block: {err:?}")), - } -} + + // The stored header format is the same VrfBlock prefix used at the + // beginning of every block file. + match VrfBlock::from_bytes(&binary_data).await { + Ok(block) => Ok(block), + Err(err) => Err(format!("Error parsing block: {err:?}")), + } +} diff --git a/src/records/wallet_registry/helpers.rs b/src/records/wallet_registry/helpers.rs index cfcb220..657137f 100644 --- a/src/records/wallet_registry/helpers.rs +++ b/src/records/wallet_registry/helpers.rs @@ -1,5 +1,5 @@ -use super::*; - +use super::*; + pub fn get_registered_pubkey(db: &Db, short_address: &[u8]) -> sled::Result>> { // The primary registry maps canonical short-address bytes to long public keys. let tree = db.open_tree(WALLET_REGISTRY_TREE)?; diff --git a/src/records/wallet_registry/mappings.rs b/src/records/wallet_registry/mappings.rs index b1d0ddb..c4b3c19 100644 --- a/src/records/wallet_registry/mappings.rs +++ b/src/records/wallet_registry/mappings.rs @@ -1,79 +1,79 @@ -use super::*; - -pub fn resolve_canonical_registered_short_address( - db: &Db, - address: &str, -) -> sled::Result> { - // Normalize long addresses, normal short addresses, and vanity-shaped - // addresses into the current-network short address format first. - let normalized = match Wallet::normalize_to_short_address(address) { - Some(address) => address, - None => return Ok(None), - }; - - // A direct registry hit means the input was already the canonical short - // address for a registered wallet. - let normalized_bytes = match Wallet::short_address_to_bytes(&normalized) { - Some(bytes) => bytes, - None => return Ok(None), - }; - - if short_address_exists(db, &normalized_bytes)? { - return Ok(Some(normalized)); - } - - // If the normalized text is a vanity address, resolve it through the - // vanity-to-owner tree and return the owner's canonical short address. - if let Some(owner_short_address) = resolve_owner_from_vanity_address(db, &normalized)? { - return Ok(Some(owner_short_address)); - } - - Ok(None) -} - -pub fn resolve_pubkey_from_short_address( - db: &Db, - short_address: &str, -) -> sled::Result>> { - // Resolve vanity aliases before loading the public key so signature checks - // always use the registered owner address. - let canonical_short_address = - match resolve_canonical_registered_short_address(db, short_address)? { - Some(address) => address, - None => return Ok(None), - }; - - let short_address_bytes = match Wallet::short_address_to_bytes(&canonical_short_address) { - Some(bytes) => bytes, - None => return Ok(None), - }; - - get_registered_pubkey(db, &short_address_bytes) -} - -pub fn require_canonical_registered_short_address( - db: &Db, - address: &str, - label: &str, -) -> Result { - // This path is used where vanity aliases are not allowed. The input may be - // normalized, but it must already equal the canonical registered address. - let normalized = Wallet::normalize_to_short_address(address) - .ok_or_else(|| format!("{label} is invalid."))?; - - let canonical = resolve_canonical_registered_short_address(db, &normalized) - .map_err(|err| format!("{label} lookup failed: {err}"))? - .ok_or_else(|| format!("{label} is not registered."))?; - - if canonical != normalized { - return Err(format!( - "{label} must use the canonical short address instead of a vanity alias." - )); - } - - Ok(canonical) -} - +use super::*; + +pub fn resolve_canonical_registered_short_address( + db: &Db, + address: &str, +) -> sled::Result> { + // Normalize long addresses, normal short addresses, and vanity-shaped + // addresses into the current-network short address format first. + let normalized = match Wallet::normalize_to_short_address(address) { + Some(address) => address, + None => return Ok(None), + }; + + // A direct registry hit means the input was already the canonical short + // address for a registered wallet. + let normalized_bytes = match Wallet::short_address_to_bytes(&normalized) { + Some(bytes) => bytes, + None => return Ok(None), + }; + + if short_address_exists(db, &normalized_bytes)? { + return Ok(Some(normalized)); + } + + // If the normalized text is a vanity address, resolve it through the + // vanity-to-owner tree and return the owner's canonical short address. + if let Some(owner_short_address) = resolve_owner_from_vanity_address(db, &normalized)? { + return Ok(Some(owner_short_address)); + } + + Ok(None) +} + +pub fn resolve_pubkey_from_short_address( + db: &Db, + short_address: &str, +) -> sled::Result>> { + // Resolve vanity aliases before loading the public key so signature checks + // always use the registered owner address. + let canonical_short_address = + match resolve_canonical_registered_short_address(db, short_address)? { + Some(address) => address, + None => return Ok(None), + }; + + let short_address_bytes = match Wallet::short_address_to_bytes(&canonical_short_address) { + Some(bytes) => bytes, + None => return Ok(None), + }; + + get_registered_pubkey(db, &short_address_bytes) +} + +pub fn require_canonical_registered_short_address( + db: &Db, + address: &str, + label: &str, +) -> Result { + // This path is used where vanity aliases are not allowed. The input may be + // normalized, but it must already equal the canonical registered address. + let normalized = Wallet::normalize_to_short_address(address) + .ok_or_else(|| format!("{label} is invalid."))?; + + let canonical = resolve_canonical_registered_short_address(db, &normalized) + .map_err(|err| format!("{label} lookup failed: {err}"))? + .ok_or_else(|| format!("{label} is not registered."))?; + + if canonical != normalized { + return Err(format!( + "{label} must use the canonical short address instead of a vanity alias." + )); + } + + Ok(canonical) +} + pub fn resolve_local_input_short_address(address: &str) -> Result { // CLI tools may receive long, short, or vanity addresses, so normalize the // user input before opening the local registry. @@ -96,100 +96,100 @@ pub fn resolve_local_input_short_address(address: &str) -> Result sled::Result, Vec)>> { - let tree = db.open_tree(WALLET_REGISTRY_TREE)?; - let mut wallets = Vec::new(); - - // Registry sync ships raw short-address/public-key pairs across peers. - for entry in tree.iter() { - let (short_address, public_key) = entry?; - wallets.push((short_address.to_vec(), public_key.to_vec())); - } - - Ok(wallets) -} - -pub fn get_registered_vanity_for_owner( - db: &Db, - owner_short_address: &str, -) -> sled::Result> { - // Owner lookups are canonicalized first so callers can pass either long or - // short owner addresses. - let canonical_owner = match resolve_canonical_registered_short_address(db, owner_short_address)? - { - Some(address) => address, - None => return Ok(None), - }; - - let owner_bytes = match Wallet::short_address_to_bytes(&canonical_owner) { - Some(bytes) => bytes, - None => return Ok(None), - }; - - let tree = db.open_tree(WALLET_VANITY_OWNER_TREE)?; - // Owner tree stores owner short-address bytes -> vanity-address bytes. - let Some(vanity_bytes) = tree.get(owner_bytes)?.map(|value| value.to_vec()) else { - return Ok(None); - }; - - Ok(Wallet::bytes_to_vanity_address(&vanity_bytes)) -} - -pub fn resolve_owner_from_vanity_address( - db: &Db, - vanity_address: &str, -) -> sled::Result> { - // Vanity text is normalized to the fixed byte payload before sled lookup. - let vanity_bytes = match Wallet::vanity_address_to_bytes(vanity_address) { - Some(bytes) => bytes, - None => return Ok(None), - }; - - let tree = db.open_tree(WALLET_VANITY_ADDRESS_TREE)?; - // Vanity tree stores vanity-address bytes -> owner short-address bytes. - let Some(owner_bytes) = tree.get(vanity_bytes)?.map(|value| value.to_vec()) else { - return Ok(None); - }; - - Ok(Wallet::bytes_to_short_address(&owner_bytes)) -} - -pub fn take_previous_vanity_for_txid( - db: &Db, - txid_hex: &str, -) -> sled::Result>> { - let tree = db.open_tree(WALLET_VANITY_ROLLBACK_TREE)?; - let key = decode(txid_hex).unwrap_or_default(); - // Taking removes the rollback marker so the same vanity undo cannot be - // replayed twice. - let Some(value) = tree.remove(key)?.map(|value| value.to_vec()) else { - return Ok(None); - }; - - if value.is_empty() { - // Empty bytes mean the owner had no prior vanity mapping. - return Ok(Some(None)); - } - - Ok(Some(Wallet::bytes_to_vanity_address(&value))) -} + +pub fn list_registered_wallets(db: &Db) -> sled::Result, Vec)>> { + let tree = db.open_tree(WALLET_REGISTRY_TREE)?; + let mut wallets = Vec::new(); + + // Registry sync ships raw short-address/public-key pairs across peers. + for entry in tree.iter() { + let (short_address, public_key) = entry?; + wallets.push((short_address.to_vec(), public_key.to_vec())); + } + + Ok(wallets) +} + +pub fn get_registered_vanity_for_owner( + db: &Db, + owner_short_address: &str, +) -> sled::Result> { + // Owner lookups are canonicalized first so callers can pass either long or + // short owner addresses. + let canonical_owner = match resolve_canonical_registered_short_address(db, owner_short_address)? + { + Some(address) => address, + None => return Ok(None), + }; + + let owner_bytes = match Wallet::short_address_to_bytes(&canonical_owner) { + Some(bytes) => bytes, + None => return Ok(None), + }; + + let tree = db.open_tree(WALLET_VANITY_OWNER_TREE)?; + // Owner tree stores owner short-address bytes -> vanity-address bytes. + let Some(vanity_bytes) = tree.get(owner_bytes)?.map(|value| value.to_vec()) else { + return Ok(None); + }; + + Ok(Wallet::bytes_to_vanity_address(&vanity_bytes)) +} + +pub fn resolve_owner_from_vanity_address( + db: &Db, + vanity_address: &str, +) -> sled::Result> { + // Vanity text is normalized to the fixed byte payload before sled lookup. + let vanity_bytes = match Wallet::vanity_address_to_bytes(vanity_address) { + Some(bytes) => bytes, + None => return Ok(None), + }; + + let tree = db.open_tree(WALLET_VANITY_ADDRESS_TREE)?; + // Vanity tree stores vanity-address bytes -> owner short-address bytes. + let Some(owner_bytes) = tree.get(vanity_bytes)?.map(|value| value.to_vec()) else { + return Ok(None); + }; + + Ok(Wallet::bytes_to_short_address(&owner_bytes)) +} + +pub fn take_previous_vanity_for_txid( + db: &Db, + txid_hex: &str, +) -> sled::Result>> { + let tree = db.open_tree(WALLET_VANITY_ROLLBACK_TREE)?; + let key = decode(txid_hex).unwrap_or_default(); + // Taking removes the rollback marker so the same vanity undo cannot be + // replayed twice. + let Some(value) = tree.remove(key)?.map(|value| value.to_vec()) else { + return Ok(None); + }; + + if value.is_empty() { + // Empty bytes mean the owner had no prior vanity mapping. + return Ok(Some(None)); + } + + Ok(Some(Wallet::bytes_to_vanity_address(&value))) +} diff --git a/src/records/wallet_registry/storage.rs b/src/records/wallet_registry/storage.rs index 9da43fc..ab29f98 100644 --- a/src/records/wallet_registry/storage.rs +++ b/src/records/wallet_registry/storage.rs @@ -1,76 +1,76 @@ -use super::*; - -pub fn register_short_address( - db: &Db, - short_address: &[u8], - public_key: &[u8], +use super::*; + +pub fn register_short_address( + db: &Db, + short_address: &[u8], + public_key: &[u8], ) -> sled::Result { let tree = db.open_tree(WALLET_REGISTRY_TREE)?; // Re-registering the same public key is harmless, but a different public // key for the same short address is a real conflict. if let Some(existing) = tree.get(short_address)? { - if existing.as_ref() == public_key { - return Ok(WalletRegistrationResult::AlreadyRegistered); - } - return Ok(WalletRegistrationResult::Conflict); - } - - tree.insert(short_address, public_key)?; - Ok(WalletRegistrationResult::Inserted) -} - -pub fn register_or_update_vanity_address( - db: &Db, - owner_short_address: &str, - vanity_address: &str, + if existing.as_ref() == public_key { + return Ok(WalletRegistrationResult::AlreadyRegistered); + } + return Ok(WalletRegistrationResult::Conflict); + } + + tree.insert(short_address, public_key)?; + Ok(WalletRegistrationResult::Inserted) +} + +pub fn register_or_update_vanity_address( + db: &Db, + owner_short_address: &str, + vanity_address: &str, ) -> sled::Result { // Vanity ownership is only valid for an already registered canonical owner. let normalized_owner = match resolve_canonical_registered_short_address(db, owner_short_address)? { - Some(address) => address, - None => return Ok(VanityRegistrationResult::OwnerNotRegistered), - }; + Some(address) => address, + None => return Ok(VanityRegistrationResult::OwnerNotRegistered), + }; let normalized_vanity = match Wallet::vanity_address_to_bytes(vanity_address) .and_then(|bytes| Wallet::bytes_to_vanity_address(&bytes)) { - Some(address) => address, - None => return Ok(VanityRegistrationResult::InvalidVanity), - }; - - let owner_bytes = match Wallet::short_address_to_bytes(&normalized_owner) { - Some(bytes) => bytes, - None => return Ok(VanityRegistrationResult::InvalidOwner), - }; - let vanity_bytes = match Wallet::vanity_address_to_bytes(&normalized_vanity) { - Some(bytes) => bytes, - None => return Ok(VanityRegistrationResult::InvalidVanity), - }; - + Some(address) => address, + None => return Ok(VanityRegistrationResult::InvalidVanity), + }; + + let owner_bytes = match Wallet::short_address_to_bytes(&normalized_owner) { + Some(bytes) => bytes, + None => return Ok(VanityRegistrationResult::InvalidOwner), + }; + let vanity_bytes = match Wallet::vanity_address_to_bytes(&normalized_vanity) { + Some(bytes) => bytes, + None => return Ok(VanityRegistrationResult::InvalidVanity), + }; + let owner_tree = db.open_tree(WALLET_VANITY_OWNER_TREE)?; let vanity_tree = db.open_tree(WALLET_VANITY_ADDRESS_TREE)?; // Check the owner tree first to see whether this is an insert, update, or // no-op re-registration of the same vanity. let existing_vanity_bytes = owner_tree.get(&owner_bytes)?.map(|value| value.to_vec()); - if let Some(existing_vanity_bytes) = &existing_vanity_bytes { - if *existing_vanity_bytes == vanity_bytes { - if let Some(existing_owner_bytes) = - vanity_tree.get(&vanity_bytes)?.map(|value| value.to_vec()) - { - if existing_owner_bytes == owner_bytes { - return Ok(VanityRegistrationResult::AlreadyRegistered); - } - } - } - } - - let result = if existing_vanity_bytes.is_some() { - VanityRegistrationResult::Updated - } else { - VanityRegistrationResult::Inserted - }; - + if let Some(existing_vanity_bytes) = &existing_vanity_bytes { + if *existing_vanity_bytes == vanity_bytes { + if let Some(existing_owner_bytes) = + vanity_tree.get(&vanity_bytes)?.map(|value| value.to_vec()) + { + if existing_owner_bytes == owner_bytes { + return Ok(VanityRegistrationResult::AlreadyRegistered); + } + } + } + } + + let result = if existing_vanity_bytes.is_some() { + VanityRegistrationResult::Updated + } else { + VanityRegistrationResult::Inserted + }; + if let Some(old_vanity_bytes) = existing_vanity_bytes { // Updating an owner removes the old reverse vanity -> owner mapping // before writing the new pair. @@ -80,29 +80,29 @@ pub fn register_or_update_vanity_address( // Keep both directions in sync so vanity resolution and wallet restoration // can each use the efficient lookup direction they need. vanity_tree.insert(&vanity_bytes, owner_bytes.clone())?; - owner_tree.insert(&owner_bytes, vanity_bytes.clone())?; - - Ok(result) -} - -pub fn remove_registered_vanity_for_owner( - db: &Db, - owner_short_address: &str, + owner_tree.insert(&owner_bytes, vanity_bytes.clone())?; + + Ok(result) +} + +pub fn remove_registered_vanity_for_owner( + db: &Db, + owner_short_address: &str, ) -> sled::Result { // Removing by owner canonicalizes first so long/short owner inputs remove // the same vanity mapping. let normalized_owner = - match resolve_canonical_registered_short_address(db, owner_short_address)? { - Some(address) => address, - None => return Ok(false), - }; - let owner_bytes = match Wallet::short_address_to_bytes(&normalized_owner) { - Some(bytes) => bytes, - None => return Ok(false), - }; - - let owner_tree = db.open_tree(WALLET_VANITY_OWNER_TREE)?; - let vanity_tree = db.open_tree(WALLET_VANITY_ADDRESS_TREE)?; + match resolve_canonical_registered_short_address(db, owner_short_address)? { + Some(address) => address, + None => return Ok(false), + }; + let owner_bytes = match Wallet::short_address_to_bytes(&normalized_owner) { + Some(bytes) => bytes, + None => return Ok(false), + }; + + let owner_tree = db.open_tree(WALLET_VANITY_OWNER_TREE)?; + let vanity_tree = db.open_tree(WALLET_VANITY_ADDRESS_TREE)?; let Some(vanity_bytes) = owner_tree.remove(&owner_bytes)?.map(|value| value.to_vec()) else { return Ok(false); @@ -110,23 +110,23 @@ pub fn remove_registered_vanity_for_owner( // The reverse vanity lookup must be removed with the owner mapping. vanity_tree.remove(&vanity_bytes)?; - - Ok(true) -} - -pub fn store_previous_vanity_for_txid( - db: &Db, - txid_hex: &str, - previous_vanity: Option<&str>, + + Ok(true) +} + +pub fn store_previous_vanity_for_txid( + db: &Db, + txid_hex: &str, + previous_vanity: Option<&str>, ) -> sled::Result<()> { let tree = db.open_tree(WALLET_VANITY_ROLLBACK_TREE)?; let key = decode(txid_hex).unwrap_or_default(); // A missing previous vanity is stored as an empty value so undo can // distinguish "known none" from "no rollback record exists". let value = match previous_vanity { - Some(vanity) => Wallet::vanity_address_to_bytes(vanity).unwrap_or_default(), - None => Vec::new(), - }; - let _ = tree.insert(key, value)?; - Ok(()) -} + Some(vanity) => Wallet::vanity_address_to_bytes(vanity).unwrap_or_default(), + None => Vec::new(), + }; + let _ = tree.insert(key, value)?; + Ok(()) +} diff --git a/src/records/wallet_registry/structs.rs b/src/records/wallet_registry/structs.rs index fdf2ab9..2ab1902 100644 --- a/src/records/wallet_registry/structs.rs +++ b/src/records/wallet_registry/structs.rs @@ -1,17 +1,17 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WalletRegistrationResult { - Inserted, - AlreadyRegistered, - Conflict, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VanityRegistrationResult { - Inserted, - Updated, - AlreadyRegistered, - Conflict, - OwnerNotRegistered, - InvalidOwner, - InvalidVanity, -} + Inserted, + AlreadyRegistered, + Conflict, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VanityRegistrationResult { + Inserted, + Updated, + AlreadyRegistered, + Conflict, + OwnerNotRegistered, + InvalidOwner, + InvalidVanity, +} diff --git a/src/rpc/client/block_hash_vote.rs b/src/rpc/client/block_hash_vote.rs index b8fef4d..e6473dc 100644 --- a/src/rpc/client/block_hash_vote.rs +++ b/src/rpc/client/block_hash_vote.rs @@ -1,5 +1,5 @@ use crate::encode; -use crate::records::memory::response_channels::{reserve_entry, Command}; +use crate::records::memory::response_channels::{reserve_entry_with_context, Command}; use crate::rpc::command_maps::RPC_BLOCK_HASH_AT_HEIGHT; use crate::rpc::responses::RpcResponse; use crate::{timeout, Arc, Duration, Mutex, TcpStream}; @@ -10,7 +10,12 @@ pub async fn request_block_hash_at_height( connections_key: String, height: u32, ) -> Result { - let (hashmap_key, _tx, rx) = reserve_entry(map).await; + let (hashmap_key, _tx, rx) = reserve_entry_with_context( + map, + Some(RPC_BLOCK_HASH_AT_HEIGHT), + Some(connections_key.clone()), + ) + .await; let mut message = vec![RPC_BLOCK_HASH_AT_HEIGHT]; message.extend_from_slice(&hashmap_key); diff --git a/src/rpc/client/syncing.rs b/src/rpc/client/syncing.rs index 3ea6064..6ad4a7c 100644 --- a/src/rpc/client/syncing.rs +++ b/src/rpc/client/syncing.rs @@ -4,8 +4,9 @@ use crate::log::{error, info, warn}; use crate::orphans::structs::OrphanCheckup2; use crate::orphans::sync_check::sync_checkup; use crate::records::block_height::get_block_height::get_height; -use crate::records::memory::response_channels::reserve_entry; +use crate::records::memory::response_channels::reserve_entry_with_context; use crate::records::memory::response_channels::Command; +use crate::rpc::command_maps::RPC_TORRENT_BY_HEIGHT; use crate::sled::Db; use crate::timeout; use crate::torrent::structs::Torrent; @@ -37,7 +38,12 @@ pub async fn node_syncing( // Walk forward block-by-block until the local chain catches up to // the advertised remote height. while remote_height >= local_height { - let (hashmap_key, _torrent_tx, torrent_rx) = reserve_entry(map.clone()).await; + let (hashmap_key, _torrent_tx, torrent_rx) = reserve_entry_with_context( + map.clone(), + Some(RPC_TORRENT_BY_HEIGHT), + Some(connections_key.clone()), + ) + .await; send_request_torrent_message( stream.clone(), local_height, diff --git a/src/rpc/command_maps.rs b/src/rpc/command_maps.rs index dd8f585..b84d15c 100644 --- a/src/rpc/command_maps.rs +++ b/src/rpc/command_maps.rs @@ -54,8 +54,6 @@ pub const RPC_WALLET_REGISTRY_SYNC: u8 = 39; pub const RPC_VANITY_LOOKUP: u8 = 40; pub const RPC_TORRENT_CANDIDATES: u8 = 41; pub const RPC_BLOCK_HASH_AT_HEIGHT: u8 = 42; -pub const RPC_NETWORK_MONITOR_ADD: u8 = 43; -pub const RPC_NETWORK_MONITOR_REMOVE: u8 = 44; pub const RPC_VANITY_OWNER_LOOKUP: u8 = 45; pub const RPC_REPLY: u8 = 255; pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024; diff --git a/src/rpc/commands/add_network_node.rs b/src/rpc/commands/add_network_node.rs index 7253831..a102bb8 100644 --- a/src/rpc/commands/add_network_node.rs +++ b/src/rpc/commands/add_network_node.rs @@ -47,6 +47,20 @@ pub async fn add_network_node( let added_signature = read_bytes_from_stream::read_signature_from_stream(connections_key, stream_locked.clone()) .await?; + let monitor_count = + read_bytes_from_stream::read_u16_from_stream(connections_key, stream_locked.clone()) + .await? as usize; + let mut monitors = Vec::with_capacity(monitor_count); + for _ in 0..monitor_count { + let monitor_bytes = read_bytes_from_stream::read_short_address_from_stream( + connections_key, + stream_locked.clone(), + ) + .await?; + if let Some(monitor) = Wallet::bytes_to_short_address(&monitor_bytes) { + monitors.push(monitor); + } + } let remote_ip = read_bytes_from_stream::read_caller_ip(stream_locked).await?; // NodeInfo owns the signature checks, local re-signing rules, and @@ -60,6 +74,7 @@ pub async fn add_network_node( modified_timestamp: added_timestamp, modified_signature: added_signature, }, + monitors, blocks_mined: 0_u8, remote_ip, db: db.clone(), diff --git a/src/rpc/commands/mod.rs b/src/rpc/commands/mod.rs index 0915ca5..3c9d1aa 100644 --- a/src/rpc/commands/mod.rs +++ b/src/rpc/commands/mod.rs @@ -18,8 +18,6 @@ pub mod largest_tx_fee; pub mod latest_block; pub mod memory_by_signature; pub mod network_info; -pub mod network_monitor_add; -pub mod network_monitor_remove; pub mod nft_list; pub mod nft_lookup; pub mod random_node; diff --git a/src/rpc/commands/network_info.rs b/src/rpc/commands/network_info.rs index 4af373f..ffa893b 100644 --- a/src/rpc/commands/network_info.rs +++ b/src/rpc/commands/network_info.rs @@ -1,97 +1,97 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::records::block_height::get_block_height::get_height; -use crate::records::memory::mempool::{largest_fee, total_transactions}; -use crate::records::unpack_block::unpack_header::load_block_header; -use crate::rpc::commands::structs::NetworkInfo; -use crate::rpc::responses::RpcResponse; -use crate::sled::Db; -use crate::Utc; - -async fn network_info_to_bytes(info: NetworkInfo) -> Vec { - // Serialize the network-info snapshot into the fixed RPC payload - // layout expected by peers and external clients. - let mut bytes = Vec::new(); - - let version_bytes = info.version.to_le_bytes(); - let network_bytes = info.network.as_bytes(); - let time_bytes = info.time.to_le_bytes(); - let prefix_bytes = info.wallet_prefix.as_bytes(); - let block_height_bytes = info.height.to_le_bytes(); - let next_block_difficulty_bytes = info.next_block_difficulty.to_le_bytes(); - let total_block_transactions_bytes = info.total_block_transactions.to_le_bytes(); - let transaction_response = total_transactions().await; - let RpcResponse::Binary(total_mempool_transactions_bytes) = transaction_response; - let fee_response = largest_fee().await; - let RpcResponse::Binary(largest_tx_fee_bytes) = fee_response; - - bytes.extend(version_bytes); - bytes.extend(network_bytes); - bytes.extend(time_bytes); - bytes.extend(prefix_bytes); - bytes.extend(block_height_bytes); - bytes.extend(next_block_difficulty_bytes); - bytes.extend(total_block_transactions_bytes); - bytes.extend(total_mempool_transactions_bytes); - bytes.extend(largest_tx_fee_bytes); - - bytes -} - -pub async fn request_network_info(db: &Db) -> RpcResponse { - // Build a point-in-time network snapshot from local chain state, - // mempool counts, fee stats, and network naming configuration. - let version = 1; - - let ( - network_name, - _wallet_type, - suffix, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let network = network_name.to_string(); - let wallet_prefix = suffix.to_uppercase(); - - let height = get_height(db); - - let block = match load_block_header(height).await { - Ok(data) => data, - Err(_) => { - let error = "Error: Calcaulting Network Info" - .to_string() - .as_bytes() - .to_vec(); - return RpcResponse::Binary(error); - } - }; - - let tree = db.open_tree("txid").unwrap(); - let total_block_transactions = tree.len() as u32; - - let time = Utc::now().timestamp() as u32; - - // These fields are filled inside `to_bytes` from their live RPC - // helpers so the serialization step stays self-contained. - let total_mempool_transactions = 0_u32; - let largest_tx_fee = 0_u64; - - let network_info = NetworkInfo { - version, - network, - time, - wallet_prefix, - height, - next_block_difficulty: block.unmined_block.next_block_difficulty, - total_block_transactions, - total_mempool_transactions, - largest_tx_fee, - }; - - let info_bytes = network_info_to_bytes(network_info).await; - - RpcResponse::Binary(info_bytes) -} +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::records::block_height::get_block_height::get_height; +use crate::records::memory::mempool::{largest_fee, total_transactions}; +use crate::records::unpack_block::unpack_header::load_block_header; +use crate::rpc::commands::structs::NetworkInfo; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::Utc; + +async fn network_info_to_bytes(info: NetworkInfo) -> Vec { + // Serialize the network-info snapshot into the fixed RPC payload + // layout expected by peers and external clients. + let mut bytes = Vec::new(); + + let version_bytes = info.version.to_le_bytes(); + let network_bytes = info.network.as_bytes(); + let time_bytes = info.time.to_le_bytes(); + let prefix_bytes = info.wallet_prefix.as_bytes(); + let block_height_bytes = info.height.to_le_bytes(); + let next_block_difficulty_bytes = info.next_block_difficulty.to_le_bytes(); + let total_block_transactions_bytes = info.total_block_transactions.to_le_bytes(); + let transaction_response = total_transactions().await; + let RpcResponse::Binary(total_mempool_transactions_bytes) = transaction_response; + let fee_response = largest_fee().await; + let RpcResponse::Binary(largest_tx_fee_bytes) = fee_response; + + bytes.extend(version_bytes); + bytes.extend(network_bytes); + bytes.extend(time_bytes); + bytes.extend(prefix_bytes); + bytes.extend(block_height_bytes); + bytes.extend(next_block_difficulty_bytes); + bytes.extend(total_block_transactions_bytes); + bytes.extend(total_mempool_transactions_bytes); + bytes.extend(largest_tx_fee_bytes); + + bytes +} + +pub async fn request_network_info(db: &Db) -> RpcResponse { + // Build a point-in-time network snapshot from local chain state, + // mempool counts, fee stats, and network naming configuration. + let version = 1; + + let ( + network_name, + _wallet_type, + suffix, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let network = network_name.to_string(); + let wallet_prefix = suffix.to_uppercase(); + + let height = get_height(db); + + let block = match load_block_header(height).await { + Ok(data) => data, + Err(_) => { + let error = "Error: Calcaulting Network Info" + .to_string() + .as_bytes() + .to_vec(); + return RpcResponse::Binary(error); + } + }; + + let tree = db.open_tree("txid").unwrap(); + let total_block_transactions = tree.len() as u32; + + let time = Utc::now().timestamp() as u32; + + // These fields are filled inside `to_bytes` from their live RPC + // helpers so the serialization step stays self-contained. + let total_mempool_transactions = 0_u32; + let largest_tx_fee = 0_u64; + + let network_info = NetworkInfo { + version, + network, + time, + wallet_prefix, + height, + next_block_difficulty: block.unmined_block.next_block_difficulty, + total_block_transactions, + total_mempool_transactions, + largest_tx_fee, + }; + + let info_bytes = network_info_to_bytes(network_info).await; + + RpcResponse::Binary(info_bytes) +} diff --git a/src/rpc/commands/route_reply.rs b/src/rpc/commands/route_reply.rs index 21559c5..041f9e1 100644 --- a/src/rpc/commands/route_reply.rs +++ b/src/rpc/commands/route_reply.rs @@ -1,7 +1,7 @@ use crate::log::warn; use crate::records::memory::enums::ClientType; use crate::records::memory::response_channels::{ - delete_entry, get_entry, is_retired_entry, Command, + delete_entry, get_entry, is_retired_entry, trace_entry, Command, }; use crate::rpc::command_maps::MAX_RPC_REPLY_BYTES; use crate::rpc::commands::bad_rpc_call; @@ -38,6 +38,7 @@ pub async fn route_reply( let tx_option = get_entry(map.clone(), uid).await; if let Some(tx) = tx_option { + let trace = trace_entry(map.clone(), uid).await; // Replies are payload-only after the UID and length prefix; the // waiting request task already knows how to interpret the bytes. let buffer = read_bytes_from_stream::read_usize_from_stream( @@ -49,6 +50,16 @@ pub async fn route_reply( if tx.send(buffer).await.is_err() { warn!("[rpc] reply receiver dropped before payload delivery: {uid:?}"); } + if let Some(trace) = trace { + if trace.age_ms >= 1_000 { + warn!( + "[rpc_trace] slow reply routed: uid={uid:?} cmd={:?} peer={} age_ms={} payload_bytes={message_length}", + trace.command, + trace.peer.as_deref().unwrap_or(connections_key), + trace.age_ms + ); + } + } delete_entry(map, uid).await; return Ok(()); @@ -59,7 +70,17 @@ pub async fn route_reply( // Retired UIDs are normal timeout fallout: the requester gave up, // but the peer eventually answered. Drain without penalizing so // startup/sync latency does not poison an otherwise valid peer. - warn!("[rpc] late reply arrived for retired uid: {uid:?}"); + let trace = trace_entry(map.clone(), uid).await; + if let Some(trace) = trace { + warn!( + "[rpc] late reply arrived for retired uid: {uid:?} cmd={:?} peer={} age_ms={} payload_bytes={message_length}", + trace.command, + trace.peer.as_deref().unwrap_or(connections_key), + trace.age_ms + ); + } else { + warn!("[rpc] late reply arrived for retired uid: {uid:?}"); + } } else { // Unknown, never-reserved UIDs can still indicate malformed or // forged traffic, so those keep counting as bad RPC behavior. diff --git a/src/rpc/commands/torrent_candidates.rs b/src/rpc/commands/torrent_candidates.rs index 7d37f42..703ed82 100644 --- a/src/rpc/commands/torrent_candidates.rs +++ b/src/rpc/commands/torrent_candidates.rs @@ -1,64 +1,64 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::orphans::snapshot_check::snapshot_height; -use crate::records::block_height::get_block_height::get_height; -use crate::rpc::responses::RpcResponse; -use crate::sled::Db; -use crate::torrent::torrenting_system::save_torrent::{list_staged_torrents, read_staged_torrent}; -use crate::{read, Path}; - -async fn canonical_torrent_bytes(height: u32) -> Option> { - // Canonical torrents are the saved `.torrent` files that match - // blocks already accepted into the local chain. - let ( - _network_name, - _padded_base_coin, - _block_ext, - torrent_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let file_path = Path::new(&torrent_path).join(format!("{height}.torrent")); - read(&file_path).await.ok() -} - -pub async fn request_torrent_candidates(db: &Db) -> RpcResponse { - // Send both canonical and staged torrent files from the last saved - // snapshot onward so a freshly connected peer can fill its staging area. - let start_height = snapshot_height(db).await.unwrap_or(0); - let current_height = get_height(db); - let mut candidates = Vec::new(); - - for height in start_height..=current_height { - if let Some(torrent_bytes) = canonical_torrent_bytes(height).await { - candidates.push((height, torrent_bytes)); - } - } - - if let Ok(staged_torrents) = list_staged_torrents().await { - for (height, staged_path) in staged_torrents { - if height < start_height { - continue; - } - // Staged torrents may not yet be canonical on this node, but - // peers still need them when evaluating short-range reorgs. - if let Ok(torrent_bytes) = read_staged_torrent(&staged_path).await { - candidates.push((height, torrent_bytes)); - } - } - } - - // Response layout: candidate count, then repeated height, byte - // length, and raw torrent bytes. - let mut response = Vec::new(); - response.extend_from_slice(&(candidates.len() as u32).to_le_bytes()); - for (height, torrent_bytes) in candidates { - response.extend_from_slice(&height.to_le_bytes()); - response.extend_from_slice(&(torrent_bytes.len() as u32).to_le_bytes()); - response.extend_from_slice(&torrent_bytes); - } - - RpcResponse::Binary(response) -} +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::orphans::snapshot_check::snapshot_height; +use crate::records::block_height::get_block_height::get_height; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::torrent::torrenting_system::save_torrent::{list_staged_torrents, read_staged_torrent}; +use crate::{read, Path}; + +async fn canonical_torrent_bytes(height: u32) -> Option> { + // Canonical torrents are the saved `.torrent` files that match + // blocks already accepted into the local chain. + let ( + _network_name, + _padded_base_coin, + _block_ext, + torrent_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let file_path = Path::new(&torrent_path).join(format!("{height}.torrent")); + read(&file_path).await.ok() +} + +pub async fn request_torrent_candidates(db: &Db) -> RpcResponse { + // Send both canonical and staged torrent files from the last saved + // snapshot onward so a freshly connected peer can fill its staging area. + let start_height = snapshot_height(db).await.unwrap_or(0); + let current_height = get_height(db); + let mut candidates = Vec::new(); + + for height in start_height..=current_height { + if let Some(torrent_bytes) = canonical_torrent_bytes(height).await { + candidates.push((height, torrent_bytes)); + } + } + + if let Ok(staged_torrents) = list_staged_torrents().await { + for (height, staged_path) in staged_torrents { + if height < start_height { + continue; + } + // Staged torrents may not yet be canonical on this node, but + // peers still need them when evaluating short-range reorgs. + if let Ok(torrent_bytes) = read_staged_torrent(&staged_path).await { + candidates.push((height, torrent_bytes)); + } + } + } + + // Response layout: candidate count, then repeated height, byte + // length, and raw torrent bytes. + let mut response = Vec::new(); + response.extend_from_slice(&(candidates.len() as u32).to_le_bytes()); + for (height, torrent_bytes) in candidates { + response.extend_from_slice(&height.to_le_bytes()); + response.extend_from_slice(&(torrent_bytes.len() as u32).to_le_bytes()); + response.extend_from_slice(&torrent_bytes); + } + + RpcResponse::Binary(response) +} diff --git a/src/rpc/commands/tx_count.rs b/src/rpc/commands/tx_count.rs index 3f5c520..ed89f94 100644 --- a/src/rpc/commands/tx_count.rs +++ b/src/rpc/commands/tx_count.rs @@ -1,150 +1,150 @@ -use crate::blocks::block::VRF_BLOCK_BYTES; -use crate::common::binary_conversions::binary_to_string; -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::common::types::{GENESIS_TYPE, REWARDS_TYPE, VANITY_ADDRESS_TYPE}; -use crate::rpc::command_maps; -use crate::rpc::responses::RpcResponse; -use crate::sled::Db; -use crate::PathBuf; -use crate::{AsyncReadExt, AsyncSeekExt, File, SeekFrom}; - -const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64; - -async fn lookup_transaction_location(db: &Db, txid: Vec) -> Result<(u64, u32, String), String> { - // The txid tree stores `block:index`, which is enough to locate the - // transaction inside the saved block file on disk. - let tree = db.open_tree("txid").unwrap(); - let value = match tree.get(txid) { - Ok(Some(result)) => result.to_vec(), - Ok(None) => { - return Err("error: Key not found".to_string()); - } - Err(_) => { - return Err("error: Error retrieving value".to_string()); - } - }; - - let value_str = binary_to_string(value); - // Stored txid locations are saved as ASCII `height:index`. - let parts: Vec<&str> = value_str.split(':').collect(); - - let block: u64 = parts[0].parse().unwrap_or_default(); - let position: u32 = parts[1].parse().unwrap_or_default(); - - let ( - _network_name, - _padded_base_coin, - block_ext, - _torrent_path, - _wallet_path, - block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let block_filename = PathBuf::from(block_path) - .join(format!("{block}.{block_ext}")) - .to_string_lossy() - .into_owned(); - Ok((block, position, block_filename)) -} - -async fn read_transaction_type(file_path: &str, position: u64) -> Option { - // Transaction offsets are located by repeatedly reading the type byte - // so the fixed encoded size for each saved transaction can be applied. - let mut file = File::open(file_path).await.ok()?; - file.seek(SeekFrom::Start(position)).await.ok()?; - - let mut transaction_type_byte = [0u8; 1]; - file.read_exact(&mut transaction_type_byte).await.ok()?; - - Some(transaction_type_byte[0]) -} - -async fn calculate_offset(file_path: &str, position: u32) -> Option> { - // Walk forward through the serialized block body until the requested - // transaction index is reached, then read exactly that transaction. - let mut total_bytes_to_skip: u64 = HEADER_SIZE; - let mut current_position: u32 = 1; - - let mut transaction_type = read_transaction_type(file_path, HEADER_SIZE).await?; - - while current_position < position { - // Transaction records are fixed-size by type, so the type byte - // determines how far to skip to reach the requested index. - let size = command_maps::get_bytes(transaction_type) as u64; - total_bytes_to_skip += size; - transaction_type = read_transaction_type(file_path, total_bytes_to_skip).await?; - current_position += 1; - } - - let size = command_maps::get_bytes(transaction_type) as u64; - let mut file = File::open(file_path).await.ok()?; - file.seek(SeekFrom::Start(total_bytes_to_skip)).await.ok()?; - - let mut transaction_bytes = vec![0u8; size as usize]; - file.read_exact(&mut transaction_bytes).await.ok()?; - - Some(transaction_bytes) -} - -fn reward_value(tx_bytes: &[u8]) -> u64 { - // Rewards are counted twice: total rewards and non-zero rewards. - if tx_bytes.len() < 13 { - return 0; - } - - let mut value_bytes = [0u8; 8]; - value_bytes.copy_from_slice(&tx_bytes[5..13]); - u64::from_le_bytes(value_bytes) -} - -pub async fn request_tx_count(db: &Db) -> RpcResponse { - // Count saved transactions by type directly from the txid index and - // block files so the response reflects the committed chain state. - let tree = db.open_tree("txid").unwrap(); - let mut totals = [0u64; (VANITY_ADDRESS_TYPE as usize) + 1]; - let mut non_zero = [0u64; (VANITY_ADDRESS_TYPE as usize) + 1]; - - for entry in tree.iter() { - let Ok((txid, _)) = entry else { - continue; - }; - - let Ok((_block, position, block_filename)) = - lookup_transaction_location(db, txid.to_vec()).await - else { - continue; - }; - - let Some(tx_bytes) = calculate_offset(&block_filename, position).await else { - continue; - }; - - let Some(&txtype) = tx_bytes.first() else { - continue; - }; - - if txtype == GENESIS_TYPE || txtype > VANITY_ADDRESS_TYPE { - continue; - } - - // Genesis is excluded from the public count table; every other - // known transaction type gets total and non-zero columns. - totals[txtype as usize] = totals[txtype as usize].saturating_add(1); - - if txtype == REWARDS_TYPE && reward_value(&tx_bytes) > 0 { - non_zero[txtype as usize] = non_zero[txtype as usize].saturating_add(1); - } - } - - let mut response = Vec::with_capacity(VANITY_ADDRESS_TYPE as usize * 17); - for txtype in 1u8..=VANITY_ADDRESS_TYPE { - // Response rows are type byte, total count, non-zero count. - response.push(txtype); - response.extend_from_slice(&totals[txtype as usize].to_le_bytes()); - response.extend_from_slice(&non_zero[txtype as usize].to_le_bytes()); - } - - RpcResponse::Binary(response) -} +use crate::blocks::block::VRF_BLOCK_BYTES; +use crate::common::binary_conversions::binary_to_string; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::common::types::{GENESIS_TYPE, REWARDS_TYPE, VANITY_ADDRESS_TYPE}; +use crate::rpc::command_maps; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::PathBuf; +use crate::{AsyncReadExt, AsyncSeekExt, File, SeekFrom}; + +const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64; + +async fn lookup_transaction_location(db: &Db, txid: Vec) -> Result<(u64, u32, String), String> { + // The txid tree stores `block:index`, which is enough to locate the + // transaction inside the saved block file on disk. + let tree = db.open_tree("txid").unwrap(); + let value = match tree.get(txid) { + Ok(Some(result)) => result.to_vec(), + Ok(None) => { + return Err("error: Key not found".to_string()); + } + Err(_) => { + return Err("error: Error retrieving value".to_string()); + } + }; + + let value_str = binary_to_string(value); + // Stored txid locations are saved as ASCII `height:index`. + let parts: Vec<&str> = value_str.split(':').collect(); + + let block: u64 = parts[0].parse().unwrap_or_default(); + let position: u32 = parts[1].parse().unwrap_or_default(); + + let ( + _network_name, + _padded_base_coin, + block_ext, + _torrent_path, + _wallet_path, + block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let block_filename = PathBuf::from(block_path) + .join(format!("{block}.{block_ext}")) + .to_string_lossy() + .into_owned(); + Ok((block, position, block_filename)) +} + +async fn read_transaction_type(file_path: &str, position: u64) -> Option { + // Transaction offsets are located by repeatedly reading the type byte + // so the fixed encoded size for each saved transaction can be applied. + let mut file = File::open(file_path).await.ok()?; + file.seek(SeekFrom::Start(position)).await.ok()?; + + let mut transaction_type_byte = [0u8; 1]; + file.read_exact(&mut transaction_type_byte).await.ok()?; + + Some(transaction_type_byte[0]) +} + +async fn calculate_offset(file_path: &str, position: u32) -> Option> { + // Walk forward through the serialized block body until the requested + // transaction index is reached, then read exactly that transaction. + let mut total_bytes_to_skip: u64 = HEADER_SIZE; + let mut current_position: u32 = 1; + + let mut transaction_type = read_transaction_type(file_path, HEADER_SIZE).await?; + + while current_position < position { + // Transaction records are fixed-size by type, so the type byte + // determines how far to skip to reach the requested index. + let size = command_maps::get_bytes(transaction_type) as u64; + total_bytes_to_skip += size; + transaction_type = read_transaction_type(file_path, total_bytes_to_skip).await?; + current_position += 1; + } + + let size = command_maps::get_bytes(transaction_type) as u64; + let mut file = File::open(file_path).await.ok()?; + file.seek(SeekFrom::Start(total_bytes_to_skip)).await.ok()?; + + let mut transaction_bytes = vec![0u8; size as usize]; + file.read_exact(&mut transaction_bytes).await.ok()?; + + Some(transaction_bytes) +} + +fn reward_value(tx_bytes: &[u8]) -> u64 { + // Rewards are counted twice: total rewards and non-zero rewards. + if tx_bytes.len() < 13 { + return 0; + } + + let mut value_bytes = [0u8; 8]; + value_bytes.copy_from_slice(&tx_bytes[5..13]); + u64::from_le_bytes(value_bytes) +} + +pub async fn request_tx_count(db: &Db) -> RpcResponse { + // Count saved transactions by type directly from the txid index and + // block files so the response reflects the committed chain state. + let tree = db.open_tree("txid").unwrap(); + let mut totals = [0u64; (VANITY_ADDRESS_TYPE as usize) + 1]; + let mut non_zero = [0u64; (VANITY_ADDRESS_TYPE as usize) + 1]; + + for entry in tree.iter() { + let Ok((txid, _)) = entry else { + continue; + }; + + let Ok((_block, position, block_filename)) = + lookup_transaction_location(db, txid.to_vec()).await + else { + continue; + }; + + let Some(tx_bytes) = calculate_offset(&block_filename, position).await else { + continue; + }; + + let Some(&txtype) = tx_bytes.first() else { + continue; + }; + + if txtype == GENESIS_TYPE || txtype > VANITY_ADDRESS_TYPE { + continue; + } + + // Genesis is excluded from the public count table; every other + // known transaction type gets total and non-zero columns. + totals[txtype as usize] = totals[txtype as usize].saturating_add(1); + + if txtype == REWARDS_TYPE && reward_value(&tx_bytes) > 0 { + non_zero[txtype as usize] = non_zero[txtype as usize].saturating_add(1); + } + } + + let mut response = Vec::with_capacity(VANITY_ADDRESS_TYPE as usize * 17); + for txtype in 1u8..=VANITY_ADDRESS_TYPE { + // Response rows are type byte, total count, non-zero count. + response.push(txtype); + response.extend_from_slice(&totals[txtype as usize].to_le_bytes()); + response.extend_from_slice(&non_zero[txtype as usize].to_le_bytes()); + } + + RpcResponse::Binary(response) +} diff --git a/src/rpc/server/rpc_command_loop.rs b/src/rpc/server/rpc_command_loop.rs index b8be218..00fd4ae 100644 --- a/src/rpc/server/rpc_command_loop.rs +++ b/src/rpc/server/rpc_command_loop.rs @@ -1,5 +1,6 @@ use crate::common::binary_conversions::binary_to_string; use crate::encode; +use crate::log::warn; use crate::records::memory::enums::ClientType; use crate::records::memory::response_channels::Command; use crate::rpc::server::command_loop_state::next_incoming_command; @@ -9,6 +10,7 @@ use crate::sled::Db; use crate::wallets::structures::Wallet; use crate::Arc; use crate::AsyncWriteExt; +use crate::Instant; use crate::Mutex; use crate::TcpStream; @@ -29,6 +31,7 @@ pub async fn start_loop( let command = incoming_command.command; let ip = incoming_command.ip; let client_type = incoming_command.client_type; + let command_started = Instant::now(); match command { 1 => { @@ -813,34 +816,6 @@ pub async fn start_loop( .send(&stream_locked, Some(&connections_key), uid) .await; } - 43 => { - // add a monitor edge in the network map - let (uid, result) = commands::network_monitor_add::network_monitor_add( - &connections_key, - stream_locked.clone(), - &db, - wallet.clone(), - map.clone(), - ) - .await?; - result - .send(&stream_locked, Some(&connections_key), uid) - .await; - } - 44 => { - // remove a monitor edge in the network map - let (uid, result) = commands::network_monitor_remove::network_monitor_remove( - &connections_key, - stream_locked.clone(), - &db, - wallet.clone(), - map.clone(), - ) - .await?; - result - .send(&stream_locked, Some(&connections_key), uid) - .await; - } 45 => { // request the canonical short address that owns a registered vanity address let (uid, _) = read_bytes_from_stream::read_uid_from_stream( @@ -893,6 +868,13 @@ pub async fn start_loop( } } + let elapsed_ms = command_started.elapsed().as_millis(); + if elapsed_ms >= 1_000 { + warn!( + "[rpc_trace] slow handler: cmd={command} peer={connections_key} elapsed_ms={elapsed_ms}" + ); + } + // Wallet-backed client sessions are short-lived request/response // connections, so close them after each non-reply command. if client_type == ClientType::Client && command != command_maps::RPC_REPLY { diff --git a/src/startup/connections.rs b/src/startup/connections.rs index 19ee910..179c6d4 100644 --- a/src/startup/connections.rs +++ b/src/startup/connections.rs @@ -1,18 +1,18 @@ -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}; +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::sled::Db; use crate::sleep; use crate::wallets::structures::Wallet; use crate::Arc; -use crate::Duration; -use crate::Mutex; - +use crate::Duration; +use crate::Mutex; + pub async fn handle_connections( db: Db, wallet: Arc, @@ -34,22 +34,22 @@ pub async fn handle_connections( // 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 { - // build the outbound handshake request using cloned - // shared state so each attempt can run independently - let db_clone = db.clone(); - + 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(); + // parse the configured peer string once before spawning - // the outbound connection attempt - let socket_address = server.parse().expect("Failed to parse the socket address"); - - // Clone the Arc for use in other async functions - let map_clone = Arc::clone(&map); - - let first: bool = true; - let connect_params = Connect { + // the outbound connection attempt + let socket_address = server.parse().expect("Failed to parse the socket address"); + + // Clone the Arc for use in other async functions + let map_clone = Arc::clone(&map); + + let first: bool = true; + let connect_params = Connect { addr: socket_address, db: db_clone, node_ip: server.to_string(), @@ -57,33 +57,33 @@ pub async fn handle_connections( map: map_clone, first, }; - - let err_string = match connect_and_handshake(connect_params).await { - Ok(()) => { - info!("Connected to {server}"); - return Ok(()); - } - 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); - } + + let err_string = match connect_and_handshake(connect_params).await { + Ok(()) => { + info!("Connected to {server}"); + return Ok(()); + } + 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}"); 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 { + 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(()) +} diff --git a/src/startup/daemonize.rs b/src/startup/daemonize.rs index 50f3c25..f20c8ac 100644 --- a/src/startup/daemonize.rs +++ b/src/startup/daemonize.rs @@ -1,236 +1,236 @@ -#[cfg(unix)] -use crate::common::network_paths_and_settings::block_extension_and_paths; -#[cfg(unix)] -use crate::env; -#[cfg(unix)] -use crate::lazy_static; -#[cfg(unix)] -use crate::log::{error, info, warn}; -#[cfg(unix)] -use crate::sled::Db; -#[cfg(unix)] -use crate::task; -#[cfg(unix)] -use crate::PathBuf; -#[cfg(unix)] -use nix::sys::signal::{kill, Signal}; -#[cfg(unix)] -use nix::unistd::{daemon, Pid}; -#[cfg(unix)] -use std::sync::Mutex as StdMutex; - -#[cfg(unix)] -lazy_static! { - static ref PID_FILE_PATH: StdMutex> = StdMutex::new(None); -} - -#[cfg(unix)] -fn should_daemonize() -> bool { - // --foreground keeps Linux startup attached to the terminal for debugging. - !env::args().any(|arg| arg == "--foreground") -} - -#[cfg(unix)] -fn read_pid(pid_path: &PathBuf) -> Result> { - // PID files are plain text so shell tools can inspect them too. - let pid_contents = std::fs::read_to_string(pid_path)?; - Ok(pid_contents.trim().parse::()?) -} - -#[cfg(unix)] -fn pid_file_path() -> PathBuf { - // PID files live under the network-scoped db path so testnet/mainnet never collide. - let ( - _network_name, - _padded_base_coin, - _suffix, - _torrent_path, - _wallet_path, - _block_path, - db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - PathBuf::from(db_path).join("contractless.pid") -} - -#[cfg(unix)] -fn existing_process_running(pid_path: &PathBuf) -> Result> { - // The PID file is treated as the source of truth for duplicate-start checks. - if !pid_path.exists() { - return Ok(false); - } - - let pid = read_pid(pid_path)?; - - match kill(Pid::from_raw(pid), None) { - Ok(_) => Ok(true), - Err(_) => Ok(false), - } -} - -#[cfg(unix)] -fn remove_pid_file_if_present(pid_path: &PathBuf) { - // Missing PID files are harmless during cleanup. - let _ = std::fs::remove_file(pid_path); -} - -#[cfg(unix)] -fn write_pid_file(pid_path: &PathBuf) -> Result<(), Box> { - // The PID file is written only after the process has detached so later control - // commands point at the background daemon rather than the original shell session. - if let Some(parent) = pid_path.parent() { - std::fs::create_dir_all(parent)?; - } - - let pid = std::process::id().to_string(); - std::fs::write(pid_path, pid)?; - - let mut slot = PID_FILE_PATH.lock().expect("failed to lock pid file slot"); - *slot = Some(pid_path.clone()); - - Ok(()) -} - -#[cfg(unix)] -pub fn remove_registered_pid_file() { - let pid_path = { - let slot = PID_FILE_PATH.lock().expect("failed to lock pid file slot"); - slot.clone() - }; - - if let Some(path) = pid_path { - remove_pid_file_if_present(&path); - } -} - -#[cfg(unix)] -pub fn daemonize_after_wallet_prompt() -> Result> { - // Linux waits until after the wallet prompt to detach so the encryption key never - // needs to be handed off to a second process. - if !should_daemonize() { - return Ok(false); - } - - let pid_path = pid_file_path(); - // Refuse to detach if an active process already owns the network-scoped PID file. - if existing_process_running(&pid_path)? { - return Err(format!( - "Contractless is already running. Remove stale PID file if needed: {}", - pid_path.display() - ) - .into()); - } - - remove_pid_file_if_present(&pid_path); - // daemon(true, false) keeps the working directory but redirects stdio for background mode. - daemon(true, false)?; - write_pid_file(&pid_path)?; - info!("Daemonized node process with pid {}", std::process::id()); - - Ok(true) -} - -#[cfg(unix)] -pub fn handle_control_command() -> Result> { - let args: Vec = env::args().skip(1).collect(); - let pid_path = pid_file_path(); - - // The control commands operate through the PID file rather than by interacting - // with a terminal session, so they work even after the node detaches. - if args.iter().any(|arg| arg == "--status") { - if existing_process_running(&pid_path)? { - let pid = read_pid(&pid_path)?; - println!("Contractless is running with pid {pid}."); - } else if pid_path.exists() { - println!( - "Contractless is not running, but a stale PID file exists at {}.", - pid_path.display() - ); - } else { - println!("Contractless is not running."); - } - return Ok(true); - } - - if args.iter().any(|arg| arg == "--stop") { - if !pid_path.exists() { - println!("Contractless is not running."); - return Ok(true); - } - - let pid = read_pid(&pid_path)?; - // SIGTERM triggers install_shutdown_cleanup in the daemon process. - match kill(Pid::from_raw(pid), Some(Signal::SIGTERM)) { - Ok(_) => { - println!("Sent SIGTERM to Contractless process {pid}."); +#[cfg(unix)] +use crate::common::network_paths_and_settings::block_extension_and_paths; +#[cfg(unix)] +use crate::env; +#[cfg(unix)] +use crate::lazy_static; +#[cfg(unix)] +use crate::log::{error, info, warn}; +#[cfg(unix)] +use crate::sled::Db; +#[cfg(unix)] +use crate::task; +#[cfg(unix)] +use crate::PathBuf; +#[cfg(unix)] +use nix::sys::signal::{kill, Signal}; +#[cfg(unix)] +use nix::unistd::{daemon, Pid}; +#[cfg(unix)] +use std::sync::Mutex as StdMutex; + +#[cfg(unix)] +lazy_static! { + static ref PID_FILE_PATH: StdMutex> = StdMutex::new(None); +} + +#[cfg(unix)] +fn should_daemonize() -> bool { + // --foreground keeps Linux startup attached to the terminal for debugging. + !env::args().any(|arg| arg == "--foreground") +} + +#[cfg(unix)] +fn read_pid(pid_path: &PathBuf) -> Result> { + // PID files are plain text so shell tools can inspect them too. + let pid_contents = std::fs::read_to_string(pid_path)?; + Ok(pid_contents.trim().parse::()?) +} + +#[cfg(unix)] +fn pid_file_path() -> PathBuf { + // PID files live under the network-scoped db path so testnet/mainnet never collide. + let ( + _network_name, + _padded_base_coin, + _suffix, + _torrent_path, + _wallet_path, + _block_path, + db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + PathBuf::from(db_path).join("contractless.pid") +} + +#[cfg(unix)] +fn existing_process_running(pid_path: &PathBuf) -> Result> { + // The PID file is treated as the source of truth for duplicate-start checks. + if !pid_path.exists() { + return Ok(false); + } + + let pid = read_pid(pid_path)?; + + match kill(Pid::from_raw(pid), None) { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } +} + +#[cfg(unix)] +fn remove_pid_file_if_present(pid_path: &PathBuf) { + // Missing PID files are harmless during cleanup. + let _ = std::fs::remove_file(pid_path); +} + +#[cfg(unix)] +fn write_pid_file(pid_path: &PathBuf) -> Result<(), Box> { + // The PID file is written only after the process has detached so later control + // commands point at the background daemon rather than the original shell session. + if let Some(parent) = pid_path.parent() { + std::fs::create_dir_all(parent)?; + } + + let pid = std::process::id().to_string(); + std::fs::write(pid_path, pid)?; + + let mut slot = PID_FILE_PATH.lock().expect("failed to lock pid file slot"); + *slot = Some(pid_path.clone()); + + Ok(()) +} + +#[cfg(unix)] +pub fn remove_registered_pid_file() { + let pid_path = { + let slot = PID_FILE_PATH.lock().expect("failed to lock pid file slot"); + slot.clone() + }; + + if let Some(path) = pid_path { + remove_pid_file_if_present(&path); + } +} + +#[cfg(unix)] +pub fn daemonize_after_wallet_prompt() -> Result> { + // Linux waits until after the wallet prompt to detach so the encryption key never + // needs to be handed off to a second process. + if !should_daemonize() { + return Ok(false); + } + + let pid_path = pid_file_path(); + // Refuse to detach if an active process already owns the network-scoped PID file. + if existing_process_running(&pid_path)? { + return Err(format!( + "Contractless is already running. Remove stale PID file if needed: {}", + pid_path.display() + ) + .into()); + } + + remove_pid_file_if_present(&pid_path); + // daemon(true, false) keeps the working directory but redirects stdio for background mode. + daemon(true, false)?; + write_pid_file(&pid_path)?; + info!("Daemonized node process with pid {}", std::process::id()); + + Ok(true) +} + +#[cfg(unix)] +pub fn handle_control_command() -> Result> { + let args: Vec = env::args().skip(1).collect(); + let pid_path = pid_file_path(); + + // The control commands operate through the PID file rather than by interacting + // with a terminal session, so they work even after the node detaches. + if args.iter().any(|arg| arg == "--status") { + if existing_process_running(&pid_path)? { + let pid = read_pid(&pid_path)?; + println!("Contractless is running with pid {pid}."); + } else if pid_path.exists() { + println!( + "Contractless is not running, but a stale PID file exists at {}.", + pid_path.display() + ); + } else { + println!("Contractless is not running."); + } + return Ok(true); + } + + if args.iter().any(|arg| arg == "--stop") { + if !pid_path.exists() { + println!("Contractless is not running."); + return Ok(true); + } + + let pid = read_pid(&pid_path)?; + // SIGTERM triggers install_shutdown_cleanup in the daemon process. + match kill(Pid::from_raw(pid), Some(Signal::SIGTERM)) { + Ok(_) => { + println!("Sent SIGTERM to Contractless process {pid}."); } Err(err) => { remove_pid_file_if_present(&pid_path); return Err(format!("Failed to stop Contractless process {pid}: {err}").into()); } } - - return Ok(true); - } - - Ok(false) -} - -#[cfg(unix)] -pub fn install_shutdown_cleanup(db: Db) { - task::spawn(async move { - use tokio::signal::unix::{signal, SignalKind}; - - // The Unix shutdown handler flushes sled and removes the PID file so stop, - // reboot, and signal-driven exits leave the node in a clean state. - let mut sigterm = match signal(SignalKind::terminate()) { - Ok(stream) => stream, - Err(err) => { - error!("Failed to register SIGTERM handler: {err}"); - return; - } - }; - - let mut sigint = match signal(SignalKind::interrupt()) { - Ok(stream) => stream, - Err(err) => { - error!("Failed to register SIGINT handler: {err}"); - return; - } - }; - - tokio::select! { - _ = sigterm.recv() => { - warn!("Received SIGTERM, shutting down."); - } - _ = sigint.recv() => { - warn!("Received SIGINT, shutting down."); - } - } - - if let Err(err) = db.flush_async().await { - error!("Failed to flush sled during shutdown: {err}"); - } - - // Removing the PID file here lets the next startup proceed without manual cleanup. - remove_registered_pid_file(); - std::process::exit(0); - }); -} - -#[cfg(not(unix))] -pub fn daemonize_after_wallet_prompt() -> Result> { - Ok(false) -} - -#[cfg(not(unix))] -pub fn handle_control_command() -> Result> { - Ok(false) -} - -#[cfg(not(unix))] -pub fn install_shutdown_cleanup(_db: crate::sled::Db) {} - -#[cfg(not(unix))] -pub fn remove_registered_pid_file() {} + + return Ok(true); + } + + Ok(false) +} + +#[cfg(unix)] +pub fn install_shutdown_cleanup(db: Db) { + task::spawn(async move { + use tokio::signal::unix::{signal, SignalKind}; + + // The Unix shutdown handler flushes sled and removes the PID file so stop, + // reboot, and signal-driven exits leave the node in a clean state. + let mut sigterm = match signal(SignalKind::terminate()) { + Ok(stream) => stream, + Err(err) => { + error!("Failed to register SIGTERM handler: {err}"); + return; + } + }; + + let mut sigint = match signal(SignalKind::interrupt()) { + Ok(stream) => stream, + Err(err) => { + error!("Failed to register SIGINT handler: {err}"); + return; + } + }; + + tokio::select! { + _ = sigterm.recv() => { + warn!("Received SIGTERM, shutting down."); + } + _ = sigint.recv() => { + warn!("Received SIGINT, shutting down."); + } + } + + if let Err(err) = db.flush_async().await { + error!("Failed to flush sled during shutdown: {err}"); + } + + // Removing the PID file here lets the next startup proceed without manual cleanup. + remove_registered_pid_file(); + std::process::exit(0); + }); +} + +#[cfg(not(unix))] +pub fn daemonize_after_wallet_prompt() -> Result> { + Ok(false) +} + +#[cfg(not(unix))] +pub fn handle_control_command() -> Result> { + Ok(false) +} + +#[cfg(not(unix))] +pub fn install_shutdown_cleanup(_db: crate::sled::Db) {} + +#[cfg(not(unix))] +pub fn remove_registered_pid_file() {} diff --git a/src/startup/network_broadcast.rs b/src/startup/network_broadcast.rs index dce7962..b9c1c43 100644 --- a/src/startup/network_broadcast.rs +++ b/src/startup/network_broadcast.rs @@ -1,15 +1,13 @@ use crate::common::binary_conversions::{binary_to_ip, binary_to_string, ip_to_binary}; use crate::common::network_startup::get_ip_and_port; -use crate::records::memory::network_mapping::monitor::MONITOR_ACTION_ADD; use crate::records::memory::network_mapping::structs::{ - AddAddressParams, MonitorAddressParams, SignedMonitorEdit, SignedNodeEdit, - NODE_ADDED_BY_OFFSET, NODE_ADDED_SIGNATURE_OFFSET, NODE_ADDED_TIMESTAMP_OFFSET, - NODE_BLOCKS_MINED_OFFSET, NODE_DELETED_BLOCK_OFFSET, NODE_DELETED_TIMESTAMP_OFFSET, - NODE_IP_OFFSET, NODE_MONITOR_COUNT_OFFSET, NODE_RECORD_FIXED_BYTES, + AddAddressParams, SignedNodeEdit, NODE_ADDED_BY_OFFSET, NODE_ADDED_SIGNATURE_OFFSET, + NODE_ADDED_TIMESTAMP_OFFSET, NODE_BLOCKS_MINED_OFFSET, NODE_DELETED_BLOCK_OFFSET, + NODE_DELETED_TIMESTAMP_OFFSET, NODE_IP_OFFSET, NODE_MONITOR_COUNT_OFFSET, + NODE_RECORD_FIXED_BYTES, }; use crate::records::memory::network_mapping::NodeInfo; use crate::records::memory::response_channels::{reserve_entry, Command}; -use crate::records::memory::structs::Connection; use crate::rpc::command_maps::{RPC_ADD_NETWORK_NODE, RPC_REQUEST_NODE_LIST}; use crate::rpc::responses::RpcResponse; use crate::sled::Db; @@ -21,55 +19,6 @@ use crate::Mutex; use crate::TcpStream; use crate::Utc; -async fn record_local_monitor_for_peer( - connections_key: &str, - command_map: Arc>, - db: &Db, - wallet: Arc, -) { - let Some(peer_wallet_bytes) = Connection::get_wallet_for_connection_key(connections_key).await - else { - return; - }; - let Some(monitored_address) = Wallet::public_key_bytes_to_short_address(&peer_wallet_bytes) - else { - return; - }; - let monitoring_address = wallet.saved.short_address.clone(); - if monitored_address == monitoring_address { - return; - } - let Some((target_ip, _)) = connections_key.rsplit_once(':') else { - return; - }; - let timestamp = Utc::now().timestamp_millis() as u64; - let signature = NodeInfo::monitor_signature( - MONITOR_ACTION_ADD, - &monitored_address, - &monitoring_address, - target_ip, - timestamp, - &wallet, - ) - .await; - let _ = NodeInfo::add_monitor(MonitorAddressParams { - map: command_map, - edit: SignedMonitorEdit { - action: MONITOR_ACTION_ADD, - monitored_address, - monitoring_address, - target_ip: target_ip.to_string(), - modified_timestamp: timestamp, - modified_signature: signature, - }, - remote_ip: String::new(), - db: db.clone(), - wallet, - connections_key: connections_key.to_string(), - }) - .await; -} - pub async fn announce_self_to_network( unlocked_stream: Arc>, address: &str, @@ -114,6 +63,7 @@ pub async fn announce_self_to_network( message.extend_from_slice(&modified_by_bytes); message.extend_from_slice(&modified_timestamp_bytes); message.extend_from_slice(&modified_signature_bytes); + message.extend_from_slice(&0u16.to_le_bytes()); RpcResponse::send_raw(&unlocked_stream, Some(connections_key), &message).await; @@ -141,7 +91,6 @@ pub async fn announce_self_to_network( connections_key, ) .await?; - record_local_monitor_for_peer(connections_key, command_map, db, wallet).await; Ok(()) } @@ -215,6 +164,16 @@ pub async fn get_network_mapping( .unwrap(), ); let remote_ip = ""; + let mut monitors = Vec::with_capacity(monitor_count); + for monitor_index in 0..monitor_count { + let start = + NODE_RECORD_FIXED_BYTES + monitor_index * Wallet::SHORT_ADDRESS_BYTES_LENGTH; + let end = start + Wallet::SHORT_ADDRESS_BYTES_LENGTH; + if let Some(monitor) = Wallet::bytes_to_short_address(&chunk[start..end]) { + monitors.push(monitor); + } + } + // Add records are imported through NodeInfo so local validation/signing rules stay central. NodeInfo::add_address(AddAddressParams { map: command_map.clone(), @@ -225,6 +184,7 @@ pub async fn get_network_mapping( modified_timestamp: added_timestamp, modified_signature: added_signature, }, + monitors, blocks_mined, remote_ip: remote_ip.to_string(), db: db.clone(), @@ -233,17 +193,6 @@ pub async fn get_network_mapping( }) .await; - let mut monitors = Vec::with_capacity(monitor_count); - for monitor_index in 0..monitor_count { - let start = - NODE_RECORD_FIXED_BYTES + monitor_index * Wallet::SHORT_ADDRESS_BYTES_LENGTH; - let end = start + Wallet::SHORT_ADDRESS_BYTES_LENGTH; - if let Some(monitor) = Wallet::bytes_to_short_address(&chunk[start..end]) { - monitors.push(monitor); - } - } - NodeInfo::set_monitors_from_mapping(&address, monitors).await; - if deleted_timestamp > 0 { NodeInfo::set_deleted_metadata_from_mapping(&address, deleted_timestamp, deleted_block) .await; diff --git a/src/startup/remote_height.rs b/src/startup/remote_height.rs index 7f99240..a3e0908 100644 --- a/src/startup/remote_height.rs +++ b/src/startup/remote_height.rs @@ -1,4 +1,4 @@ -use crate::records::memory::response_channels::{reserve_entry, Command}; +use crate::records::memory::response_channels::{reserve_entry_with_context, Command}; use crate::rpc::command_maps::RPC_BLOCK_HEIGHT; use crate::rpc::responses::RpcResponse; use crate::Arc; @@ -13,7 +13,9 @@ pub async fn request_remote_height( ) -> Result { // request the remote node's current chain height using // the standard reply-channel request/response flow - let (hashmap_key, _tx, rx) = reserve_entry(map.clone()).await; + let (hashmap_key, _tx, rx) = + reserve_entry_with_context(map.clone(), Some(RPC_BLOCK_HEIGHT), Some(connections_key.clone())) + .await; // message format is the height command plus the unique // reply key used to route the response back here diff --git a/src/startup/unlock_pipe.rs b/src/startup/unlock_pipe.rs index fc7973b..97476df 100644 --- a/src/startup/unlock_pipe.rs +++ b/src/startup/unlock_pipe.rs @@ -1,205 +1,205 @@ -#[cfg(windows)] -use crate::common::network_paths_and_settings::block_extension_and_paths; -#[cfg(windows)] -use crate::from_slice; -#[cfg(windows)] -use crate::log::{error, info, warn}; -#[cfg(windows)] -use crate::startup::unlock_structs::{ServiceWaitState, UnlockPipeRequest, UnlockPipeResponse}; -#[cfg(windows)] -use crate::to_string; -#[cfg(windows)] -use crate::wallets::structures::Wallet; -#[cfg(windows)] -use crate::Arc; -#[cfg(windows)] -use crate::{sleep, timeout, AsyncReadExt, AsyncWriteExt, Duration, RwLock}; -#[cfg(windows)] -use crate::{AtomicBool, AtomicOrdering}; -#[cfg(windows)] -use tokio::net::windows::named_pipe::ServerOptions; -#[cfg(windows)] -use tokio::sync::mpsc; -#[cfg(windows)] -use windows_sys::Win32::Foundation::LocalFree; -#[cfg(windows)] -use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW; -#[cfg(windows)] -use windows_sys::Win32::Security::{PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES}; - -#[cfg(windows)] -pub fn pipe_name() -> String { - // Include the active network name so testnet and mainnet services never share a pipe. - let ( - network_name, - _padded_base_coin, - _suffix, - _torrent_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - format!(r"\\.\pipe\contractless_{network_name}_unlock") -} - -#[cfg(windows)] -fn create_pipe_server( - pipe_name: &str, - first_instance: bool, -) -> std::io::Result { - // The unlock pipe is local-only IPC between the Windows service and the helper - // tool that submits the wallet key after the service has already started. - let mut options = ServerOptions::new(); - options.reject_remote_clients(true); - if first_instance { - options.first_pipe_instance(true); - } - - // Allow the service, local administrators, interactive users, and normal local users - // to talk to the unlock pipe while still rejecting remote clients at the pipe layer. - let security_descriptor = wide_null("D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;IU)(A;;GRGW;;;BU)"); - let mut raw_sd: PSECURITY_DESCRIPTOR = std::ptr::null_mut(); - - let converted = unsafe { - ConvertStringSecurityDescriptorToSecurityDescriptorW( - security_descriptor.as_ptr(), - 1, - &mut raw_sd, - std::ptr::null_mut(), - ) - }; - - if converted == 0 || raw_sd.is_null() { - return Err(std::io::Error::last_os_error()); - } - - let mut attrs = SECURITY_ATTRIBUTES { - nLength: std::mem::size_of::() as u32, - lpSecurityDescriptor: raw_sd as *mut _, - bInheritHandle: 0, - }; - - let server = unsafe { - options.create_with_security_attributes_raw(pipe_name, &mut attrs as *mut _ as *mut _) - }; - - unsafe { - LocalFree(raw_sd as *mut _); - } - - server -} - -#[cfg(windows)] -fn wide_null(value: &str) -> Vec { - // Windows security APIs expect null-terminated UTF-16 strings. - value.encode_utf16().chain(std::iter::once(0)).collect() -} - -#[cfg(windows)] +#[cfg(windows)] +use crate::common::network_paths_and_settings::block_extension_and_paths; +#[cfg(windows)] +use crate::from_slice; +#[cfg(windows)] +use crate::log::{error, info, warn}; +#[cfg(windows)] +use crate::startup::unlock_structs::{ServiceWaitState, UnlockPipeRequest, UnlockPipeResponse}; +#[cfg(windows)] +use crate::to_string; +#[cfg(windows)] +use crate::wallets::structures::Wallet; +#[cfg(windows)] +use crate::Arc; +#[cfg(windows)] +use crate::{sleep, timeout, AsyncReadExt, AsyncWriteExt, Duration, RwLock}; +#[cfg(windows)] +use crate::{AtomicBool, AtomicOrdering}; +#[cfg(windows)] +use tokio::net::windows::named_pipe::ServerOptions; +#[cfg(windows)] +use tokio::sync::mpsc; +#[cfg(windows)] +use windows_sys::Win32::Foundation::LocalFree; +#[cfg(windows)] +use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW; +#[cfg(windows)] +use windows_sys::Win32::Security::{PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES}; + +#[cfg(windows)] +pub fn pipe_name() -> String { + // Include the active network name so testnet and mainnet services never share a pipe. + let ( + network_name, + _padded_base_coin, + _suffix, + _torrent_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + format!(r"\\.\pipe\contractless_{network_name}_unlock") +} + +#[cfg(windows)] +fn create_pipe_server( + pipe_name: &str, + first_instance: bool, +) -> std::io::Result { + // The unlock pipe is local-only IPC between the Windows service and the helper + // tool that submits the wallet key after the service has already started. + let mut options = ServerOptions::new(); + options.reject_remote_clients(true); + if first_instance { + options.first_pipe_instance(true); + } + + // Allow the service, local administrators, interactive users, and normal local users + // to talk to the unlock pipe while still rejecting remote clients at the pipe layer. + let security_descriptor = wide_null("D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;IU)(A;;GRGW;;;BU)"); + let mut raw_sd: PSECURITY_DESCRIPTOR = std::ptr::null_mut(); + + let converted = unsafe { + ConvertStringSecurityDescriptorToSecurityDescriptorW( + security_descriptor.as_ptr(), + 1, + &mut raw_sd, + std::ptr::null_mut(), + ) + }; + + if converted == 0 || raw_sd.is_null() { + return Err(std::io::Error::last_os_error()); + } + + let mut attrs = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: raw_sd as *mut _, + bInheritHandle: 0, + }; + + let server = unsafe { + options.create_with_security_attributes_raw(pipe_name, &mut attrs as *mut _ as *mut _) + }; + + unsafe { + LocalFree(raw_sd as *mut _); + } + + server +} + +#[cfg(windows)] +fn wide_null(value: &str) -> Vec { + // Windows security APIs expect null-terminated UTF-16 strings. + value.encode_utf16().chain(std::iter::once(0)).collect() +} + +#[cfg(windows)] pub async fn run_unlock_pipe_server( service_state: Arc>, shutdown: Arc, unlock_sender: mpsc::UnboundedSender>, ) { - let pipe_name = pipe_name(); - let mut first_instance = true; - - info!("Named pipe listener started at {pipe_name}"); - - // A new pipe instance is created for each request so the service can accept - // repeated status checks and a later wallet submission through the same name. - while !shutdown.load(AtomicOrdering::SeqCst) { - let mut server = match create_pipe_server(&pipe_name, first_instance) { + let pipe_name = pipe_name(); + let mut first_instance = true; + + info!("Named pipe listener started at {pipe_name}"); + + // A new pipe instance is created for each request so the service can accept + // repeated status checks and a later wallet submission through the same name. + while !shutdown.load(AtomicOrdering::SeqCst) { + let mut server = match create_pipe_server(&pipe_name, first_instance) { Ok(server) => server, Err(err) => { error!("Failed to create named pipe {pipe_name}: {err}"); sleep(Duration::from_secs(1)).await; continue; } - }; - - first_instance = false; - - // Use a short connect timeout so shutdown checks are not blocked by an idle pipe. - match timeout(Duration::from_secs(1), server.connect()).await { - Ok(Ok(())) => {} - Ok(Err(err)) => { - warn!("Named pipe connect failed: {err}"); - continue; - } - Err(_) => { - continue; - } - } - - // Requests are length-prefixed JSON so the service can read exactly one command. - let request_len = match server.read_u32_le().await { - Ok(len) => len as usize, - Err(err) => { - warn!("Named pipe length read failed: {err}"); - continue; - } - }; - - let mut request_bytes = vec![0u8; request_len]; - if let Err(err) = server.read_exact(&mut request_bytes).await { - warn!("Named pipe read failed: {err}"); - continue; - } - - let response = - handle_request(&request_bytes, service_state.clone(), unlock_sender.clone()).await; - - // Responses use the same length-prefixed JSON shape as requests. - let response_bytes = match to_string(&response) { - Ok(json) => json.into_bytes(), - Err(err) => { - error!("Failed to serialize named pipe response: {err}"); - continue; - } - }; - - if let Err(err) = server.write_u32_le(response_bytes.len() as u32).await { - warn!("Named pipe response length write failed: {err}"); - continue; - } - - if let Err(err) = server.write_all(&response_bytes).await { - warn!("Named pipe write failed: {err}"); - } - } - - info!("Named pipe listener stopped."); -} - -#[cfg(windows)] + }; + + first_instance = false; + + // Use a short connect timeout so shutdown checks are not blocked by an idle pipe. + match timeout(Duration::from_secs(1), server.connect()).await { + Ok(Ok(())) => {} + Ok(Err(err)) => { + warn!("Named pipe connect failed: {err}"); + continue; + } + Err(_) => { + continue; + } + } + + // Requests are length-prefixed JSON so the service can read exactly one command. + let request_len = match server.read_u32_le().await { + Ok(len) => len as usize, + Err(err) => { + warn!("Named pipe length read failed: {err}"); + continue; + } + }; + + let mut request_bytes = vec![0u8; request_len]; + if let Err(err) = server.read_exact(&mut request_bytes).await { + warn!("Named pipe read failed: {err}"); + continue; + } + + let response = + handle_request(&request_bytes, service_state.clone(), unlock_sender.clone()).await; + + // Responses use the same length-prefixed JSON shape as requests. + let response_bytes = match to_string(&response) { + Ok(json) => json.into_bytes(), + Err(err) => { + error!("Failed to serialize named pipe response: {err}"); + continue; + } + }; + + if let Err(err) = server.write_u32_le(response_bytes.len() as u32).await { + warn!("Named pipe response length write failed: {err}"); + continue; + } + + if let Err(err) = server.write_all(&response_bytes).await { + warn!("Named pipe write failed: {err}"); + } + } + + info!("Named pipe listener stopped."); +} + +#[cfg(windows)] async fn handle_request( request_bytes: &[u8], service_state: Arc>, unlock_sender: mpsc::UnboundedSender>, ) -> UnlockPipeResponse { - // Malformed helper requests are reported back through the pipe instead of panicking the service. - let request = match from_slice::(request_bytes) { - Ok(request) => request, - Err(err) => { - return UnlockPipeResponse::Error { - message: format!("Invalid request payload: {err}"), - }; - } - }; - - match request { - UnlockPipeRequest::Ping => UnlockPipeResponse::Pong, - UnlockPipeRequest::Status => { - let state = *service_state.read().await; - UnlockPipeResponse::Status { state } - } - UnlockPipeRequest::SubmitKey { wallet_key } => { - // The service only accepts a wallet key while it is still in the locked - // waiting state, and it validates the key before allowing normal startup. + // Malformed helper requests are reported back through the pipe instead of panicking the service. + let request = match from_slice::(request_bytes) { + Ok(request) => request, + Err(err) => { + return UnlockPipeResponse::Error { + message: format!("Invalid request payload: {err}"), + }; + } + }; + + match request { + UnlockPipeRequest::Ping => UnlockPipeResponse::Pong, + UnlockPipeRequest::Status => { + let state = *service_state.read().await; + UnlockPipeResponse::Status { state } + } + UnlockPipeRequest::SubmitKey { wallet_key } => { + // The service only accepts a wallet key while it is still in the locked + // waiting state, and it validates the key before allowing normal startup. let current_state = *service_state.read().await; if !matches!(current_state, ServiceWaitState::WaitingForUnlock) { return UnlockPipeResponse::Error { @@ -208,7 +208,7 @@ async fn handle_request( ), }; } - + match Wallet::try_obtain_wallet(wallet_key, None).await { Ok(wallet) => { // Mark unlocked before sending the wallet so status checks immediately reflect progress. @@ -222,19 +222,19 @@ async fn handle_request( let mut state = service_state.write().await; *state = ServiceWaitState::WaitingForUnlock; return UnlockPipeResponse::Error { - message: "Service failed to accept the unlock request.".to_string(), - }; - } - - UnlockPipeResponse::KeyAccepted - } - Err(err) => UnlockPipeResponse::Error { message: err }, - } - } - } -} - -#[cfg(not(windows))] -pub fn pipe_name() -> String { - String::new() -} + message: "Service failed to accept the unlock request.".to_string(), + }; + } + + UnlockPipeResponse::KeyAccepted + } + Err(err) => UnlockPipeResponse::Error { message: err }, + } + } + } +} + +#[cfg(not(windows))] +pub fn pipe_name() -> String { + String::new() +} diff --git a/src/torrent/torrenting_system/request_piece.rs b/src/torrent/torrenting_system/request_piece.rs index 157f380..76804ee 100644 --- a/src/torrent/torrenting_system/request_piece.rs +++ b/src/torrent/torrenting_system/request_piece.rs @@ -1,4 +1,4 @@ -use crate::records::memory::response_channels::{delete_entry, reserve_entry}; +use crate::records::memory::response_channels::{delete_entry, reserve_entry_with_context}; use crate::rpc::command_maps::RPC_BLOCK_PIECE; use crate::rpc::responses::RpcResponse; use crate::torrent::structs::RequestPiece; @@ -9,7 +9,12 @@ use crate::{timeout, Duration}; pub async fn request_piece_from_node(params: RequestPiece) -> Result, String> { // Reserve a response slot in the shared hashmap so the incoming // piece bytes can be routed back to this request. - let (hashmap_key, _block_tx, block_rx) = reserve_entry(params.map.clone()).await; + let (hashmap_key, _block_tx, block_rx) = reserve_entry_with_context( + params.map.clone(), + Some(RPC_BLOCK_PIECE), + Some(params.connections_key.clone()), + ) + .await; // Compute the exact piece length, including the shortened final // piece when the block length is not an exact multiple. diff --git a/src/torrent/torrenting_system/save_torrent.rs b/src/torrent/torrenting_system/save_torrent.rs index e6f0ecc..c6fa492 100644 --- a/src/torrent/torrenting_system/save_torrent.rs +++ b/src/torrent/torrenting_system/save_torrent.rs @@ -2,70 +2,70 @@ use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::torrent::structs::Torrent; use crate::{create_dir_all, fs, read, read_dir, remove_file, AsyncWriteExt, Path}; use std::io::ErrorKind; - -fn staged_torrent_dir() -> Result { - // Keep staged torrents under a dedicated subdirectory so incoming - // network data is separated from canonical saved torrent files. - let ( - _network_name, - _padded_base_coin, - _block_ext, - out_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let staging_dir = Path::new(&out_path).join("staging"); - Ok(staging_dir) -} - + +fn staged_torrent_dir() -> Result { + // Keep staged torrents under a dedicated subdirectory so incoming + // network data is separated from canonical saved torrent files. + let ( + _network_name, + _padded_base_coin, + _block_ext, + out_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let staging_dir = Path::new(&out_path).join("staging"); + Ok(staging_dir) +} + fn staged_torrent_path(height: u32, info_hash: &str) -> std::path::PathBuf { // Each candidate gets its own deterministic staging path, so competing // torrents at the same height cannot race over a shared suffix. let ( _network_name, _padded_base_coin, - _block_ext, - out_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, + _block_ext, + out_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, ) = block_extension_and_paths(); Path::new(&out_path) .join("staging") .join(format!("{height}.{info_hash}.torrent")) } - -fn canonical_torrent_path(height: u32) -> std::path::PathBuf { - let ( - _network_name, - _padded_base_coin, - _block_ext, - out_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - Path::new(&out_path).join(format!("{height}.torrent")) -} - -async fn torrent_bytes_match(path: &Path, torrent_bytes: &[u8]) -> Result { - if !path.exists() { - return Ok(false); - } - - let existing = read(path) - .await - .map_err(|e| format!("Failed to read torrent {}: {}", path.display(), e))?; - Ok(existing == torrent_bytes) -} - + +fn canonical_torrent_path(height: u32) -> std::path::PathBuf { + let ( + _network_name, + _padded_base_coin, + _block_ext, + out_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + Path::new(&out_path).join(format!("{height}.torrent")) +} + +async fn torrent_bytes_match(path: &Path, torrent_bytes: &[u8]) -> Result { + if !path.exists() { + return Ok(false); + } + + let existing = read(path) + .await + .map_err(|e| format!("Failed to read torrent {}: {}", path.display(), e))?; + Ok(existing == torrent_bytes) +} + fn parse_staged_torrent_file_name(file_name: &str) -> Option<(u32, String)> { // New staged names encode both height and candidate hash: // `..torrent`. @@ -84,58 +84,58 @@ fn parse_staged_torrent_file_name(file_name: &str) -> Option<(u32, String)> { let suffix = suffix_str.parse::().ok()?; Some((height, format!("legacy-{suffix:010}"))) } - -pub async fn list_staged_torrents() -> Result, String> { + +pub async fn list_staged_torrents() -> Result, String> { // Enumerate staged torrents in height/candidate order so replay and // cleanup logic handle them in a stable sequence. - let staging_dir = staged_torrent_dir()?; - - if !staging_dir.exists() { - return Ok(Vec::new()); - } - - let mut entries = read_dir(&staging_dir) - .await - .map_err(|e| format!("Failed to read staging directory: {e}"))?; - let mut staged = Vec::new(); - - while let Some(entry) = entries - .next_entry() - .await - .map_err(|e| format!("Failed to iterate staging directory: {e}"))? - { - let path = entry.path(); - let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { - continue; - }; + let staging_dir = staged_torrent_dir()?; + + if !staging_dir.exists() { + return Ok(Vec::new()); + } + + let mut entries = read_dir(&staging_dir) + .await + .map_err(|e| format!("Failed to read staging directory: {e}"))?; + let mut staged = Vec::new(); + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|e| format!("Failed to iterate staging directory: {e}"))? + { + let path = entry.path(); + let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; if let Some((height, candidate_key)) = parse_staged_torrent_file_name(file_name) { staged.push((height, candidate_key, path)); } } - - staged.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); - Ok(staged - .into_iter() - .map(|(height, _suffix, path)| (height, path)) - .collect()) -} - -pub async fn list_staged_torrents_for_height( - height: u32, -) -> Result, String> { - let staged = list_staged_torrents().await?; - Ok(staged - .into_iter() - .filter_map(|(staged_height, path)| { - if staged_height == height { - Some(path) - } else { - None - } - }) - .collect()) -} - + + staged.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); + Ok(staged + .into_iter() + .map(|(height, _suffix, path)| (height, path)) + .collect()) +} + +pub async fn list_staged_torrents_for_height( + height: u32, +) -> Result, String> { + let staged = list_staged_torrents().await?; + Ok(staged + .into_iter() + .filter_map(|(staged_height, path)| { + if staged_height == height { + Some(path) + } else { + None + } + }) + .collect()) +} + pub async fn save_staged_torrent(height: u32, torrent_bytes: &[u8]) -> Result { // Store this candidate by its advertised block info hash. That makes // staging idempotent per candidate and prevents same-height races from @@ -183,9 +183,9 @@ pub async fn save_staged_torrent(height: u32, torrent_bytes: &[u8]) -> Result return Err(format!("Failed to create staged torrent file: {err}")), }; - - // Keep torrent staging separate from the canonical torrent file so - // validation and orphan logic can decide when promotion is safe. + + // Keep torrent staging separate from the canonical torrent file so + // validation and orphan logic can decide when promotion is safe. torrent_file .write_all(torrent_bytes) .await @@ -194,55 +194,55 @@ pub async fn save_staged_torrent(height: u32, torrent_bytes: &[u8]) -> Result Result, String> { - read(path) - .await - .map_err(|e| format!("Failed to read staged torrent {}: {}", path.display(), e)) -} - -pub async fn remove_staged_torrent(path: &Path) -> Result<(), String> { - if path.exists() { - remove_file(path) - .await - .map_err(|e| format!("Failed to remove staged torrent {}: {}", path.display(), e))?; - } - Ok(()) -} - -pub async fn remove_staged_torrents_for_height(height: u32) -> Result<(), String> { - // Remove every staged copy for the requested height so stale - // torrents do not interfere with later replay or promotion. - let staged = list_staged_torrents().await?; - for (staged_height, path) in staged { - if staged_height == height { - remove_staged_torrent(&path).await?; - } - } - Ok(()) -} - -pub async fn prune_staged_torrents(current_height: u32) -> Result<(), String> { - // Snapshot-based pruning keeps recent orphan evidence available - // until the trusted rollback floor advances past it. - let cutoff = current_height; - let staged = list_staged_torrents().await?; - for (staged_height, path) in staged { - if staged_height <= cutoff { - remove_staged_torrent(&path).await?; - } - } - Ok(()) -} - -pub fn promote_staged_torrent(staged_path: &Path, height: u32) -> Result { - // Promotion moves the selected staged torrent into the canonical - // filename once the corresponding block has been saved. - let canonical_path = canonical_torrent_path(height); - + + Ok(torrent_file_path.to_string_lossy().to_string()) +} + +pub async fn read_staged_torrent(path: &Path) -> Result, String> { + read(path) + .await + .map_err(|e| format!("Failed to read staged torrent {}: {}", path.display(), e)) +} + +pub async fn remove_staged_torrent(path: &Path) -> Result<(), String> { + if path.exists() { + remove_file(path) + .await + .map_err(|e| format!("Failed to remove staged torrent {}: {}", path.display(), e))?; + } + Ok(()) +} + +pub async fn remove_staged_torrents_for_height(height: u32) -> Result<(), String> { + // Remove every staged copy for the requested height so stale + // torrents do not interfere with later replay or promotion. + let staged = list_staged_torrents().await?; + for (staged_height, path) in staged { + if staged_height == height { + remove_staged_torrent(&path).await?; + } + } + Ok(()) +} + +pub async fn prune_staged_torrents(current_height: u32) -> Result<(), String> { + // Snapshot-based pruning keeps recent orphan evidence available + // until the trusted rollback floor advances past it. + let cutoff = current_height; + let staged = list_staged_torrents().await?; + for (staged_height, path) in staged { + if staged_height <= cutoff { + remove_staged_torrent(&path).await?; + } + } + Ok(()) +} + +pub fn promote_staged_torrent(staged_path: &Path, height: u32) -> Result { + // Promotion moves the selected staged torrent into the canonical + // filename once the corresponding block has been saved. + let canonical_path = canonical_torrent_path(height); + if !staged_path.exists() { return Err(format!( "Staged torrent does not exist for block {} at {}", @@ -257,6 +257,6 @@ pub fn promote_staged_torrent(staged_path: &Path, height: u32) -> Result Result { - // Look for the canonical torrent file for this network and height. - let ( - _network_name, - _padded_base_coin, - _file_ext, - torrent_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - - let torrent_file = PathBuf::from(&torrent_path) - .join(format!("{height}.torrent")) - .to_string_lossy() - .into_owned(); - let file_exists = Path::new(&torrent_file).exists(); - if file_exists { - // Existing canonical torrent path can be loaded by the caller. - Ok(torrent_file) - } else { - // If the chain height points past available torrent data, step the recorded height back. - let check_height = get_height(db); - if check_height > 0 { - let torrent_file = PathBuf::from(torrent_path) - .join(format!("{height}.torrent")) - .to_string_lossy() - .into_owned(); - - let file_exists = Path::new(&torrent_file).exists(); - if !file_exists { - decrease_height(check_height - 1, db); - } - } - Err("Failed to load torrent".to_string()) - } -} - -pub async fn load_torrent(db: &Db, height: u32) -> Result { - let torrent = get_or_correct_local_torrent(db, height).await?; - - if let Ok(mut torrent_file) = File::open(&torrent).await { - // Torrent files are stored in the compact binary format from Torrent::to_bytes. - let mut torrent_contents = Vec::new(); - torrent_file - .read_to_end(&mut torrent_contents) - .await - .map_err(|e| e.to_string())?; - // Convert the saved bytes back into the in-memory metadata struct. - Torrent::from_bytes(&torrent_contents) - .await - .map_err(|e| e.to_string()) - } else { - Err("Could not open torrent".to_string()) - } -} +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::records::block_height::decrease_block_height::decrease_height; +use crate::records::block_height::get_block_height::get_height; +use crate::sled::Db; +use crate::torrent::structs::Torrent; +use crate::AsyncReadExt; +use crate::File; +use crate::Path; +use crate::PathBuf; + +async fn get_or_correct_local_torrent(db: &Db, height: u32) -> Result { + // Look for the canonical torrent file for this network and height. + let ( + _network_name, + _padded_base_coin, + _file_ext, + torrent_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + + let torrent_file = PathBuf::from(&torrent_path) + .join(format!("{height}.torrent")) + .to_string_lossy() + .into_owned(); + let file_exists = Path::new(&torrent_file).exists(); + if file_exists { + // Existing canonical torrent path can be loaded by the caller. + Ok(torrent_file) + } else { + // If the chain height points past available torrent data, step the recorded height back. + let check_height = get_height(db); + if check_height > 0 { + let torrent_file = PathBuf::from(torrent_path) + .join(format!("{height}.torrent")) + .to_string_lossy() + .into_owned(); + + let file_exists = Path::new(&torrent_file).exists(); + if !file_exists { + decrease_height(check_height - 1, db); + } + } + Err("Failed to load torrent".to_string()) + } +} + +pub async fn load_torrent(db: &Db, height: u32) -> Result { + let torrent = get_or_correct_local_torrent(db, height).await?; + + if let Ok(mut torrent_file) = File::open(&torrent).await { + // Torrent files are stored in the compact binary format from Torrent::to_bytes. + let mut torrent_contents = Vec::new(); + torrent_file + .read_to_end(&mut torrent_contents) + .await + .map_err(|e| e.to_string())?; + // Convert the saved bytes back into the in-memory metadata struct. + Torrent::from_bytes(&torrent_contents) + .await + .map_err(|e| e.to_string()) + } else { + Err("Could not open torrent".to_string()) + } +} diff --git a/src/wallets/load_wallets.rs b/src/wallets/load_wallets.rs index 9b7f93e..75def39 100644 --- a/src/wallets/load_wallets.rs +++ b/src/wallets/load_wallets.rs @@ -1,90 +1,90 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::decode_image_and_extract_text; -use crate::decrypts; -use crate::from_slice; -use crate::log::error; -use crate::metadata; -use crate::read; -use crate::wallets::structures::{SavedWallet, Wallet}; -use crate::Path; - -impl Wallet { - pub async fn get_wallet_path() -> String { - // Load the active network paths and keep only the wallet file path. - let ( - _network_name, - _padded_base_coin, - _suffix, - _torrent_path, - wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - wallet_path - } - - pub async fn private_key_from_wallet( - wallet_path: &Path, - wallet_key: String, - ) -> Result { - // Read the wallet JSON file from disk. - if let Ok(wallet_content) = read(wallet_path).await { - // Deserialize the saved wallet before extracting the encrypted image payload. - let mut wallet: SavedWallet = from_slice(&wallet_content) - .map_err(|e| format!("Deserialization of wallet failed: {e}"))?; - - // Extract the encrypted private key text from the wallet image. - if let Some(encrypted_text) = decode_image_and_extract_text(&wallet.private_key) { - // Decrypt the private key with the user-provided wallet key. - if let Some(decrypted_private_key) = decrypts(&encrypted_text, Some(&wallet_key)) { - // Replace the saved encrypted image payload with the decrypted private key in memory. - wallet.private_key = decrypted_private_key; - - // Attach the encryption key so the full wallet can be reused by callers. - let full_wallet = Wallet { - saved: wallet, - encryption_key: wallet_key.clone(), - }; - - // Return the loaded wallet when image extraction and decryption both succeed. - Ok(full_wallet) - } else { - error!("Decryption of private key failed."); - Err("Decryption of private key failed.".into()) - } - } else { - error!("Decryption of image failed."); - Err("Decryption of image failed.".into()) - } - } else { - error!("Wallet path did not exist"); - Err("Wallet path did not exist".into()) - } - } - - async fn load_wallet(wallet_path: &Path, wallet_key: String) -> Result { - // Load an existing wallet when the file exists. - if metadata(wallet_path).await.is_ok() { - Self::private_key_from_wallet(wallet_path, wallet_key).await - } else { - // Create, save, and load a new wallet when no wallet file exists. - Self::generate_saved_struct(wallet_path, wallet_key).await - } - } - - pub async fn try_obtain_wallet( - wallet_key: String, - path: Option<&str>, - ) -> Result { - // Use a caller-provided path when supplied, otherwise use the active network wallet path. - let wallet_path = match path { - Some(p) => p.to_string(), - None => Wallet::get_wallet_path().await, - }; - - // Load or create the wallet and return any failure to the caller. - Self::load_wallet(Path::new(&wallet_path), wallet_key).await - } -} +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::decode_image_and_extract_text; +use crate::decrypts; +use crate::from_slice; +use crate::log::error; +use crate::metadata; +use crate::read; +use crate::wallets::structures::{SavedWallet, Wallet}; +use crate::Path; + +impl Wallet { + pub async fn get_wallet_path() -> String { + // Load the active network paths and keep only the wallet file path. + let ( + _network_name, + _padded_base_coin, + _suffix, + _torrent_path, + wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + wallet_path + } + + pub async fn private_key_from_wallet( + wallet_path: &Path, + wallet_key: String, + ) -> Result { + // Read the wallet JSON file from disk. + if let Ok(wallet_content) = read(wallet_path).await { + // Deserialize the saved wallet before extracting the encrypted image payload. + let mut wallet: SavedWallet = from_slice(&wallet_content) + .map_err(|e| format!("Deserialization of wallet failed: {e}"))?; + + // Extract the encrypted private key text from the wallet image. + if let Some(encrypted_text) = decode_image_and_extract_text(&wallet.private_key) { + // Decrypt the private key with the user-provided wallet key. + if let Some(decrypted_private_key) = decrypts(&encrypted_text, Some(&wallet_key)) { + // Replace the saved encrypted image payload with the decrypted private key in memory. + wallet.private_key = decrypted_private_key; + + // Attach the encryption key so the full wallet can be reused by callers. + let full_wallet = Wallet { + saved: wallet, + encryption_key: wallet_key.clone(), + }; + + // Return the loaded wallet when image extraction and decryption both succeed. + Ok(full_wallet) + } else { + error!("Decryption of private key failed."); + Err("Decryption of private key failed.".into()) + } + } else { + error!("Decryption of image failed."); + Err("Decryption of image failed.".into()) + } + } else { + error!("Wallet path did not exist"); + Err("Wallet path did not exist".into()) + } + } + + async fn load_wallet(wallet_path: &Path, wallet_key: String) -> Result { + // Load an existing wallet when the file exists. + if metadata(wallet_path).await.is_ok() { + Self::private_key_from_wallet(wallet_path, wallet_key).await + } else { + // Create, save, and load a new wallet when no wallet file exists. + Self::generate_saved_struct(wallet_path, wallet_key).await + } + } + + pub async fn try_obtain_wallet( + wallet_key: String, + path: Option<&str>, + ) -> Result { + // Use a caller-provided path when supplied, otherwise use the active network wallet path. + let wallet_path = match path { + Some(p) => p.to_string(), + None => Wallet::get_wallet_path().await, + }; + + // Load or create the wallet and return any failure to the caller. + Self::load_wallet(Path::new(&wallet_path), wallet_key).await + } +}