startup fixes

This commit is contained in:
viraladmin 2026-06-28 16:12:38 -06:00
parent dbea91ee65
commit f03367c3d7
11 changed files with 393 additions and 148 deletions

View File

@ -9,29 +9,62 @@ 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::asset_names::loan_collateral_asset_name_or_base;
use crate::common::nft_assets::nft_asset_name; use crate::common::nft_assets::nft_asset_name;
use crate::common::types::Transaction; use crate::common::types::Transaction;
use crate::decode; use crate::decode;
use crate::records::memory::mempool::{restore_processed_by_signatures, BASECOIN}; use crate::log::warn;
use crate::records::memory::mempool::{db_client, restore_processed_by_signatures, BASECOIN};
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;
use crate::verifications::async_funcs::checks::balance_check::balance_checkup; use crate::verifications::async_funcs::checks::balance_check::balance_checkup;
async fn restore_if_spendable<F, Fut>(signatures: &[String], spendable: bool, insert: F) async fn restore_if_spendable<F, Fut>(
where tx_label: &str,
txid: &str,
signatures: &[String],
spendable: bool,
insert: F,
) where
F: FnOnce() -> Fut, F: FnOnce() -> Fut,
Fut: std::future::Future<Output = ()>, Fut: std::future::Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>>,
{ {
if !spendable { if !spendable {
warn!("[mempool_restore] skipped rolled-back tx: type={tx_label} txid={txid} reason=not_spendable_after_rollback");
return; return;
} }
match restore_processed_by_signatures(signatures).await { match restore_processed_by_signatures(signatures).await {
Ok(false) | Err(_) => insert().await,
Ok(true) => {} Ok(true) => {}
Ok(false) => {
if let Err(err) = insert().await {
warn!("[mempool_restore] failed to reinsert rolled-back tx: type={tx_label} txid={txid} err={err}");
} }
}
Err(err) => {
warn!("[mempool_restore] failed to unmark processed rolled-back tx: type={tx_label} txid={txid} err={err}");
if let Err(insert_err) = insert().await {
warn!("[mempool_restore] failed to reinsert rolled-back tx after unmark error: type={tx_label} txid={txid} err={insert_err}");
}
}
}
}
async fn restore_if_spendable_void<F, Fut>(
tx_label: &str,
txid: &str,
signatures: &[String],
spendable: bool,
insert: F,
) where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = ()>,
{
restore_if_spendable(tx_label, txid, signatures, spendable, || async {
insert().await;
Ok(())
})
.await;
} }
async fn loan_coin_for_contract(db: &Db, contract_hash: &str) -> Option<String> { async fn loan_coin_for_contract(db: &Db, contract_hash: &str) -> Option<String> {
@ -48,6 +81,54 @@ async fn loan_coin_for_contract(db: &Db, contract_hash: &str) -> Option<String>
.map(|contract| contract.unsigned_loan_contract.loan_coin) .map(|contract| contract.unsigned_loan_contract.loan_coin)
} }
async fn restore_loan_contract_row(
transaction: &LoanContractTransaction,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client_handle = db_client().await?;
let client = client_handle.as_ref();
let hash = &transaction.hash;
let signature1 = &transaction.signature1;
let signature2 = &transaction.signature2;
client
.execute(
r#"
UPDATE loan_contract
SET processed=false,
processed_block_number=NULL
WHERE signature1 = $1
OR signature2 = $2
OR hash = $3
OR txid = $3
"#,
&[signature1, signature2, hash],
)
.await?;
let row = client
.query_one(
r#"
SELECT COUNT(*)
FROM loan_contract
WHERE processed=false
AND (
signature1 = $1
OR signature2 = $2
OR hash = $3
OR txid = $3
)
"#,
&[signature1, signature2, hash],
)
.await?;
let pending_count: i64 = row.get(0);
if pending_count == 0 {
transaction.add_to_memory().await?;
}
Ok(())
}
pub async fn restore_transfer(transaction: &TransferTransaction, db: &Db) { pub async fn restore_transfer(transaction: &TransferTransaction, db: &Db) {
let transfer = &transaction.unsigned_transfer; let transfer = &transaction.unsigned_transfer;
let asset = nft_asset_name(&transfer.coin, transfer.nft_series); let asset = nft_asset_name(&transfer.coin, transfer.nft_series);
@ -55,7 +136,7 @@ pub async fn restore_transfer(transaction: &TransferTransaction, db: &Db) {
balance_checkup(db, transfer.value, transfer.txfee, asset, &transfer.sender).await; balance_checkup(db, transfer.value, transfer.txfee, asset, &transfer.sender).await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable_void("transfer", "unknown", &[signature], spendable, || async {
let _ = transaction.add_to_memory().await; let _ = transaction.add_to_memory().await;
}) })
.await; .await;
@ -67,7 +148,7 @@ pub async fn restore_burn(transaction: &BurnTransaction, db: &Db) {
let spendable = balance_checkup(db, burn.value, burn.txfee, asset, &burn.address).await; let spendable = balance_checkup(db, burn.value, burn.txfee, asset, &burn.address).await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable_void("burn", "unknown", &[signature], spendable, || async {
let _ = transaction.add_to_memory().await; let _ = transaction.add_to_memory().await;
}) })
.await; .await;
@ -78,9 +159,15 @@ pub async fn restore_create_token(transaction: &CreateTokenTransaction, db: &Db)
let spendable = balance_checkup(db, 0, token.txfee, BASECOIN.clone(), &token.creator).await; let spendable = balance_checkup(db, 0, token.txfee, BASECOIN.clone(), &token.creator).await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable_void(
"create_token",
"unknown",
&[signature],
spendable,
|| async {
let _ = transaction.add_to_memory().await; let _ = transaction.add_to_memory().await;
}) },
)
.await; .await;
} }
@ -89,9 +176,15 @@ pub async fn restore_issue_token(transaction: &IssueTokenTransaction, db: &Db) {
let spendable = balance_checkup(db, 0, token.txfee, BASECOIN.clone(), &token.creator).await; let spendable = balance_checkup(db, 0, token.txfee, BASECOIN.clone(), &token.creator).await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable_void(
"issue_token",
"unknown",
&[signature],
spendable,
|| async {
let _ = transaction.add_to_memory().await; let _ = transaction.add_to_memory().await;
}) },
)
.await; .await;
} }
@ -100,7 +193,7 @@ pub async fn restore_create_nft(transaction: &CreateNftTransaction, db: &Db) {
let spendable = balance_checkup(db, 0, nft.txfee, BASECOIN.clone(), &nft.creator).await; let spendable = balance_checkup(db, 0, nft.txfee, BASECOIN.clone(), &nft.creator).await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable_void("create_nft", "unknown", &[signature], spendable, || async {
let _ = transaction.add_to_memory().await; let _ = transaction.add_to_memory().await;
}) })
.await; .await;
@ -118,7 +211,7 @@ pub async fn restore_marketing(transaction: &MarketingTransaction, db: &Db) {
.await; .await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable_void("marketing", "unknown", &[signature], spendable, || async {
let _ = transaction.add_to_memory().await; let _ = transaction.add_to_memory().await;
}) })
.await; .await;
@ -137,7 +230,9 @@ pub async fn restore_swap(transaction: &SwapTransaction, db: &Db) {
transaction.signature2.clone(), transaction.signature2.clone(),
]; ];
restore_if_spendable( restore_if_spendable_void(
"swap",
"unknown",
&signatures, &signatures,
sender1_spendable && sender2_spendable, sender1_spendable && sender2_spendable,
|| async { || async {
@ -147,34 +242,13 @@ pub async fn restore_swap(transaction: &SwapTransaction, db: &Db) {
.await; .await;
} }
pub async fn restore_loan_creation(transaction: &LoanContractTransaction, db: &Db) { pub async fn restore_loan_creation(transaction: &LoanContractTransaction, _db: &Db) {
let loan = &transaction.unsigned_loan_contract; if let Err(err) = restore_loan_contract_row(transaction).await {
let Some(collateral) = loan_collateral_asset_name_or_base(&loan.collateral, &BASECOIN) else { warn!(
return; "[mempool_restore] failed to restore rolled-back loan_contract txid={} err={err}",
}; transaction.hash
let lender_spendable = balance_checkup( );
db, }
loan.loan_amount,
loan.txfee,
loan.loan_coin.clone(),
&loan.lender,
)
.await;
let borrower_spendable =
balance_checkup(db, loan.collateral_amount, 0, collateral, &loan.borrower).await;
let signatures = vec![
transaction.signature1.clone(),
transaction.signature2.clone(),
];
restore_if_spendable(
&signatures,
lender_spendable && borrower_spendable,
|| async {
let _ = transaction.add_to_memory().await;
},
)
.await;
} }
pub async fn restore_borrower(transaction: &ContractPaymentTransaction, db: &Db) { pub async fn restore_borrower(transaction: &ContractPaymentTransaction, db: &Db) {
@ -186,9 +260,15 @@ pub async fn restore_borrower(transaction: &ContractPaymentTransaction, db: &Db)
let spendable = balance_checkup(db, value, payment.txfee, loan_coin, &payment.address).await; let spendable = balance_checkup(db, value, payment.txfee, loan_coin, &payment.address).await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable_void(
"loan_payment",
"unknown",
&[signature],
spendable,
|| async {
let _ = transaction.add_to_memory().await; let _ = transaction.add_to_memory().await;
}) },
)
.await; .await;
} }
@ -204,9 +284,15 @@ pub async fn restore_collateral(transaction: &CollateralClaimTransaction, db: &D
.await; .await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable_void(
"collateral_claim",
"unknown",
&[signature],
spendable,
|| async {
let _ = transaction.add_to_memory().await; let _ = transaction.add_to_memory().await;
}) },
)
.await; .await;
} }
@ -215,7 +301,7 @@ pub async fn restore_vanity(transaction: &VanityAddressTransaction, db: &Db) {
let spendable = balance_checkup(db, 0, vanity.txfee, BASECOIN.clone(), &vanity.address).await; let spendable = balance_checkup(db, 0, vanity.txfee, BASECOIN.clone(), &vanity.address).await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable_void("vanity", "unknown", &[signature], spendable, || async {
let _ = transaction.add_to_memory().await; let _ = transaction.add_to_memory().await;
}) })
.await; .await;

