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