View File

@ -264,21 +264,35 @@ impl NodeInfo {
// Once the chain is mature, adding nodes is restricted to older // Once the chain is mature, adding nodes is restricted to older
// active participants with sufficient mined history. // active participants with sufficient mined history.
if get_height(&db) > 10000 { let local_height = get_height(&db);
if !Self::self_add_allowed_at_height(local_height) {
let signer_key = Wallet::normalize_to_short_address(&edit.modified_by) let signer_key = Wallet::normalize_to_short_address(&edit.modified_by)
.unwrap_or_else(|| edit.modified_by.clone()); .unwrap_or_else(|| edit.modified_by.clone());
let signer_node = address_map.get(&signer_key); let signer_node = address_map.get(&signer_key);
let signer_is_local = signer_key == wallet.saved.short_address;
let valid_added_by = signer_node let valid_added_by = signer_node
.map(|node| { .map(|node| {
current_timestamp.saturating_sub(node.added_timestamp) >= ONE_HOUR_MILLIS current_timestamp.saturating_sub(node.added_timestamp) >= ONE_HOUR_MILLIS
&& node.deleted_timestamp == 0 && node.deleted_timestamp == 0
&& (!signer_is_local
|| Self::local_runtime_is_mature(current_timestamp))
}) })
.unwrap_or(false); .unwrap_or(false);
if !valid_added_by { if !valid_added_by {
return RpcResponse::Binary( let signer_exists = signer_node.is_some();
b"Error: This address cannot add nodes. It must exist for at least 60 minutes and not be marked for deletion" let signer_deleted_timestamp =
.to_vec(), signer_node.map(|node| node.deleted_timestamp).unwrap_or(0);
); let signer_age_ms = signer_node
.map(|node| current_timestamp.saturating_sub(node.added_timestamp))
.unwrap_or(0);
let signer_blocks_mined =
signer_node.map(|node| node.blocks_mined).unwrap_or(0);
return RpcResponse::Binary(format!(
"Error: This address cannot add nodes. It must exist for at least 60 minutes and not be marked for deletion local_height={local_height} self_add_block={} self_add_limit={} signer={signer_key} signer_exists={signer_exists} signer_is_local={signer_is_local} signer_deleted_timestamp={signer_deleted_timestamp} signer_age_ms={signer_age_ms} signer_blocks_mined={signer_blocks_mined}",
super::SELF_ADD_BLOCK,
Self::self_add_limit_height(),
)
.into_bytes());
} }
let mined_count = signer_node.map(|node| node.blocks_mined).unwrap_or(0); let mined_count = signer_node.map(|node| node.blocks_mined).unwrap_or(0);
if mined_count < 100 { if mined_count < 100 {

View File

@ -21,12 +21,18 @@ use crate::wallets::structures::Wallet;
use crate::Arc; use crate::Arc;
use crate::HashMap; use crate::HashMap;
use crate::Mutex; use crate::Mutex;
use crate::OnceLock;
use crate::Utc; use crate::Utc;
lazy_static! { lazy_static! {
static ref ADDRESS_MAP: Mutex<HashMap<String, NodeInfo>> = Mutex::new(HashMap::new()); static ref ADDRESS_MAP: Mutex<HashMap<String, NodeInfo>> = Mutex::new(HashMap::new());
} }
pub const SELF_ADD_BLOCK: u32 = 34_000;
pub const SELF_ADD_WINDOW_BLOCKS: u32 = 10_000;
static NODE_RUNTIME_STARTED_MILLIS: OnceLock<u64> = OnceLock::new();
#[derive(Debug)] #[derive(Debug)]
pub struct NodeInfo { pub struct NodeInfo {
ip: String, ip: String,
@ -40,6 +46,26 @@ pub struct NodeInfo {
} }
impl NodeInfo { impl NodeInfo {
pub fn initialize_runtime_start() {
let _ = NODE_RUNTIME_STARTED_MILLIS.set(Utc::now().timestamp_millis() as u64);
}
fn runtime_started_millis() -> u64 {
*NODE_RUNTIME_STARTED_MILLIS.get_or_init(|| Utc::now().timestamp_millis() as u64)
}
fn local_runtime_is_mature(current_timestamp: u64) -> bool {
current_timestamp.saturating_sub(Self::runtime_started_millis()) >= 3_600_000
}
pub fn self_add_allowed_at_height(height: u32) -> bool {
height <= SELF_ADD_BLOCK.saturating_add(SELF_ADD_WINDOW_BLOCKS)
}
pub fn self_add_limit_height() -> u32 {
SELF_ADD_BLOCK.saturating_add(SELF_ADD_WINDOW_BLOCKS)
}
fn new( fn new(
ip: String, ip: String,
blocks_mined: u8, blocks_mined: u8,

View File

@ -24,6 +24,7 @@ impl NodeInfo {
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,
local_address: &str,
current_timestamp: u64, current_timestamp: u64,
deleted_block: u32, deleted_block: u32,
) { ) {
@ -31,6 +32,7 @@ impl NodeInfo {
while let Some(address) = stack.pop() { while let Some(address) = stack.pop() {
let should_cascade = match address_map.get_mut(&address) { let should_cascade = match address_map.get_mut(&address) {
Some(_) if address == local_address => false,
Some(node) if node.deleted_timestamp == 0 && node.monitoring.is_empty() => { Some(node) if node.deleted_timestamp == 0 && node.monitoring.is_empty() => {
node.deleted_timestamp = current_timestamp; node.deleted_timestamp = current_timestamp;
node.deleted_block = deleted_block; node.deleted_block = deleted_block;
@ -155,6 +157,7 @@ impl NodeInfo {
Self::mark_deleted_and_cascade( Self::mark_deleted_and_cascade(
&mut address_map, &mut address_map,
&edit.monitored_address, &edit.monitored_address,
&local_short,
current_timestamp, current_timestamp,
deleted_block, deleted_block,
); );

View File

@ -375,7 +375,34 @@ pub async fn process_handshake_response(
))); )));
} }
if params.first { let remote_height = request_remote_height(
broadcast_stream.clone(),
params.map.clone(),
connections_key.clone(),
)
.await
.map_err(|err| {
let connections_key = connections_key.clone();
tokio::spawn(async move {
remove_key_from_memory(&connections_key).await;
});
io::Error::other(format!(
"Remote height lookup failed before network join: {err}"
))
})?;
let self_add_block = crate::records::memory::network_mapping::SELF_ADD_BLOCK;
let self_add_limit = crate::records::memory::network_mapping::NodeInfo::self_add_limit_height();
let join_mode = if crate::records::memory::network_mapping::NodeInfo::self_add_allowed_at_height(
remote_height,
) {
"bootstrap_self_add"
} else {
"sponsored_add"
};
info!(
"[handshake_state] peer={connections_key} remote_height={remote_height} self_add_block={self_add_block} self_add_limit={self_add_limit} join_mode={join_mode}"
);
announce_self_to_network( announce_self_to_network(
broadcast_stream.clone(), broadcast_stream.clone(),
&wallet.saved.short_address.clone(), &wallet.saved.short_address.clone(),
@ -390,9 +417,11 @@ pub async fn process_handshake_response(
tokio::spawn(async move { tokio::spawn(async move {
remove_key_from_memory(&connections_key).await; remove_key_from_memory(&connections_key).await;
}); });
io::Error::other(format!("Network map sync failed: {err}")) io::Error::other(format!("Network join/map sync failed: {err}"))
})?; })?;
mark_peer_network_map_synced(&connections_key).await; mark_peer_network_map_synced(&connections_key).await;
if params.first {
let bsparams = BootstrapParams { let bsparams = BootstrapParams {
stream: Arc::clone(&stream), stream: Arc::clone(&stream),
connections_key: connections_key.clone(), connections_key: connections_key.clone(),
@ -405,23 +434,6 @@ pub async fn process_handshake_response(
spawn_bootstrap_peer_discovery(bsparams); spawn_bootstrap_peer_discovery(bsparams);
} else { } else {
announce_self_to_network(
broadcast_stream.clone(),
&wallet.saved.short_address.clone(),
params.map.clone(),
&params.db.clone(),
params.wallet.clone(),
&connections_key,
)
.await
.map_err(|err| {
let connections_key = connections_key.clone();
tokio::spawn(async move {
remove_key_from_memory(&connections_key).await;
});
io::Error::other(format!("Network map sync failed: {err}"))
})?;
mark_peer_network_map_synced(&connections_key).await;
if crate::miner::flag::is_normal_mode() { if crate::miner::flag::is_normal_mode() {
mark_peer_operational(&connections_key, params.map.clone()).await; mark_peer_operational(&connections_key, params.map.clone()).await;
} }

View File

@ -4,11 +4,14 @@ use crate::blocks::loans::LoanContractTransaction;
use crate::common::loan_schedule::overdue_payment_count; use crate::common::loan_schedule::overdue_payment_count;
use crate::encode; use crate::encode;
use crate::records::memory::mempool::get_pending_payments_for_contract; use crate::records::memory::mempool::get_pending_payments_for_contract;
use crate::records::record_chain::wallet_tx_index::WALLET_TX_INDEX_TREE;
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;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::{Local, TimeZone, Utc}; use crate::{Local, TimeZone, Utc};
use std::collections::{BTreeMap, BTreeSet};
use std::ops::Bound;
fn format_amount(value: u64) -> f64 { fn format_amount(value: u64) -> f64 {
// Contract RPC output presents coin amounts as user-facing decimal values. // Contract RPC output presents coin amounts as user-facing decimal values.
@ -52,9 +55,10 @@ async fn load_contract(db: &Db, hash: &[u8]) -> Result<LoanContractTransaction,
.map_err(|e| format!("error: Failed to parse loan contract: {e}")) .map_err(|e| format!("error: Failed to parse loan contract: {e}"))
} }
async fn collect_contract_activity( async fn collect_contract_activity_from_txids(
db: &Db, db: &Db,
contract_hash: &str, contract_hash: &str,
txids: Vec<Vec<u8>>,
) -> Result< ) -> Result<
( (
Vec<ContractPaymentTransaction>, Vec<ContractPaymentTransaction>,
@ -62,16 +66,12 @@ async fn collect_contract_activity(
), ),
String, String,
> { > {
// Scan saved txids to gather all payments and any collateral-claim // Inspect only txids already indexed to the contract parties. This keeps
// transaction associated with the requested contract. // loan lookups bounded by wallet activity instead of total chain size.
let tree = db
.open_tree("txid")
.map_err(|e| format!("error: Failed to open txid tree: {e}"))?;
let mut payments = Vec::new(); let mut payments = Vec::new();
let mut collateral_claim: Option<CollateralClaimTransaction> = None; let mut collateral_claim: Option<CollateralClaimTransaction> = None;
for entry in tree.iter() { for txid_bytes in txids {
let (txid_bytes, _) = entry.map_err(|e| format!("error: Failed to read txid tree: {e}"))?;
let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await; let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await;
if bytes.is_empty() { if bytes.is_empty() {
continue; continue;
@ -112,19 +112,24 @@ async fn collect_contract_activity(
Ok((payments, collateral_claim)) Ok((payments, collateral_claim))
} }
async fn build_contract_summary(hash: Vec<u8>, db: &Db) -> Result<String, String> { async fn build_contract_summary_from_parts(
contract_hash: String,
contract: LoanContractTransaction,
mut payments: Vec<ContractPaymentTransaction>,
collateral_claim: Option<CollateralClaimTransaction>,
db: &Db,
) -> Result<String, String> {
// Build the text view of a contract by combining the original loan, // Build the text view of a contract by combining the original loan,
// its payments, and any collateral-claim activity into one summary. // its payments, and any collateral-claim activity into one summary.
let contract_hash = encode(&hash); payments.sort_by_key(|payment| payment.unsigned_contract_payment.timestamp);
let contract = load_contract(db, &hash).await?;
let (payments, collateral_claim) = collect_contract_activity(db, &contract_hash).await?;
let loan_tree = db let loan_tree = db
.open_tree("loan") .open_tree("loan")
.map_err(|err| format!("error: Failed to open loan tree: {err}"))?; .map_err(|err| format!("error: Failed to open loan tree: {err}"))?;
// The loan tree stores whether collateral remains claimable; if the // The loan tree stores whether collateral remains claimable; if the
// tree does not have the row, fall back to discovered claim history. // tree does not have the row, fall back to discovered claim history.
let collateral_already_claimed = match loan_tree.get(&hash) { let contract_hash_bytes = crate::decode(&contract_hash)
.map_err(|err| format!("error: Invalid contract hash: {err}"))?;
let collateral_already_claimed = match loan_tree.get(&contract_hash_bytes) {
Ok(Some(value)) => value == b"false", Ok(Some(value)) => value == b"false",
_ => collateral_claim.is_some(), _ => collateral_claim.is_some(),
}; };
@ -216,6 +221,16 @@ async fn build_contract_summary(hash: Vec<u8>, db: &Db) -> Result<String, String
Ok(output) Ok(output)
} }
async fn build_contract_summary(hash: Vec<u8>, db: &Db) -> Result<String, String> {
let contract_hash = encode(&hash);
let contract = load_contract(db, &hash).await?;
let loan = &contract.unsigned_loan_contract;
let txids = indexed_txids_for_addresses(db, &[loan.lender.trim(), loan.borrower.trim()])?;
let (payments, collateral_claim) =
collect_contract_activity_from_txids(db, &contract_hash, txids).await?;
build_contract_summary_from_parts(contract_hash, contract, payments, collateral_claim, db).await
}
pub async fn contract_details(hash: Vec<u8>, db: &Db) -> RpcResponse { pub async fn contract_details(hash: Vec<u8>, db: &Db) -> RpcResponse {
// Return the text summary for one contract hash. // Return the text summary for one contract hash.
let output = match build_contract_summary(hash, db).await { let output = match build_contract_summary(hash, db).await {
@ -225,6 +240,57 @@ pub async fn contract_details(hash: Vec<u8>, db: &Db) -> RpcResponse {
RpcResponse::Binary(output.into_bytes()) RpcResponse::Binary(output.into_bytes())
} }
fn prefix_successor(prefix: &[u8]) -> Option<Vec<u8>> {
let mut next = prefix.to_vec();
for index in (0..next.len()).rev() {
if next[index] != u8::MAX {
next[index] += 1;
next.truncate(index + 1);
return Some(next);
}
}
None
}
fn indexed_txids_for_address(db: &Db, address: &str) -> Result<Vec<Vec<u8>>, String> {
let address_bytes = Wallet::short_address_to_bytes(address)
.ok_or_else(|| "error: Invalid wallet address".to_string())?;
let tree = db
.open_tree(WALLET_TX_INDEX_TREE)
.map_err(|err| format!("error: Failed to open wallet transaction index: {err}"))?;
let start = Bound::Included(address_bytes.clone());
let end = prefix_successor(&address_bytes)
.map(Bound::Excluded)
.unwrap_or(Bound::Unbounded);
let mut txids = Vec::new();
let mut seen = BTreeSet::new();
for entry in tree.range((start, end)) {
let (_key, value) = entry
.map_err(|err| format!("error: Failed to read wallet transaction index: {err}"))?;
if value.len() == 32 && seen.insert(value.to_vec()) {
txids.push(value.to_vec());
}
}
Ok(txids)
}
fn indexed_txids_for_addresses(db: &Db, addresses: &[&str]) -> Result<Vec<Vec<u8>>, String> {
let mut seen = BTreeSet::new();
let mut txids = Vec::new();
for address in addresses {
for txid in indexed_txids_for_address(db, address.trim())? {
if seen.insert(txid.clone()) {
txids.push(txid);
}
}
}
Ok(txids)
}
pub async fn contract_details_by_address(address: String, db: &Db) -> RpcResponse { pub async fn contract_details_by_address(address: String, db: &Db) -> RpcResponse {
// Return every saved contract where the address appears as either the // Return every saved contract where the address appears as either the
// lender or borrower, each expanded into the same text summary view. // lender or borrower, each expanded into the same text summary view.
@ -232,27 +298,13 @@ pub async fn contract_details_by_address(address: String, db: &Db) -> RpcRespons
return RpcResponse::Binary(b"error: Invalid wallet address".to_vec()); return RpcResponse::Binary(b"error: Invalid wallet address".to_vec());
}; };
let tree = match db.open_tree("txid") { let txids = match indexed_txids_for_address(db, &address) {
Ok(tree) => tree, Ok(txids) => txids,
Err(err) => { Err(err) => return RpcResponse::Binary(err.into_bytes()),
return RpcResponse::Binary(
format!("error: Failed to open txid tree: {err}").into_bytes(),
)
}
};
let mut contracts = Vec::new();
for entry in tree.iter() {
let (txid_bytes, _) = match entry {
Ok(entry) => entry,
Err(err) => {
return RpcResponse::Binary(
format!("error: Failed to read txid tree: {err}").into_bytes(),
)
}
}; };
let mut contracts = BTreeMap::<String, LoanContractTransaction>::new();
for txid_bytes in txids {
let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await; let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await;
if bytes.is_empty() { if bytes.is_empty() {
continue; continue;
@ -263,28 +315,47 @@ pub async fn contract_details_by_address(address: String, db: &Db) -> RpcRespons
continue; continue;
} }
// Only loan-contract transactions can be expanded into the
// contract summary returned by this RPC.
let contract = match LoanContractTransaction::from_bytes(txtype, &bytes[1..]).await { let contract = match LoanContractTransaction::from_bytes(txtype, &bytes[1..]).await {
Ok(contract) => contract, Ok(contract) => contract,
Err(_) => continue, Err(_) => continue,
}; };
if contract.unsigned_loan_contract.lender == address
if contract.unsigned_loan_contract.lender != address || contract.unsigned_loan_contract.borrower == address
&& contract.unsigned_loan_contract.borrower != address
{ {
continue; contracts.insert(encode(&txid_bytes), contract);
}
} }
let summary = match build_contract_summary(txid_bytes.to_vec(), db).await { let mut summaries = Vec::new();
for (contract_hash, contract) in contracts {
let loan = &contract.unsigned_loan_contract;
let txids =
match indexed_txids_for_addresses(db, &[loan.lender.trim(), loan.borrower.trim()]) {
Ok(txids) => txids,
Err(err) => return RpcResponse::Binary(err.into_bytes()),
};
let (payments, collateral_claim) =
match collect_contract_activity_from_txids(db, &contract_hash, txids).await {
Ok(activity) => activity,
Err(err) => return RpcResponse::Binary(err.into_bytes()),
};
let summary = match build_contract_summary_from_parts(
contract_hash,
contract,
payments,
collateral_claim,
db,
)
.await
{
Ok(summary) => summary, Ok(summary) => summary,
Err(err) => return RpcResponse::Binary(err.into_bytes()), Err(err) => return RpcResponse::Binary(err.into_bytes()),
}; };
contracts.push(summary); summaries.push(summary);
} }
let mut output = format!("address={}\ncontracts={}\n", address, contracts.len()); let mut output = format!("address={}\ncontracts={}\n", address, summaries.len());
for (index, contract) in contracts.iter().enumerate() { for (index, contract) in summaries.iter().enumerate() {
output.push_str(&format!("contract_{}:\n{}\n", index + 1, contract)); output.push_str(&format!("contract_{}:\n{}\n", index + 1, contract));
} }

View File

@ -19,7 +19,7 @@ use crate::records::memory::response_channels::{reserve_transient_entry_with_con
use crate::rpc::command_maps::RPC_SUBMIT_TRANSACTION; use crate::rpc::command_maps::RPC_SUBMIT_TRANSACTION;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::sled::Db; use crate::sled::Db;
use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory; use crate::torrent::torrenting_system::get_nodes::get_live_node_broadcast_peers;
use crate::Arc; use crate::Arc;
use crate::Mutex; use crate::Mutex;
use crate::OnceLock; use crate::OnceLock;
@ -33,7 +33,7 @@ fn loan_payment_submit_lock() -> &'static Mutex<()> {
async fn broadcast_tx(tx_bytes: Vec<u8>, map: Arc<Mutex<Command>>) { async fn broadcast_tx(tx_bytes: Vec<u8>, map: Arc<Mutex<Command>>) {
// Broadcast newly accepted mempool transactions only to miner peers, // Broadcast newly accepted mempool transactions only to miner peers,
// since those are the nodes that need transaction fan-out most. // since those are the nodes that need transaction fan-out most.
let nodes = get_nodes_from_memory().await; let nodes = get_live_node_broadcast_peers().await;
for (key, stream) in nodes { for (key, stream) in nodes {
let client_type = get_client_type_from_memory(&key) let client_type = get_client_type_from_memory(&key)

View File

@ -25,7 +25,7 @@ use crate::rpc::server::structs::{CombineAndSendDataParams, HandshakeTestParams}
use crate::rpc::server::tests::{endpoint_port, is_port_open}; use crate::rpc::server::tests::{endpoint_port, is_port_open};
use crate::sled::Db; use crate::sled::Db;
use crate::sleep; use crate::sleep;
use crate::startup::network_broadcast::announce_self_to_network; use crate::startup::network_broadcast::{announce_self_to_network, get_network_mapping};
use crate::startup::remote_height::request_remote_height; use crate::startup::remote_height::request_remote_height;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::Arc; use crate::Arc;
@ -210,11 +210,27 @@ async fn complete_incoming_miner_setup(
return; return;
} }
if get_height(db) <= 10_000 { let remote_height =
match request_remote_height(stream.clone(), map.clone(), connections_key.to_string()).await
{
Ok(height) => height,
Err(err) => {
error!(
"[startup] incoming peer remote height lookup failed before network join: {err}"
);
remove_key_from_memory(connections_key).await;
return;
}
};
let self_add_allowed =
crate::records::memory::network_mapping::NodeInfo::self_add_allowed_at_height(
remote_height,
);
if self_add_allowed {
// Before the sponsored-add gate, reverse announcement lets tiny early // Before the sponsored-add gate, reverse announcement lets tiny early
// networks bootstrap from one live peer. After the gate activates, the // networks bootstrap from one live peer. After the gate activates, the
// incoming peer may have no local history yet, so only its outgoing // incoming peer's outgoing join request is the only add path.
// self-announcement should be sponsored by this established node.
let short_address = wallet.saved.short_address.clone(); let short_address = wallet.saved.short_address.clone();
if let Err(err) = announce_self_to_network( if let Err(err) = announce_self_to_network(
stream.clone(), stream.clone(),
@ -230,6 +246,18 @@ async fn complete_incoming_miner_setup(
remove_key_from_memory(connections_key).await; remove_key_from_memory(connections_key).await;
return; return;
} }
} else if let Err(err) = get_network_mapping(
stream.clone(),
map.clone(),
db,
wallet.clone(),
connections_key,
)
.await
{
error!("[startup] incoming peer network map import failed: {err}");
remove_key_from_memory(connections_key).await;
return;
} }
mark_peer_network_map_synced(connections_key).await; mark_peer_network_map_synced(connections_key).await;

View File

@ -7,6 +7,7 @@ use crate::miner::flag::{
request_mining_stop, set_mining_state, set_node_mode, MiningState, NodeMode, request_mining_stop, set_mining_state, set_node_mode, MiningState, NodeMode,
}; };
use crate::records::memory::connections::initialize_connection; use crate::records::memory::connections::initialize_connection;
use crate::records::memory::network_mapping::NodeInfo;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::Path; use crate::Path;
@ -68,6 +69,7 @@ pub async fn prepare_pre_wallet_startup() {
set_node_mode(NodeMode::Startup); set_node_mode(NodeMode::Startup);
request_mining_stop(); request_mining_stop();
set_mining_state(MiningState::Idle); set_mining_state(MiningState::Idle);
NodeInfo::initialize_runtime_start();
initialize_connection().await; initialize_connection().await;
create_file_paths().await; create_file_paths().await;
} }

View File

@ -7,7 +7,7 @@ use crate::records::memory::response_channels::{reserve_transient_entry_with_con
use crate::rpc::command_maps::RPC_SUBMIT_TORRENT; use crate::rpc::command_maps::RPC_SUBMIT_TORRENT;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::torrent::structs::{Info, Torrent}; use crate::torrent::structs::{Info, Torrent};
use crate::torrent::torrenting_system::get_nodes::get_torrent_broadcast_nodes_from_memory; use crate::torrent::torrenting_system::get_nodes::get_live_node_broadcast_peers;
use crate::torrent::torrenting_system::torrent_cache::should_broadcast_torrent; use crate::torrent::torrenting_system::torrent_cache::should_broadcast_torrent;
use crate::Arc; use crate::Arc;
use crate::File; use crate::File;
@ -158,8 +158,8 @@ pub async fn broadcast_new_torrent_to_peers(
} }
let torrent_len = 4 + torrent_bytes.len() as u32; let torrent_len = 4 + torrent_bytes.len() as u32;
// Send the torrent to all currently connected miner peers. // Send the torrent to the same live node-peer set used by transaction relay.
let peers = get_torrent_broadcast_nodes_from_memory().await; let peers = get_live_node_broadcast_peers().await;
for (connections_key, stream) in peers { for (connections_key, stream) in peers {
// Each peer gets a short-lived reply mapping so the ack can be drained. // Each peer gets a short-lived reply mapping so the ack can be drained.
let uid_bytes = reserve_transient_entry_with_context( let uid_bytes = reserve_transient_entry_with_context(

View File

@ -56,11 +56,11 @@ pub async fn get_sync_nodes_from_memory() -> Vec<(String, Arc<Mutex<TcpStream>>)
nodes nodes
} }
pub async fn get_torrent_broadcast_nodes_from_memory() -> Vec<(String, Arc<Mutex<TcpStream>>)> { pub async fn get_live_node_broadcast_peers() -> Vec<(String, Arc<Mutex<TcpStream>>)> {
// Torrent announcements are allowed to reach miner peers that are // Live node broadcasts go to long-lived node peers that completed the
// still starting/syncing so they can stage new candidates while // wallet-registry and network-map phases. They do not require the peer to
// catching up. Consensus/piece-selection paths must keep using // be fully operational/ready, because syncing peers still need live
// get_nodes_from_memory(), which requires ready peers. // transactions, torrents, and network events staged while catching up.
let connection_storage = CONNECTIONS.read().await; let connection_storage = CONNECTIONS.read().await;
let mut nodes = Vec::new(); let mut nodes = Vec::new();
if let Some(connection) = &*connection_storage { if let Some(connection) = &*connection_storage {
@ -68,6 +68,9 @@ pub async fn get_torrent_broadcast_nodes_from_memory() -> Vec<(String, Arc<Mutex
if ClientType::from_bytes(&connection_info.client_type) != Some(ClientType::Miner) { if ClientType::from_bytes(&connection_info.client_type) != Some(ClientType::Miner) {
continue; continue;
} }
if !connection_info.wallet_registry_synced || !connection_info.network_map_synced {
continue;
}
let ip = binary_to_ip(connection_info.ip.clone()); let ip = binary_to_ip(connection_info.ip.clone());
let port = connection_info.port; let port = connection_info.port;
let key = format!("{ip}:{port}"); let key = format!("{ip}:{port}");