Fix staged torrent candidate race

This commit is contained in:
viraladmin 2026-05-26 00:24:57 -06:00
parent f92823ac90
commit 61a64cf538
131 changed files with 5957 additions and 5644 deletions

View File

@ -11,10 +11,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// Pad the asset name so it matches the 15-byte on-chain asset format. // Pad the asset name so it matches the 15-byte on-chain asset format.
fn pad_to_width(input: &str, width: usize) -> String { fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); let mut result = String::with_capacity(width);
let _ = std::fmt::write( let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
&mut result,
format_args!("{input:<width$}"),
);
result result
} }

View File

@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// Pad the ticker so it matches the fixed-width 15-byte on-chain format. // Pad the ticker so it matches the fixed-width 15-byte on-chain format.
fn pad_to_width(input: &str, width: usize) -> String { fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); let mut result = String::with_capacity(width);
let _ = std::fmt::write( let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
&mut result,
format_args!("{input:<width$}"),
);
result result
} }

View File

@ -11,10 +11,7 @@ use blockchain::{Local, LocalResult, NaiveDate, NaiveTime, TimeZone};
fn pad_to_width(input: &str, width: usize) -> String { fn pad_to_width(input: &str, width: usize) -> String {
// Asset names are fixed-width fields in the loan transaction bytes. // Asset names are fixed-width fields in the loan transaction bytes.
let mut result = String::with_capacity(width); let mut result = String::with_capacity(width);
let _ = std::fmt::write( let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
&mut result,
format_args!("{input:<width$}"),
);
result result
} }

View File

@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters // pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String { fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write( let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
&mut result,
format_args!("{input:<width$}"),
);
result result
} }

View File

@ -40,9 +40,7 @@ async fn main() {
// Refuse to overwrite an existing wallet file. // Refuse to overwrite an existing wallet file.
if let Ok(metadata) = metadata(&wallet_path).await { if let Ok(metadata) = metadata(&wallet_path).await {
if metadata.is_file() { if metadata.is_file() {
eprintln!( eprintln!("Error: Wallet already exists at the specified path: {wallet_path:?}");
"Error: Wallet already exists at the specified path: {wallet_path:?}"
);
std::process::exit(1); std::process::exit(1);
} }
} }

View File

@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters // pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String { fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write( let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
&mut result,
format_args!("{input:<width$}"),
);
result result
} }

View File

@ -12,10 +12,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters // pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String { fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write( let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
&mut result,
format_args!("{input:<width$}"),
);
result result
} }

View File

@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters // pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String { fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write( let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
&mut result,
format_args!("{input:<width$}"),
);
result result
} }

View File

@ -13,10 +13,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters // pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String { fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write( let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
&mut result,
format_args!("{input:<width$}"),
);
result result
} }

View File

@ -26,17 +26,50 @@ async fn decode_one_transaction(tx_bytes: &[u8]) -> Option<String> {
let body = &tx_bytes[1..]; let body = &tx_bytes[1..];
match txtype { match txtype {
TRANSFER_TYPE => to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok(), TRANSFER_TYPE => {
CREATE_TOKEN_TYPE => to_string_pretty(&CreateTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(), to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok()
CREATE_NFT_TYPE => to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok(), }
MARKETING_TYPE => to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok(), CREATE_TOKEN_TYPE => to_string_pretty(
&CreateTokenTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok(),
CREATE_NFT_TYPE => {
to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok()
}
MARKETING_TYPE => {
to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok()
}
SWAP_TYPE => to_string_pretty(&SwapTransaction::from_bytes(txtype, body).await.ok()?).ok(), SWAP_TYPE => to_string_pretty(&SwapTransaction::from_bytes(txtype, body).await.ok()?).ok(),
LENDER_TYPE => to_string_pretty(&LoanContractTransaction::from_bytes(txtype, body).await.ok()?).ok(), LENDER_TYPE => to_string_pretty(
BORROWER_TYPE => to_string_pretty(&ContractPaymentTransaction::from_bytes(txtype, body).await.ok()?).ok(), &LoanContractTransaction::from_bytes(txtype, body)
COLLATERAL_TYPE => to_string_pretty(&CollateralClaimTransaction::from_bytes(txtype, body).await.ok()?).ok(), .await
.ok()?,
)
.ok(),
BORROWER_TYPE => to_string_pretty(
&ContractPaymentTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok(),
COLLATERAL_TYPE => to_string_pretty(
&CollateralClaimTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok(),
BURN_TYPE => to_string_pretty(&BurnTransaction::from_bytes(txtype, body).await.ok()?).ok(), BURN_TYPE => to_string_pretty(&BurnTransaction::from_bytes(txtype, body).await.ok()?).ok(),
ISSUE_TOKEN_TYPE => to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(), ISSUE_TOKEN_TYPE => {
VANITY_ADDRESS_TYPE => to_string_pretty(&VanityAddressTransaction::from_bytes(txtype, body).await.ok()?).ok(), to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok()
}
VANITY_ADDRESS_TYPE => to_string_pretty(
&VanityAddressTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok(),
_ => None, _ => None,
} }
} }

View File

@ -27,17 +27,50 @@ async fn decode_mempool_transaction(response: &[u8]) -> Option<String> {
let body = &response[1..]; let body = &response[1..];
match txtype { match txtype {
TRANSFER_TYPE => to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok(), TRANSFER_TYPE => {
CREATE_TOKEN_TYPE => to_string_pretty(&CreateTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(), to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok()
CREATE_NFT_TYPE => to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok(), }
MARKETING_TYPE => to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok(), CREATE_TOKEN_TYPE => to_string_pretty(
&CreateTokenTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok(),
CREATE_NFT_TYPE => {
to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok()
}
MARKETING_TYPE => {
to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok()
}
SWAP_TYPE => to_string_pretty(&SwapTransaction::from_bytes(txtype, body).await.ok()?).ok(), SWAP_TYPE => to_string_pretty(&SwapTransaction::from_bytes(txtype, body).await.ok()?).ok(),
LENDER_TYPE => to_string_pretty(&LoanContractTransaction::from_bytes(txtype, body).await.ok()?).ok(), LENDER_TYPE => to_string_pretty(
BORROWER_TYPE => to_string_pretty(&ContractPaymentTransaction::from_bytes(txtype, body).await.ok()?).ok(), &LoanContractTransaction::from_bytes(txtype, body)
COLLATERAL_TYPE => to_string_pretty(&CollateralClaimTransaction::from_bytes(txtype, body).await.ok()?).ok(), .await
.ok()?,
)
.ok(),
BORROWER_TYPE => to_string_pretty(
&ContractPaymentTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok(),
COLLATERAL_TYPE => to_string_pretty(
&CollateralClaimTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok(),
BURN_TYPE => to_string_pretty(&BurnTransaction::from_bytes(txtype, body).await.ok()?).ok(), BURN_TYPE => to_string_pretty(&BurnTransaction::from_bytes(txtype, body).await.ok()?).ok(),
ISSUE_TOKEN_TYPE => to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(), ISSUE_TOKEN_TYPE => {
VANITY_ADDRESS_TYPE => to_string_pretty(&VanityAddressTransaction::from_bytes(txtype, body).await.ok()?).ok(), to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok()
}
VANITY_ADDRESS_TYPE => to_string_pretty(
&VanityAddressTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok(),
_ => None, _ => None,
} }
} }

View File

@ -45,10 +45,9 @@ fn decode_network_info(response: &[u8]) -> Option<String> {
// Mainnet uses CLC and testnet uses CLTC, so the prefix length is // Mainnet uses CLC and testnet uses CLTC, so the prefix length is
// inferred from the total payload size instead of hard-coded. // inferred from the total payload size instead of hard-coded.
let wallet_prefix = let wallet_prefix = String::from_utf8_lossy(response.get(offset..offset + wallet_prefix_len)?)
String::from_utf8_lossy(response.get(offset..offset + wallet_prefix_len)?) .trim()
.trim() .to_string();
.to_string();
offset += wallet_prefix_len; offset += wallet_prefix_len;
let height = read_u32(response, &mut offset)?; let height = read_u32(response, &mut offset)?;

View File

@ -89,9 +89,7 @@ async fn main() {
let address_file_path = if args.len() == 2 { let address_file_path = if args.len() == 2 {
args[1].clone() args[1].clone()
} else { } else {
match prompt_for_path( match prompt_for_path("Please enter the path to the file containing the wallet address: ") {
"Please enter the path to the file containing the wallet address: ",
) {
Ok(path) => path, Ok(path) => path,
Err(err) => { Err(err) => {
eprintln!("{err}"); eprintln!("{err}");

View File

@ -1,5 +1,5 @@
use blockchain::common::skein::{ use blockchain::common::skein::{
skein_256_hash_data, skein_256_hash_bytes, skein_128_hash_bytes, skein_128_hash_data, skein_128_hash_bytes, skein_128_hash_data, skein_256_hash_bytes, skein_256_hash_data,
}; };
use blockchain::env; use blockchain::env;
use blockchain::File; use blockchain::File;

View File

@ -1,189 +1,189 @@
use blockchain::common::binary_conversions::hex_to_u64; use blockchain::common::binary_conversions::hex_to_u64;
use blockchain::common::network_paths_and_settings::block_extension_and_paths; use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::skein::{skein_256_hash_data, skein_128_hash_bytes}; use blockchain::common::skein::{skein_128_hash_bytes, skein_256_hash_data};
use blockchain::encode; use blockchain::encode;
use blockchain::env; use blockchain::env;
use blockchain::records::unpack_block::unpack_header::load_block_header; use blockchain::records::unpack_block::unpack_header::load_block_header;
use blockchain::records::wallet_registry::resolve_pubkey_from_short_address; use blockchain::records::wallet_registry::resolve_pubkey_from_short_address;
use blockchain::torrent::structs::Torrent; use blockchain::torrent::structs::Torrent;
use blockchain::wallets::structures::Wallet; use blockchain::wallets::structures::Wallet;
use blockchain::{AsyncReadExt, File}; use blockchain::{AsyncReadExt, File};
use colored::*; use colored::*;
use std::process; use std::process;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Validate that a local block file, block header, and torrent metadata agree. // Validate that a local block file, block header, and torrent metadata agree.
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
if args.len() != 2 { if args.len() != 2 {
eprintln!("Usage: {} <block_number>", args[0]); eprintln!("Usage: {} <block_number>", args[0]);
process::exit(1); process::exit(1);
} }
let block_number: u32 = match args[1].parse() { let block_number: u32 = match args[1].parse() {
Ok(n) => n, Ok(n) => n,
Err(_) => { Err(_) => {
eprintln!("Block number must be an integer."); eprintln!("Block number must be an integer.");
process::exit(1); process::exit(1);
} }
}; };
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 = format!("{block_path}/{block_number}.{block_ext}"); let block_filename = format!("{block_path}/{block_number}.{block_ext}");
let torrent_filename = format!("{torrent_path}/{block_number}.torrent"); let torrent_filename = format!("{torrent_path}/{block_number}.torrent");
// Load and decode the torrent metadata first because later checks compare against it. // Load and decode the torrent metadata first because later checks compare against it.
let mut torrent_bytes = Vec::new(); let mut torrent_bytes = Vec::new();
let mut torrent_file = File::open(&torrent_filename).await.unwrap_or_else(|_| { let mut torrent_file = File::open(&torrent_filename).await.unwrap_or_else(|_| {
eprintln!("Error: cannot open torrent file '{torrent_filename}'"); eprintln!("Error: cannot open torrent file '{torrent_filename}'");
process::exit(1); process::exit(1);
}); });
torrent_file.read_to_end(&mut torrent_bytes).await.unwrap(); torrent_file.read_to_end(&mut torrent_bytes).await.unwrap();
let torrent = Torrent::from_bytes(&torrent_bytes).await.unwrap(); let torrent = Torrent::from_bytes(&torrent_bytes).await.unwrap();
// Load the local header and resolve the miner public key for signature/VRF checks. // Load the local header and resolve the miner public key for signature/VRF checks.
let header = load_block_header(block_number).await.unwrap(); let header = load_block_header(block_number).await.unwrap();
let block_hash = header.hash().await; let block_hash = header.hash().await;
let block_difficulty = hex_to_u64(&block_hash).await.unwrap(); let block_difficulty = hex_to_u64(&block_hash).await.unwrap();
let miner_pubkey = resolve_pubkey_from_short_address( let miner_pubkey = resolve_pubkey_from_short_address(
&blockchain::startup::initialize_startup::open_chain_state().await, &blockchain::startup::initialize_startup::open_chain_state().await,
&header.unmined_block.miner, &header.unmined_block.miner,
) )
.unwrap_or(None) .unwrap_or(None)
.unwrap_or_default(); .unwrap_or_default();
let miner_pubkey_hex = encode(&miner_pubkey); let miner_pubkey_hex = encode(&miner_pubkey);
// Load the raw block bytes for file-size, info-hash, and piece-hash checks. // Load the raw block bytes for file-size, info-hash, and piece-hash checks.
let mut block_data = Vec::new(); let mut block_data = Vec::new();
let mut block_file = File::open(&block_filename).await.unwrap_or_else(|_| { let mut block_file = File::open(&block_filename).await.unwrap_or_else(|_| {
eprintln!("Error: cannot open block file '{block_filename}'"); eprintln!("Error: cannot open block file '{block_filename}'");
process::exit(1); process::exit(1);
}); });
block_file.read_to_end(&mut block_data).await.unwrap(); block_file.read_to_end(&mut block_data).await.unwrap();
let info_hash_computed = skein_128_hash_bytes(&block_data); let info_hash_computed = skein_128_hash_bytes(&block_data);
// Rebuild the unmined-block hash used by the miner proof signature. // Rebuild the unmined-block hash used by the miner proof signature.
let unmined_json = serde_json::to_string(&header.unmined_block).unwrap(); let unmined_json = serde_json::to_string(&header.unmined_block).unwrap();
let unmined_hash = skein_256_hash_data(&unmined_json); let unmined_hash = skein_256_hash_data(&unmined_json);
let signature_ok = let signature_ok =
Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex) Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex)
.await; .await;
let passed = "[PASSED]".green(); let passed = "[PASSED]".green();
let failed = "[FAILED]".red(); let failed = "[FAILED]".red();
// Compare every header field that is duplicated inside the torrent metadata. // Compare every header field that is duplicated inside the torrent metadata.
if header.unmined_block.timestamp == torrent.info.timestamp { if header.unmined_block.timestamp == torrent.info.timestamp {
println!("timestamp match: {:>90}", format!("{passed}")); println!("timestamp match: {:>90}", format!("{passed}"));
} else { } else {
println!("timestamp match: {:>90}", format!("{failed}")); println!("timestamp match: {:>90}", format!("{failed}"));
} }
if header.unmined_block.nonce == torrent.info.nonce { if header.unmined_block.nonce == torrent.info.nonce {
println!("nonce match: {:>94}", format!("{passed}")); println!("nonce match: {:>94}", format!("{passed}"));
} else { } else {
println!("nonce match: {:>94}", format!("{failed}")); println!("nonce match: {:>94}", format!("{failed}"));
} }
if header.unmined_block.miner == torrent.mined_by { if header.unmined_block.miner == torrent.mined_by {
println!("wallet address match: {:>85}", format!("{passed}")); println!("wallet address match: {:>85}", format!("{passed}"));
} else { } else {
println!("wallet address match: {:>85}", format!("{failed}")); println!("wallet address match: {:>85}", format!("{failed}"));
} }
if block_difficulty < torrent.info.this_block_difficulty { if block_difficulty < torrent.info.this_block_difficulty {
println!("block difficulty check: {:>83}", format!("{passed}")); println!("block difficulty check: {:>83}", format!("{passed}"));
} else { } else {
println!("block difficulty check: {:>83}", format!("{failed}")); println!("block difficulty check: {:>83}", format!("{failed}"));
} }
if header.vrf == torrent.info.vrf { if header.vrf == torrent.info.vrf {
println!("VRF match: {:>96}", format!("{passed}")); println!("VRF match: {:>96}", format!("{passed}"));
} else { } else {
println!("VRF match: {:>96}", format!("{failed}")); println!("VRF match: {:>96}", format!("{failed}"));
} }
if block_data.len() == torrent.info.length as usize { if block_data.len() == torrent.info.length as usize {
println!("file size check: {:>90}", format!("{passed}")); println!("file size check: {:>90}", format!("{passed}"));
} else { } else {
println!("file size check: {:>90}", format!("{failed}")); println!("file size check: {:>90}", format!("{failed}"));
} }
if block_hash == torrent.info.block_hash { if block_hash == torrent.info.block_hash {
println!("block header hash check: {:>82}", format!("{passed}")); println!("block header hash check: {:>82}", format!("{passed}"));
} else { } else {
println!("block header hash check: {:>82}", format!("{failed}")); println!("block header hash check: {:>82}", format!("{failed}"));
} }
let vrf_ok = Wallet::vrf_verify_with_public_key( let vrf_ok = Wallet::vrf_verify_with_public_key(
header.vrf, header.vrf,
&unmined_hash, &unmined_hash,
&miner_pubkey_hex, &miner_pubkey_hex,
&header.proof, &header.proof,
) )
.await; .await;
if vrf_ok { if vrf_ok {
println!("VRF validation check: {:>85}", format!("{passed}")); println!("VRF validation check: {:>85}", format!("{passed}"));
} else { } else {
println!("VRF validation check: {:>85}", format!("{failed}")); println!("VRF validation check: {:>85}", format!("{failed}"));
} }
if signature_ok { if signature_ok {
println!("VRF Proof check: {:>90}", format!("{passed}")); println!("VRF Proof check: {:>90}", format!("{passed}"));
} else { } else {
println!("VRF Proof check: {:>90}", format!("{failed}")); println!("VRF Proof check: {:>90}", format!("{failed}"));
} }
let mut all_pieces_passed = true; let mut all_pieces_passed = true;
// Piece hashes prove the torrent metadata still matches every block-file chunk. // Piece hashes prove the torrent metadata still matches every block-file chunk.
for piece in &torrent.info.pieces { for piece in &torrent.info.pieces {
for (index, expected_hash) in piece.iter() { for (index, expected_hash) in piece.iter() {
let idx = *index as usize; let idx = *index as usize;
let start = (idx - 1) * torrent.info.piece_length as usize; let start = (idx - 1) * torrent.info.piece_length as usize;
let end = std::cmp::min(start + torrent.info.piece_length as usize, block_data.len()); let end = std::cmp::min(start + torrent.info.piece_length as usize, block_data.len());
let slice = &block_data[start..end]; let slice = &block_data[start..end];
let hash = skein_128_hash_bytes(slice); let hash = skein_128_hash_bytes(slice);
if hash == *expected_hash { if hash == *expected_hash {
println!("piece {} hash check: {:>87}", idx, format!("{passed}")); println!("piece {} hash check: {:>87}", idx, format!("{passed}"));
} else { } else {
all_pieces_passed = false; all_pieces_passed = false;
println!("piece {} hash check: {:>87}", idx, format!("{failed}")); println!("piece {} hash check: {:>87}", idx, format!("{failed}"));
} }
} }
} }
if info_hash_computed == torrent.info.info_hash { if info_hash_computed == torrent.info.info_hash {
println!("block hash check: {:>89}", format!("{passed}")); println!("block hash check: {:>89}", format!("{passed}"));
} else { } else {
println!("block hash check: {:>89}", format!("{failed}")); println!("block hash check: {:>89}", format!("{failed}"));
} }
// The final pass/fail summary requires every individual validation to pass. // The final pass/fail summary requires every individual validation to pass.
if header.unmined_block.nonce == torrent.info.nonce if header.unmined_block.nonce == torrent.info.nonce
&& header.unmined_block.miner == torrent.mined_by && header.unmined_block.miner == torrent.mined_by
&& header.unmined_block.timestamp == torrent.info.timestamp && header.unmined_block.timestamp == torrent.info.timestamp
&& block_difficulty < torrent.info.this_block_difficulty && block_difficulty < torrent.info.this_block_difficulty
&& header.vrf == torrent.info.vrf && header.vrf == torrent.info.vrf
&& block_data.len() == torrent.info.length as usize && block_data.len() == torrent.info.length as usize
&& all_pieces_passed && all_pieces_passed
&& block_hash == torrent.info.block_hash && block_hash == torrent.info.block_hash
&& signature_ok && signature_ok
&& vrf_ok && vrf_ok
{ {
println!("\nBlock {block_number} fully validated."); println!("\nBlock {block_number} fully validated.");
} else { } else {
println!("\nBlock {block_number} FAILED validation."); println!("\nBlock {block_number} FAILED validation.");
} }
} }

View File

@ -83,9 +83,7 @@ async fn main() {
let address_path = if args.len() == 2 { let address_path = if args.len() == 2 {
args[1].clone() args[1].clone()
} else { } else {
match prompt_for_path( match prompt_for_path("Please enter the path to the file containing the wallet address: ") {
"Please enter the path to the file containing the wallet address: ",
) {
Ok(path) => path, Ok(path) => path,
Err(err) => { Err(err) => {
eprintln!("{err}"); eprintln!("{err}");

View File

@ -118,9 +118,8 @@ async fn main() {
"Do you agree that payments should be made {}?", "Do you agree that payments should be made {}?",
display_payment_period(&payment_period) display_payment_period(&payment_period)
); );
let question4 = format!( let question4 =
"Do you agree that this loan requires {payment_number} total payments?" format!("Do you agree that this loan requires {payment_number} total payments?");
);
let question5 = format!( let question5 = format!(
"Do you agree that each payment should be {} {}?", "Do you agree that each payment should be {} {}?",
display_amount(payment_amount), display_amount(payment_amount),

View File

@ -1,249 +1,242 @@
use blockchain::blocks::swap::UnsignedSwapTransaction; use blockchain::blocks::swap::UnsignedSwapTransaction;
use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty}; use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty};
use blockchain::common::network_paths_and_settings::block_extension_and_paths; use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::env; use blockchain::env;
use blockchain::fs; use blockchain::fs;
use blockchain::json; use blockchain::json;
use blockchain::read_to_string; use blockchain::read_to_string;
use blockchain::records::wallet_registry::resolve_local_input_short_address; use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::wallets::structures::Wallet; use blockchain::wallets::structures::Wallet;
use blockchain::Value; use blockchain::Value;
// padd the coin to ensure 15 characters // padd the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String { fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write( let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
&mut result, result
format_args!("{input:<width$}"), }
);
result fn normalize_short_address_input(address: &str) -> Result<String, String> {
} resolve_local_input_short_address(address.trim())
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
resolve_local_input_short_address(address.trim()) #[tokio::main]
} async fn main() {
// Get the filename from the command line arguments
#[tokio::main] let args: Vec<String> = env::args().collect();
async fn main() { if args.len() != 2 {
// Get the filename from the command line arguments println!("Usage:./sign_swap <path/to/file.json>");
let args: Vec<String> = env::args().collect(); return;
if args.len() != 2 { }
println!("Usage:./sign_swap <path/to/file.json>"); let filename = &args[1];
return; let decryption_key = prompt_hidden_nonempty(
} "What is your wallet decryption key? ",
let filename = &args[1]; "Wallet key cannot be empty. Please try again.",
let decryption_key = prompt_hidden_nonempty( )
"What is your wallet decryption key? ", .await;
"Wallet key cannot be empty. Please try again.",
) let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
.await; Ok(wallet) => wallet,
Err(err) => {
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { eprintln!("Wallet decryption failed: {err}");
Ok(wallet) => wallet, return;
Err(err) => { }
eprintln!("Wallet decryption failed: {err}"); };
return;
} let private_key = &wallet.saved.private_key;
}; let address = &wallet.saved.short_address;
let private_key = &wallet.saved.private_key; // Read the contents of the file
let address = &wallet.saved.short_address; let contents = match read_to_string(filename).await {
Ok(contents) => contents,
// Read the contents of the file Err(_) => {
let contents = match read_to_string(filename).await { println!("Error reading file: {filename}");
Ok(contents) => contents, return;
Err(_) => { }
println!("Error reading file: {filename}"); };
return;
} // Parse the JSON
}; let json: Result<Value, _> = serde_json::from_str(&contents);
let json = match json {
// Parse the JSON Ok(json) => json,
let json: Result<Value, _> = serde_json::from_str(&contents); Err(_) => {
let json = match json { println!("Error parsing JSON in file: {filename}");
Ok(json) => json, return;
Err(_) => { }
println!("Error parsing JSON in file: {filename}"); };
return;
} let txtype = 6;
};
let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32;
let txtype = 6; let offer_expiration = json["offer_expiration"].as_u64().unwrap_or_default() as u32;
let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32; // get values from transaction json
let offer_expiration = json["offer_expiration"].as_u64().unwrap_or_default() as u32; let token_name1 = json["ticker1"]
.as_str()
// get values from transaction json .unwrap_or_default()
let token_name1 = json["ticker1"] .trim()
.as_str() .to_lowercase();
.unwrap_or_default() let nft_series1: u32 = json["nft_series1"].as_u64().unwrap_or_default() as u32;
.trim() let value1: u64 = json["value1"].as_u64().unwrap_or_default();
.to_lowercase(); let receive_amount = value1 as f64 / 100000000.0;
let nft_series1: u32 = json["nft_series1"].as_u64().unwrap_or_default() as u32;
let value1: u64 = json["value1"].as_u64().unwrap_or_default(); let token_name2 = json["ticker2"]
let receive_amount = value1 as f64 / 100000000.0; .as_str()
.unwrap_or_default()
let token_name2 = json["ticker2"] .trim()
.as_str() .to_lowercase();
.unwrap_or_default() let nft_series2: u32 = json["nft_series2"].as_u64().unwrap_or_default() as u32;
.trim() let value2: u64 = json["value2"].as_u64().unwrap_or_default();
.to_lowercase(); let send_amount = value2 as f64 / 100000000.0;
let nft_series2: u32 = json["nft_series2"].as_u64().unwrap_or_default() as u32;
let value2: u64 = json["value2"].as_u64().unwrap_or_default(); let txfee1_value: u64 = json["txfee1"].as_u64().unwrap_or_default();
let send_amount = value2 as f64 / 100000000.0; let tip1_value: u64 = json["tip1"].as_u64().unwrap_or_default();
let txfee1_value: u64 = json["txfee1"].as_u64().unwrap_or_default(); let sender1 = match normalize_short_address_input(json["sender1"].as_str().unwrap_or_default())
let tip1_value: u64 = json["tip1"].as_u64().unwrap_or_default(); {
Ok(address) => address,
let sender1 = match normalize_short_address_input(json["sender1"].as_str().unwrap_or_default()) Err(_) => {
{ println!("sender1 wallet invalid");
Ok(address) => address, return;
Err(_) => { }
println!("sender1 wallet invalid"); };
return;
} let txfee2_value: u64 = json["txfee2"].as_u64().unwrap_or_default();
}; let txfee2 = txfee2_value as f64 / 100000000.0;
let tip2_value: u64 = json["tip2"].as_u64().unwrap_or_default();
let txfee2_value: u64 = json["txfee2"].as_u64().unwrap_or_default(); let tip2 = tip2_value as f64 / 100000000.0;
let txfee2 = txfee2_value as f64 / 100000000.0;
let tip2_value: u64 = json["tip2"].as_u64().unwrap_or_default(); let sender2 = match normalize_short_address_input(json["sender2"].as_str().unwrap_or_default())
let tip2 = tip2_value as f64 / 100000000.0; {
Ok(address) => address,
let sender2 = match normalize_short_address_input(json["sender2"].as_str().unwrap_or_default()) Err(_) => {
{ println!("sender2 wallet invalid");
Ok(address) => address, return;
Err(_) => { }
println!("sender2 wallet invalid"); };
return;
} // ensure wallet and sender2 match
}; if sender2 != address.trim() {
println!(
// ensure wallet and sender2 match "Transaction is not valid for your wallet address. Expected {sender2} found {address}"
if sender2 != address.trim() { );
println!( return;
"Transaction is not valid for your wallet address. Expected {sender2} found {address}" }
);
return; let (
} _network_name,
network_coin,
let ( _suffix,
_network_name, _torrent_path,
network_coin, _wallet_path,
_suffix, _blockpath,
_torrent_path, _db_path,
_wallet_path, _balance_path,
_blockpath, _log_path,
_db_path, ) = block_extension_and_paths();
_balance_path, // setup validation questions
_log_path, let question1 = format!("Are you expecting to receive {receive_amount} {token_name1}?");
) = block_extension_and_paths(); let question2 = format!("Are you expecting to send {send_amount} {token_name2}?");
// setup validation questions let question3 = format!("Are you willing to spend {txfee2} {network_coin} in fees?");
let question1 = format!( let question4 = format!("Are you willing to tip {tip2} {token_name2}?");
"Are you expecting to receive {receive_amount} {token_name1}?"
); // ask validation questions
let question2 = format!("Are you expecting to send {send_amount} {token_name2}?"); if !ask_yes_no_question(&question1).await {
let question3 = format!( println!("Transaction is not valid");
"Are you willing to spend {txfee2} {network_coin} in fees?" return;
); }
let question4 = format!("Are you willing to tip {tip2} {token_name2}?"); if !ask_yes_no_question(&question2).await {
println!("Transaction is not valid");
// ask validation questions return;
if !ask_yes_no_question(&question1).await { }
println!("Transaction is not valid"); if !ask_yes_no_question(&question3).await {
return; println!("Transaction is not valid");
} return;
if !ask_yes_no_question(&question2).await { }
println!("Transaction is not valid"); if !ask_yes_no_question(&question4).await {
return; println!("Transaction is not valid");
} return;
if !ask_yes_no_question(&question3).await { }
println!("Transaction is not valid");
return; let padded_token_name1 = pad_to_width(&token_name1, 15);
} let padded_token_name2 = pad_to_width(&token_name2, 15);
if !ask_yes_no_question(&question4).await {
println!("Transaction is not valid"); let unsigned_swap = UnsignedSwapTransaction::new(
return; txtype,
} timestamp,
offer_expiration,
let padded_token_name1 = pad_to_width(&token_name1, 15); &padded_token_name1,
let padded_token_name2 = pad_to_width(&token_name2, 15); nft_series1,
value1,
let unsigned_swap = UnsignedSwapTransaction::new( &padded_token_name2,
txtype, nft_series2,
timestamp, value2,
offer_expiration, &sender1,
&padded_token_name1, address.trim(),
nft_series1, tip1_value,
value1, tip2_value,
&padded_token_name2, txfee1_value,
nft_series2, txfee2_value,
value2, )
&sender1, .await;
address.trim(),
tip1_value, let hashed_data = unsigned_swap.hash().await;
tip2_value,
txfee1_value, let original_hash = &json["hash"].as_str().unwrap_or_default().trim();
txfee2_value, let signature1 = &json["signature1"].as_str().unwrap_or_default().trim();
)
.await; let signature2 = if hashed_data == *original_hash.to_string() {
match unsigned_swap.hash_and_sign(&private_key.to_string()).await {
let hashed_data = unsigned_swap.hash().await; Ok(signature) => signature,
Err(err) => {
let original_hash = &json["hash"].as_str().unwrap_or_default().trim(); println!("Signing transaction failed: {err}");
let signature1 = &json["signature1"].as_str().unwrap_or_default().trim(); return;
}
let signature2 = if hashed_data == *original_hash.to_string() { }
match unsigned_swap.hash_and_sign(&private_key.to_string()).await { } else {
Ok(signature) => signature, println!("Signing transaction failed. The included hash was incorrect.");
Err(err) => { return;
println!("Signing transaction failed: {err}"); };
return;
} let output = json!({
} "txtype": txtype,
} else { "timestamp": timestamp,
println!("Signing transaction failed. The included hash was incorrect."); "offer_expiration": offer_expiration,
return; "ticker1": padded_token_name1,
}; "nft_series1": nft_series1,
"value1": value1,
let output = json!({ "ticker2": padded_token_name2,
"txtype": txtype, "nft_series2": nft_series2,
"timestamp": timestamp, "value2": value2,
"offer_expiration": offer_expiration, "sender1": sender1,
"ticker1": padded_token_name1, "sender2": address.trim(),
"nft_series1": nft_series1, "tip1": tip1_value,
"value1": value1, "tip2": tip2_value,
"ticker2": padded_token_name2, "txfee1": txfee1_value,
"nft_series2": nft_series2, "txfee2": txfee2_value,
"value2": value2, "hash": hashed_data,
"sender1": sender1, "signature1": signature1,
"sender2": address.trim(), "signature2": signature2
"tip1": tip1_value, });
"tip2": tip2_value, let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
"txfee1": txfee1_value,
"txfee2": txfee2_value, // Define the directory path
"hash": hashed_data, let dir_path = "./transactions";
"signature1": signature1,
"signature2": signature2 // Create the directory if it doesn't exist
}); if let Err(e) = fs::create_dir_all(dir_path) {
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON"); eprintln!("Failed to create directory: {e}");
return;
// Define the directory path }
let dir_path = "./transactions";
// Define the file path
// Create the directory if it doesn't exist let file_path = format!("{dir_path}/{hashed_data}.json");
if let Err(e) = fs::create_dir_all(dir_path) {
eprintln!("Failed to create directory: {e}"); // Write the JSON string to the file
return; if let Err(e) = fs::write(&file_path, &output_str) {
} eprintln!("Failed to write file: {e}");
return;
// Define the file path }
let file_path = format!("{dir_path}/{hashed_data}.json");
println!("Transaction: {output_str}");
// Write the JSON string to the file }
if let Err(e) = fs::write(&file_path, &output_str) {
eprintln!("Failed to write file: {e}");
return;
}
println!("Transaction: {output_str}");
}

View File

@ -185,10 +185,8 @@ impl VrfBlock {
cursor cursor
.write_all(&self.unmined_block.timestamp.to_le_bytes()) .write_all(&self.unmined_block.timestamp.to_le_bytes())
.await?; .await?;
let miner_bytes = let miner_bytes = Wallet::short_address_to_bytes(&self.unmined_block.miner)
Wallet::short_address_to_bytes(&self.unmined_block.miner).ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid short miner address"))?;
tokio::io::Error::other("Invalid short miner address")
})?;
cursor.write_all(&miner_bytes).await?; cursor.write_all(&miner_bytes).await?;
cursor cursor
.write_all(&decode(&self.unmined_block.previous_hash).unwrap()) .write_all(&decode(&self.unmined_block.previous_hash).unwrap())
@ -207,8 +205,7 @@ impl VrfBlock {
pub async fn from_bytes(bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(bytes: &[u8]) -> tokio::io::Result<Self> {
// A VRF header must be exactly the fixed header byte length. // A VRF header must be exactly the fixed header byte length.
if bytes.len() != VRF_BLOCK_BYTES { if bytes.len() != VRF_BLOCK_BYTES {
return Err(tokio::io::Error::other("Invalid Byte Count for Block", return Err(tokio::io::Error::other("Invalid Byte Count for Block"));
));
} }
// Read from the fixed-width VRF header bytes. // Read from the fixed-width VRF header bytes.
@ -219,9 +216,8 @@ impl VrfBlock {
let mut miner_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut miner_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut miner_bytes).await?; cursor.read_exact(&mut miner_bytes).await?;
let miner = Wallet::bytes_to_short_address(&miner_bytes).ok_or_else(|| { let miner = Wallet::bytes_to_short_address(&miner_bytes)
tokio::io::Error::other("Invalid short miner address") .ok_or_else(|| tokio::io::Error::other("Invalid short miner address"))?;
})?;
// Decode parent hash, difficulty, nonce, VRF number, and proof. // Decode parent hash, difficulty, nonce, VRF number, and proof.
let mut prev_hash_bytes = vec![0; 32]; let mut prev_hash_bytes = vec![0; 32];

View File

@ -105,9 +105,7 @@ impl BurnTransaction {
.write_all(&self.unsigned_burn.time.to_le_bytes()) .write_all(&self.unsigned_burn.time.to_le_bytes())
.await?; .await?;
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_burn.address) let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_burn.address)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid burn short address"))?;
tokio::io::Error::other("Invalid burn short address")
})?;
cursor.write_all(&address_bytes).await?; cursor.write_all(&address_bytes).await?;
cursor.write_all(self.unsigned_burn.coin.as_bytes()).await?; cursor.write_all(self.unsigned_burn.coin.as_bytes()).await?;
cursor cursor
@ -126,8 +124,7 @@ impl BurnTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
let mut cursor = Cursor::new(bytes); let mut cursor = Cursor::new(bytes);
@ -136,10 +133,8 @@ impl BurnTransaction {
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut address_bytes).await?; cursor.read_exact(&mut address_bytes).await?;
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| { let address = Wallet::bytes_to_short_address(&address_bytes)
tokio::io::Error::other("Invalid burn short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid burn short address bytes"))?;
)
})?;
let mut coin_bytes = vec![0; 15]; let mut coin_bytes = vec![0; 15];
cursor.read_exact(&mut coin_bytes).await?; cursor.read_exact(&mut coin_bytes).await?;

View File

@ -110,10 +110,7 @@ impl CollateralClaimTransaction {
.write_all(&decode(&self.unsigned_collateral_claim.contract_hash).unwrap()) .write_all(&decode(&self.unsigned_collateral_claim.contract_hash).unwrap())
.await?; .await?;
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_collateral_claim.address) let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_collateral_claim.address)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid collateral claimant short address"))?;
tokio::io::Error::other("Invalid collateral claimant short address",
)
})?;
cursor.write_all(&address_bytes).await?; cursor.write_all(&address_bytes).await?;
cursor cursor
.write_all(&self.unsigned_collateral_claim.txfee.to_le_bytes()) .write_all(&self.unsigned_collateral_claim.txfee.to_le_bytes())
@ -126,8 +123,7 @@ impl CollateralClaimTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read the remaining fixed-width collateral-claim bytes. // Read the remaining fixed-width collateral-claim bytes.
@ -144,8 +140,7 @@ impl CollateralClaimTransaction {
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut address_bytes).await?; cursor.read_exact(&mut address_bytes).await?;
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| { let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid collateral claimant short address bytes", tokio::io::Error::other("Invalid collateral claimant short address bytes")
)
})?; })?;
let txfee = cursor.read_u64_le().await?; let txfee = cursor.read_u64_le().await?;

View File

@ -65,8 +65,7 @@ impl GenesisTransaction {
// The transaction type is read by the block parser, so this // The transaction type is read by the block parser, so this
// function receives only the remaining genesis bytes. // function receives only the remaining genesis bytes.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read from the remaining fixed-width transaction bytes. // Read from the remaining fixed-width transaction bytes.

View File

@ -104,10 +104,7 @@ impl IssueTokenTransaction {
.write_all(&self.unsigned_issue_token.time.to_le_bytes()) .write_all(&self.unsigned_issue_token.time.to_le_bytes())
.await?; .await?;
let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_issue_token.creator) let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_issue_token.creator)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid issue-token creator short address"))?;
tokio::io::Error::other("Invalid issue-token creator short address",
)
})?;
cursor.write_all(&creator_bytes).await?; cursor.write_all(&creator_bytes).await?;
cursor cursor
.write_all(self.unsigned_issue_token.ticker.as_bytes()) .write_all(self.unsigned_issue_token.ticker.as_bytes())
@ -126,8 +123,7 @@ impl IssueTokenTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
let mut cursor = Cursor::new(bytes); let mut cursor = Cursor::new(bytes);
@ -138,8 +134,7 @@ impl IssueTokenTransaction {
let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut creator_bytes).await?; cursor.read_exact(&mut creator_bytes).await?;
let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| { let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid issue-token creator short address bytes", tokio::io::Error::other("Invalid issue-token creator short address bytes")
)
})?; })?;
// Decode token ticker, issued amount, fee, and signature. // Decode token ticker, issued amount, fee, and signature.

View File

@ -123,9 +123,7 @@ impl ContractPaymentTransaction {
.write_all(&decode(&self.unsigned_contract_payment.contract_hash).unwrap()) .write_all(&decode(&self.unsigned_contract_payment.contract_hash).unwrap())
.await?; .await?;
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_contract_payment.address) let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_contract_payment.address)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid payer short address"))?;
tokio::io::Error::other("Invalid payer short address")
})?;
cursor.write_all(&address_bytes).await?; cursor.write_all(&address_bytes).await?;
cursor cursor
.write_all(&self.unsigned_contract_payment.tip.to_le_bytes()) .write_all(&self.unsigned_contract_payment.tip.to_le_bytes())
@ -142,8 +140,7 @@ impl ContractPaymentTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read the remaining fixed-width loan-payment bytes. // Read the remaining fixed-width loan-payment bytes.
@ -160,10 +157,8 @@ impl ContractPaymentTransaction {
// Decode payer short address, miner tip, fee, txid hash, and signature. // Decode payer short address, miner tip, fee, txid hash, and signature.
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut address_bytes).await?; cursor.read_exact(&mut address_bytes).await?;
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| { let address = Wallet::bytes_to_short_address(&address_bytes)
tokio::io::Error::other("Invalid payer short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid payer short address bytes"))?;
)
})?;
let tip = cursor.read_u64_le().await?; let tip = cursor.read_u64_le().await?;
let txfee = cursor.read_u64_le().await?; let txfee = cursor.read_u64_le().await?;

View File

@ -21,9 +21,9 @@ pub struct UnsignedLoanContractTransaction {
pub payment_period: String, // 1 byte d, w, or m for days, weeks, or months pub payment_period: String, // 1 byte d, w, or m for days, weeks, or months
pub payment_number: u8, // 1 byte total number of payments pub payment_number: u8, // 1 byte total number of payments
pub payment_amount: u64, // 8 bytes amount due for each payment pub payment_amount: u64, // 8 bytes amount due for each payment
pub grace_period: u8, // 1 byte missed payments before collateral claim is allowed pub grace_period: u8, // 1 byte missed payments before collateral claim is allowed
pub max_late_value: u64, // 8 bytes max overdue value before collateral claim is allowed pub max_late_value: u64, // 8 bytes max overdue value before collateral claim is allowed
pub txfee: u64, // 8 bytes transaction fee paid by the lender pub txfee: u64, // 8 bytes transaction fee paid by the lender
} }
#[derive(Debug, Serialize, Clone)] // 1486 bytes #[derive(Debug, Serialize, Clone)] // 1486 bytes
@ -35,9 +35,23 @@ pub struct LoanContractTransaction {
} }
impl LoanContractTransaction { impl LoanContractTransaction {
pub const BYTE_LENGTH: usize = pub const BYTE_LENGTH: usize = 1
1 + 4 + 15 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 15 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 4
+ 1 + 1 + 8 + 1 + 8 + 8 + 32 + Wallet::SIGNATURE_LENGTH + Wallet::SIGNATURE_LENGTH; + 15
+ 8
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 15
+ 8
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 1
+ 1
+ 8
+ 1
+ 8
+ 8
+ 32
+ Wallet::SIGNATURE_LENGTH
+ Wallet::SIGNATURE_LENGTH;
} }
impl UnsignedLoanContractTransaction { impl UnsignedLoanContractTransaction {
@ -163,9 +177,7 @@ impl LoanContractTransaction {
.write_all(&self.unsigned_loan_contract.loan_amount.to_le_bytes()) .write_all(&self.unsigned_loan_contract.loan_amount.to_le_bytes())
.await?; .await?;
let lender_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.lender) let lender_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.lender)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid lender short address"))?;
tokio::io::Error::other("Invalid lender short address")
})?;
cursor.write_all(&lender_bytes).await?; cursor.write_all(&lender_bytes).await?;
cursor cursor
.write_all(self.unsigned_loan_contract.collateral.as_bytes()) .write_all(self.unsigned_loan_contract.collateral.as_bytes())
@ -174,10 +186,7 @@ impl LoanContractTransaction {
.write_all(&self.unsigned_loan_contract.collateral_amount.to_le_bytes()) .write_all(&self.unsigned_loan_contract.collateral_amount.to_le_bytes())
.await?; .await?;
let borrower_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.borrower) let borrower_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.borrower)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid borrower short address"))?;
tokio::io::Error::other("Invalid borrower short address",
)
})?;
cursor.write_all(&borrower_bytes).await?; cursor.write_all(&borrower_bytes).await?;
cursor cursor
.write_all(self.unsigned_loan_contract.payment_period.as_bytes()) .write_all(self.unsigned_loan_contract.payment_period.as_bytes())
@ -207,8 +216,7 @@ impl LoanContractTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read the remaining fixed-width loan-contract bytes. // Read the remaining fixed-width loan-contract bytes.
@ -226,10 +234,8 @@ impl LoanContractTransaction {
// Decode lender short address. // Decode lender short address.
let mut lender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut lender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut lender_bytes).await?; cursor.read_exact(&mut lender_bytes).await?;
let lender = Wallet::bytes_to_short_address(&lender_bytes).ok_or_else(|| { let lender = Wallet::bytes_to_short_address(&lender_bytes)
tokio::io::Error::other("Invalid lender short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid lender short address bytes"))?;
)
})?;
// Decode collateral asset and amount. // Decode collateral asset and amount.
let mut collateral_bytes = vec![0; 15]; let mut collateral_bytes = vec![0; 15];
@ -241,10 +247,8 @@ impl LoanContractTransaction {
// Decode borrower short address. // Decode borrower short address.
let mut borrower_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut borrower_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut borrower_bytes).await?; cursor.read_exact(&mut borrower_bytes).await?;
let borrower = Wallet::bytes_to_short_address(&borrower_bytes).ok_or_else(|| { let borrower = Wallet::bytes_to_short_address(&borrower_bytes)
tokio::io::Error::other("Invalid borrower short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid borrower short address bytes"))?;
)
})?;
// Decode payment schedule and late-payment rules. // Decode payment schedule and late-payment rules.
let mut payment_period_bytes = vec![0; 1]; let mut payment_period_bytes = vec![0; 1];

View File

@ -31,8 +31,19 @@ pub struct MarketingTransaction {
} }
impl MarketingTransaction { impl MarketingTransaction {
pub const BYTE_LENGTH: usize = pub const BYTE_LENGTH: usize = 1
1 + 4 + 8 + 6 + 40 + 100 + 1 + 1 + 2 + 2 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH; + 4
+ 8
+ 6
+ 40
+ 100
+ 1
+ 1
+ 2
+ 2
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 8
+ Wallet::SIGNATURE_LENGTH;
} }
impl UnsignedMarketingTransaction { impl UnsignedMarketingTransaction {
@ -152,10 +163,7 @@ impl MarketingTransaction {
.write_all(&self.unsigned_marketing.click_value.to_le_bytes()) .write_all(&self.unsigned_marketing.click_value.to_le_bytes())
.await?; .await?;
let advertiser_bytes = Wallet::short_address_to_bytes(&self.unsigned_marketing.advertiser) let advertiser_bytes = Wallet::short_address_to_bytes(&self.unsigned_marketing.advertiser)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid advertiser short address"))?;
tokio::io::Error::other("Invalid advertiser short address",
)
})?;
cursor.write_all(&advertiser_bytes).await?; cursor.write_all(&advertiser_bytes).await?;
cursor cursor
.write_all(&self.unsigned_marketing.txfee.to_le_bytes()) .write_all(&self.unsigned_marketing.txfee.to_le_bytes())
@ -168,8 +176,7 @@ impl MarketingTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read the remaining fixed-width marketing bytes. // Read the remaining fixed-width marketing bytes.
@ -201,10 +208,8 @@ impl MarketingTransaction {
// Decode advertiser short address, fee, and signature. // Decode advertiser short address, fee, and signature.
let mut advertiser_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut advertiser_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut advertiser_bytes).await?; cursor.read_exact(&mut advertiser_bytes).await?;
let advertiser = Wallet::bytes_to_short_address(&advertiser_bytes).ok_or_else(|| { let advertiser = Wallet::bytes_to_short_address(&advertiser_bytes)
tokio::io::Error::other("Invalid advertiser short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid advertiser short address bytes"))?;
)
})?;
let txfee = cursor.read_u64_le().await?; let txfee = cursor.read_u64_le().await?;
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH]; let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];

View File

@ -10,15 +10,15 @@ use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 255 bytes #[derive(Debug, Serialize, Clone)] // 255 bytes
pub struct UnsignedCreateNftTransaction { pub struct UnsignedCreateNftTransaction {
pub txtype: u8, // 1 byte transaction type, should be 4 pub txtype: u8, // 1 byte transaction type, should be 4
pub time: u32, // 4 bytes transaction timestamp pub time: u32, // 4 bytes transaction timestamp
pub creator: String, // 22 bytes creator short address pub creator: String, // 22 bytes creator short address
pub series: u8, // 1 byte 0 for single NFT, 1 for series pub series: u8, // 1 byte 0 for single NFT, 1 for series
pub nft_name: String, // 15 bytes NFT or collection name padded with spaces pub nft_name: String, // 15 bytes NFT or collection name padded with spaces
pub item_ipfs: String, // 100 bytes padded CID string pub item_ipfs: String, // 100 bytes padded CID string
pub count: u32, // 4 bytes 1 for single NFT, otherwise series item count pub count: u32, // 4 bytes 1 for single NFT, otherwise series item count
pub desc: String, // 100 bytes description padded with spaces pub desc: String, // 100 bytes description padded with spaces
pub txfee: u64, // 8 bytes transaction fee pub txfee: u64, // 8 bytes transaction fee
} }
#[derive(Debug, Serialize, Clone)] // 921 bytes #[derive(Debug, Serialize, Clone)] // 921 bytes
@ -28,8 +28,16 @@ pub struct CreateNftTransaction {
} }
impl CreateNftTransaction { impl CreateNftTransaction {
pub const BYTE_LENGTH: usize = pub const BYTE_LENGTH: usize = 1
1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 1 + 15 + 100 + 4 + 100 + 8 + Wallet::SIGNATURE_LENGTH; + 4
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 1
+ 15
+ 100
+ 4
+ 100
+ 8
+ Wallet::SIGNATURE_LENGTH;
} }
impl UnsignedCreateNftTransaction { impl UnsignedCreateNftTransaction {
@ -119,9 +127,7 @@ impl CreateNftTransaction {
.write_all(&self.unsigned_create_nft.time.to_le_bytes()) .write_all(&self.unsigned_create_nft.time.to_le_bytes())
.await?; .await?;
let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_nft.creator) let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_nft.creator)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid creator short address"))?;
tokio::io::Error::other("Invalid creator short address")
})?;
cursor.write_all(&creator_bytes).await?; cursor.write_all(&creator_bytes).await?;
cursor cursor
.write_all(&self.unsigned_create_nft.series.to_le_bytes()) .write_all(&self.unsigned_create_nft.series.to_le_bytes())
@ -149,8 +155,7 @@ impl CreateNftTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read the remaining fixed-width NFT-creation bytes. // Read the remaining fixed-width NFT-creation bytes.
@ -161,10 +166,8 @@ impl CreateNftTransaction {
let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut creator_bytes).await?; cursor.read_exact(&mut creator_bytes).await?;
let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| { let creator = Wallet::bytes_to_short_address(&creator_bytes)
tokio::io::Error::other("Invalid creator short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid creator short address bytes"))?;
)
})?;
// Decode series flag, name, CID, count, description, fee, and signature. // Decode series flag, name, CID, count, description, fee, and signature.
let series = cursor.read_u8().await?; let series = cursor.read_u8().await?;

View File

@ -65,8 +65,7 @@ impl RewardsTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read the remaining fixed reward bytes. // Read the remaining fixed reward bytes.

View File

@ -14,17 +14,17 @@ pub struct UnsignedSwapTransaction {
pub timestamp: u32, // 4 bytes offer creation timestamp pub timestamp: u32, // 4 bytes offer creation timestamp
pub offer_expiration: u32, // 4 bytes offer expiration timestamp pub offer_expiration: u32, // 4 bytes offer expiration timestamp
pub ticker1: String, // 15 bytes asset offered by sender1 pub ticker1: String, // 15 bytes asset offered by sender1
pub nft_series1: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item pub nft_series1: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item
pub value1: u64, // 8 bytes amount offered by sender1 pub value1: u64, // 8 bytes amount offered by sender1
pub ticker2: String, // 15 bytes asset offered by sender2 pub ticker2: String, // 15 bytes asset offered by sender2
pub nft_series2: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item pub nft_series2: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item
pub value2: u64, // 8 bytes amount offered by sender2 pub value2: u64, // 8 bytes amount offered by sender2
pub sender1: String, // 22 bytes sender1 short address pub sender1: String, // 22 bytes sender1 short address
pub sender2: String, // 22 bytes sender2 short address pub sender2: String, // 22 bytes sender2 short address
pub tip1: u64, // 8 bytes miner tip paid in ticker1 pub tip1: u64, // 8 bytes miner tip paid in ticker1
pub tip2: u64, // 8 bytes miner tip paid in ticker2 pub tip2: u64, // 8 bytes miner tip paid in ticker2
pub txfee1: u64, // 8 bytes sender1 fee pub txfee1: u64, // 8 bytes sender1 fee
pub txfee2: u64, // 8 bytes sender2 fee pub txfee2: u64, // 8 bytes sender2 fee
} }
#[derive(Debug, Serialize, Clone)] // 1471 bytes #[derive(Debug, Serialize, Clone)] // 1471 bytes
@ -35,9 +35,23 @@ pub struct SwapTransaction {
} }
impl SwapTransaction { impl SwapTransaction {
pub const BYTE_LENGTH: usize = pub const BYTE_LENGTH: usize = 1
1 + 4 + 4 + 15 + 4 + 8 + 15 + 4 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 4
+ 8 + 8 + 8 + 8 + Wallet::SIGNATURE_LENGTH + Wallet::SIGNATURE_LENGTH; + 4
+ 15
+ 4
+ 8
+ 15
+ 4
+ 8
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 8
+ 8
+ 8
+ 8
+ Wallet::SIGNATURE_LENGTH
+ Wallet::SIGNATURE_LENGTH;
} }
impl UnsignedSwapTransaction { impl UnsignedSwapTransaction {
@ -178,13 +192,9 @@ impl SwapTransaction {
.write_all(&self.unsigned_swap.value2.to_le_bytes()) .write_all(&self.unsigned_swap.value2.to_le_bytes())
.await?; .await?;
let sender1_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender1) let sender1_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender1)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid sender1 short address"))?;
tokio::io::Error::other("Invalid sender1 short address")
})?;
let sender2_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender2) let sender2_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender2)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid sender2 short address"))?;
tokio::io::Error::other("Invalid sender2 short address")
})?;
cursor.write_all(&sender1_bytes).await?; cursor.write_all(&sender1_bytes).await?;
cursor.write_all(&sender2_bytes).await?; cursor.write_all(&sender2_bytes).await?;
cursor cursor
@ -208,8 +218,7 @@ impl SwapTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read the remaining fixed-width swap bytes. // Read the remaining fixed-width swap bytes.
@ -238,17 +247,13 @@ impl SwapTransaction {
// Decode both sender short addresses. // Decode both sender short addresses.
let mut sender1_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut sender1_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut sender1_bytes).await?; cursor.read_exact(&mut sender1_bytes).await?;
let sender1 = Wallet::bytes_to_short_address(&sender1_bytes).ok_or_else(|| { let sender1 = Wallet::bytes_to_short_address(&sender1_bytes)
tokio::io::Error::other("Invalid sender1 short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid sender1 short address bytes"))?;
)
})?;
let mut sender2_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut sender2_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut sender2_bytes).await?; cursor.read_exact(&mut sender2_bytes).await?;
let sender2 = Wallet::bytes_to_short_address(&sender2_bytes).ok_or_else(|| { let sender2 = Wallet::bytes_to_short_address(&sender2_bytes)
tokio::io::Error::other("Invalid sender2 short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid sender2 short address bytes"))?;
)
})?;
// Decode miner tips, fees, and both signatures. // Decode miner tips, fees, and both signatures.
let tip1 = cursor.read_u64_le().await?; let tip1 = cursor.read_u64_le().await?;

View File

@ -115,9 +115,7 @@ impl CreateTokenTransaction {
.write_all(&self.unsigned_create_token.time.to_le_bytes()) .write_all(&self.unsigned_create_token.time.to_le_bytes())
.await?; .await?;
let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_token.creator) let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_token.creator)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid creator short address"))?;
tokio::io::Error::other("Invalid creator short address")
})?;
cursor.write_all(&creator_bytes).await?; cursor.write_all(&creator_bytes).await?;
cursor cursor
.write_all(self.unsigned_create_token.ticker.as_bytes()) .write_all(self.unsigned_create_token.ticker.as_bytes())
@ -139,8 +137,7 @@ impl CreateTokenTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read the remaining fixed-width token-creation bytes. // Read the remaining fixed-width token-creation bytes.
@ -152,10 +149,8 @@ impl CreateTokenTransaction {
// Decode the creator short address. // Decode the creator short address.
let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut creator_bytes).await?; cursor.read_exact(&mut creator_bytes).await?;
let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| { let creator = Wallet::bytes_to_short_address(&creator_bytes)
tokio::io::Error::other("Invalid creator short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid creator short address bytes"))?;
)
})?;
// Decode the fixed 15-byte token ticker. // Decode the fixed 15-byte token ticker.
let mut ticker_bytes = vec![0; 15]; let mut ticker_bytes = vec![0; 15];

View File

@ -14,11 +14,11 @@ pub struct UnsignedTransferTransaction {
pub txtype: u8, // 1 byte transaction type, should be 2 pub txtype: u8, // 1 byte transaction type, should be 2
pub time: u32, // 4 bytes transaction timestamp pub time: u32, // 4 bytes transaction timestamp
pub value: u64, // 8 bytes number of coins or tokens to send pub value: u64, // 8 bytes number of coins or tokens to send
pub coin: String, // 15 bytes base coin, token ticker, or NFT name, padded with spaces pub coin: String, // 15 bytes base coin, token ticker, or NFT name, padded with spaces
pub nft_series: u32, // 4 bytes 0 for coins/tokens/1-of-1 NFTs, otherwise NFT series item pub nft_series: u32, // 4 bytes 0 for coins/tokens/1-of-1 NFTs, otherwise NFT series item
pub sender: String, // 22 bytes sender short address pub sender: String, // 22 bytes sender short address
pub receiver: String, // 22 bytes receiver short address pub receiver: String, // 22 bytes receiver short address
pub txfee: u64, // 8 bytes transaction fee pub txfee: u64, // 8 bytes transaction fee
} }
#[derive(Debug, Serialize, Clone)] // 750 bytes #[derive(Debug, Serialize, Clone)] // 750 bytes
@ -28,8 +28,15 @@ pub struct TransferTransaction {
} }
impl TransferTransaction { impl TransferTransaction {
pub const BYTE_LENGTH: usize = pub const BYTE_LENGTH: usize = 1
1 + 4 + 8 + 15 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH; + 4
+ 8
+ 15
+ 4
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 8
+ Wallet::SIGNATURE_LENGTH;
} }
impl UnsignedTransferTransaction { impl UnsignedTransferTransaction {
@ -126,14 +133,9 @@ impl TransferTransaction {
.write_all(&self.unsigned_transfer.nft_series.to_le_bytes()) .write_all(&self.unsigned_transfer.nft_series.to_le_bytes())
.await?; .await?;
let sender_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.sender) let sender_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.sender)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid sender short address"))?;
tokio::io::Error::other("Invalid sender short address")
})?;
let receiver_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.receiver) let receiver_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.receiver)
.ok_or_else(|| { .ok_or_else(|| tokio::io::Error::other("Invalid receiver short address"))?;
tokio::io::Error::other("Invalid receiver short address",
)
})?;
cursor.write_all(&sender_bytes).await?; cursor.write_all(&sender_bytes).await?;
cursor.write_all(&receiver_bytes).await?; cursor.write_all(&receiver_bytes).await?;
cursor cursor
@ -147,8 +149,7 @@ impl TransferTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte. // The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 { if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count", return Err(tokio::io::Error::other("Invalid Byte Count"));
));
} }
// Read the remaining fixed-width transfer bytes. // Read the remaining fixed-width transfer bytes.
@ -168,18 +169,14 @@ impl TransferTransaction {
// Decode the sender short address. // Decode the sender short address.
let mut sender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut sender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut sender_bytes).await?; cursor.read_exact(&mut sender_bytes).await?;
let sender = Wallet::bytes_to_short_address(&sender_bytes).ok_or_else(|| { let sender = Wallet::bytes_to_short_address(&sender_bytes)
tokio::io::Error::other("Invalid sender short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid sender short address bytes"))?;
)
})?;
// Decode the receiver short address. // Decode the receiver short address.
let mut receiver_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut receiver_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut receiver_bytes).await?; cursor.read_exact(&mut receiver_bytes).await?;
let receiver = Wallet::bytes_to_short_address(&receiver_bytes).ok_or_else(|| { let receiver = Wallet::bytes_to_short_address(&receiver_bytes)
tokio::io::Error::other("Invalid receiver short address bytes", .ok_or_else(|| tokio::io::Error::other("Invalid receiver short address bytes"))?;
)
})?;
// Decode fee and signature. // Decode fee and signature.
let txfee = cursor.read_u64_le().await?; let txfee = cursor.read_u64_le().await?;

View File

@ -9,11 +9,11 @@ use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 57 bytes #[derive(Debug, Serialize, Clone)] // 57 bytes
pub struct UnsignedVanityAddressTransaction { pub struct UnsignedVanityAddressTransaction {
pub txtype: u8, // 1 byte transaction type, should be 12 pub txtype: u8, // 1 byte transaction type, should be 12
pub timestamp: u32, // 4 bytes transaction timestamp pub timestamp: u32, // 4 bytes transaction timestamp
pub address: String, // 22 bytes real short address receiving the vanity mapping pub address: String, // 22 bytes real short address receiving the vanity mapping
pub vanity_address: String, // 22 bytes vanity short address being registered pub vanity_address: String, // 22 bytes vanity short address being registered
pub txfee: u64, // 8 bytes fee paid for vanity registration pub txfee: u64, // 8 bytes fee paid for vanity registration
} }
#[derive(Debug, Serialize, Clone)] // 723 bytes #[derive(Debug, Serialize, Clone)] // 723 bytes
@ -23,8 +23,12 @@ pub struct VanityAddressTransaction {
} }
impl VanityAddressTransaction { impl VanityAddressTransaction {
pub const BYTE_LENGTH: usize = pub const BYTE_LENGTH: usize = 1
1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH; + 4
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 8
+ Wallet::SIGNATURE_LENGTH;
} }
impl UnsignedVanityAddressTransaction { impl UnsignedVanityAddressTransaction {
@ -99,10 +103,9 @@ impl VanityAddressTransaction {
.write_all(&self.unsigned_vanity_address.timestamp.to_le_bytes()) .write_all(&self.unsigned_vanity_address.timestamp.to_le_bytes())
.await?; .await?;
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_vanity_address.address) let address_bytes =
.ok_or_else(|| { Wallet::short_address_to_bytes(&self.unsigned_vanity_address.address)
tokio::io::Error::other("Invalid sender short address") .ok_or_else(|| tokio::io::Error::other("Invalid sender short address"))?;
})?;
// Vanity addresses use the same 22-byte width as short addresses // Vanity addresses use the same 22-byte width as short addresses
// but are encoded through the vanity-specific byte conversion. // but are encoded through the vanity-specific byte conversion.
let vanity_bytes = let vanity_bytes =
@ -135,15 +138,13 @@ impl VanityAddressTransaction {
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut address_bytes).await?; cursor.read_exact(&mut address_bytes).await?;
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| { let address = Wallet::bytes_to_short_address(&address_bytes)
tokio::io::Error::other("Invalid sender short address bytes") .ok_or_else(|| tokio::io::Error::other("Invalid sender short address bytes"))?;
})?;
let mut vanity_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; let mut vanity_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut vanity_bytes).await?; cursor.read_exact(&mut vanity_bytes).await?;
let vanity_address = Wallet::bytes_to_vanity_address(&vanity_bytes).ok_or_else(|| { let vanity_address = Wallet::bytes_to_vanity_address(&vanity_bytes)
tokio::io::Error::other("Invalid vanity short address bytes") .ok_or_else(|| tokio::io::Error::other("Invalid vanity short address bytes"))?;
})?;
let txfee = cursor.read_u64_le().await?; let txfee = cursor.read_u64_le().await?;

View File

@ -28,9 +28,7 @@ pub async fn hex_to_u64(hex_string: &str) -> Result<u64, String> {
pub fn binary_to_string(binary_data: Vec<u8>) -> String { pub fn binary_to_string(binary_data: Vec<u8>) -> String {
match String::from_utf8(binary_data.clone()) { match String::from_utf8(binary_data.clone()) {
Ok(s) => s, Ok(s) => s,
Err(_) => { Err(_) => "Invalid UTF-8 Data".to_string(),
"Invalid UTF-8 Data".to_string()
}
} }
} }

View File

@ -1,10 +1,10 @@
use crate::encode; use crate::encode;
use crate::ripemd::Digest as RipemdDigest;
use crate::Digest; use crate::Digest;
use crate::Output; use crate::Output;
use crate::Ripemd160; use crate::Ripemd160;
use crate::Skein256; use crate::Skein256;
use crate::Skein512; use crate::Skein512;
use crate::ripemd::Digest as RipemdDigest;
pub fn skein_128_hash_bytes(data: &[u8]) -> String { pub fn skein_128_hash_bytes(data: &[u8]) -> String {
// Contractless 128-bit hashes are Skein256 hashes reduced to 16 bytes. // Contractless 128-bit hashes are Skein256 hashes reduced to 16 bytes.

View File

@ -159,8 +159,7 @@ impl Settings {
config_dir, config_dir,
), ),
log_path: Self::expand( log_path: Self::expand(
conf.get_from(Some("Paths"), "LOG_PATH") conf.get_from(Some("Paths"), "LOG_PATH").unwrap_or("./logs"),
.unwrap_or("./logs"),
config_dir, config_dir,
), ),
log_level: conf log_level: conf

View File

@ -2,20 +2,22 @@ use crate::blocks::block::{Block, UnminedBlock};
use crate::blocks::genesis::{GenesisTransaction, UnsignedGenesisTransaction}; use crate::blocks::genesis::{GenesisTransaction, UnsignedGenesisTransaction};
use crate::common::check_genesis::genesis_checkup; use crate::common::check_genesis::genesis_checkup;
use crate::common::types::{Transaction, GENESIS_BLOCK_HASH}; use crate::common::types::{Transaction, GENESIS_BLOCK_HASH};
use crate::miner::flag::{is_mining_running, is_mining_stop_requested, is_normal_mode, set_mining_state, MiningState}; use crate::log::{error, info};
use crate::miner::flag::{
is_mining_running, is_mining_stop_requested, is_normal_mode, set_mining_state, MiningState,
};
use crate::records::memory::connections::outgoing_connection_count; use crate::records::memory::connections::outgoing_connection_count;
use crate::records::memory::response_channels::Command; use crate::records::memory::response_channels::Command;
use crate::records::record_chain::save::save_block; use crate::records::record_chain::save::save_block;
use crate::records::record_chain::structs::{SaveBlockParams, SaveType}; use crate::records::record_chain::structs::{SaveBlockParams, SaveType};
use crate::sled::Db;
use crate::sleep;
use crate::verifications::verification_service::VerificationService; use crate::verifications::verification_service::VerificationService;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::log::{error, info};
use crate::sled::Db;
use crate::Arc; use crate::Arc;
use crate::Duration; use crate::Duration;
use crate::Error; use crate::Error;
use crate::Mutex; use crate::Mutex;
use crate::sleep;
use crate::Utc; use crate::Utc;
pub async fn create_genesis_transaction( pub async fn create_genesis_transaction(

View File

@ -1,15 +1,15 @@
use crate::common::check_genesis::genesis_checkup; use crate::common::check_genesis::genesis_checkup;
use crate::records::memory::response_channels::{reserve_entry, Command}; use crate::records::memory::response_channels::{reserve_entry, Command};
use crate::sled::Db;
use crate::timeout;
use crate::torrent::structs::Torrent;
use crate::torrent::torrenting_system::torrent_requests::{ use crate::torrent::torrenting_system::torrent_requests::{
handle_response_and_save_torrent, send_request_torrent_message, handle_response_and_save_torrent, send_request_torrent_message,
}; };
use crate::sled::Db;
use crate::torrent::structs::Torrent;
use crate::Arc; use crate::Arc;
use crate::Duration; use crate::Duration;
use crate::Mutex; use crate::Mutex;
use crate::TcpStream; use crate::TcpStream;
use crate::timeout;
pub async fn create_genesis_block( pub async fn create_genesis_block(
local_height: u32, local_height: u32,

View File

@ -1,15 +1,24 @@
use crate::log::info; use crate::common::skein::skein_128_hash_bytes;
use crate::log::{info, warn};
use crate::miner::flag::begin_reorg_lock; use crate::miner::flag::begin_reorg_lock;
use crate::orphans::replay_errors::should_retry_staged_candidate;
use crate::orphans::structs::{OrphanCheckup, UndoTransactions}; use crate::orphans::structs::{OrphanCheckup, UndoTransactions};
use crate::orphans::undo_block_transactions::undo_transactions; use crate::orphans::undo_block_transactions::undo_transactions;
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,
}; };
use crate::torrent::structs::Torrent; use crate::records::unpack_block::load_by_binary_data::load_block_from_binary;
use crate::records::unpack_block::unpack_header::load_block_header;
use crate::torrent::structs::{DownloadSave, Torrent};
use crate::torrent::torrenting_system::create_file::combine_pieces;
use crate::torrent::torrenting_system::download_pieces::download_block_pieces;
use crate::torrent::torrenting_system::save_torrent::{ use crate::torrent::torrenting_system::save_torrent::{
list_staged_torrents_for_height, read_staged_torrent, list_staged_torrents_for_height, read_staged_torrent,
}; };
use crate::torrent::torrenting_system::temp_database_storage::remove_block_pieces_from_db;
use crate::torrent::torrenting_system::torrent_map::create_torrent_map;
use crate::torrent::unpack_local_torrent::load_torrent; use crate::torrent::unpack_local_torrent::load_torrent;
use crate::verifications::verification_service::global_verification_service;
async fn staged_candidates_for_height(height: u32) -> Vec<Torrent> { async fn staged_candidates_for_height(height: u32) -> Vec<Torrent> {
let mut candidates = Vec::new(); let mut candidates = Vec::new();
@ -42,12 +51,12 @@ fn torrent_beats(left: &Torrent, right: &Torrent) -> bool {
.is_lt() .is_lt()
} }
async fn best_competing_candidate( async fn ordered_competing_candidates(
height: u32, height: u32,
local_torrent: &Torrent, local_torrent: &Torrent,
candidates: &[Torrent], candidates: &[Torrent],
) -> Option<Torrent> { ) -> Vec<Torrent> {
let mut preferred: Option<Torrent> = None; let mut ordered = Vec::new();
for torrent in candidates { for torrent in candidates {
// Identical info hashes are the same block candidate, not a competing fork. // Identical info hashes are the same block candidate, not a competing fork.
@ -64,19 +73,83 @@ async fn best_competing_candidate(
continue; continue;
} }
match &preferred { ordered.push(torrent.clone());
Some(current_torrent) => { }
// Among candidates that have not been ruled out, choose the
// same deterministic winner the block fight uses. // Among candidates that have not been ruled out, choose the same
if torrent_beats(torrent, current_torrent) { // deterministic order the block fight uses.
preferred = Some(torrent.clone()); ordered.sort_by(|a, b| {
} a.info
} .timestamp
None => preferred = Some(torrent.clone()), .cmp(&b.info.timestamp)
.then(a.info.nonce.cmp(&b.info.nonce))
.then(a.info.vrf.cmp(&b.info.vrf))
});
ordered
}
async fn cleanup_candidate_pieces(db: &crate::sled::Db, height: u32, torrent: &Torrent) {
let _ = remove_block_pieces_from_db(db, height, &torrent.info.info_hash).await;
}
async fn candidate_attaches_before_rollback(
params: &OrphanCheckup,
height: u32,
torrent: &Torrent,
wallet_key: &str,
) -> Result<(), String> {
// Metadata may choose a candidate, but only downloaded block bytes can
// prove the rollback is safe.
torrent.verify(height, &params.db, wallet_key).await?;
let verification_service = global_verification_service()
.ok_or_else(|| "Verification service not initialized".to_string())?;
let torrent_map = create_torrent_map(torrent).await?;
let download_save_params = DownloadSave {
torrent_map,
torrent: torrent.clone(),
staged_path: String::new(),
block_number: height,
allow_during_reorg: true,
allow_historical: true,
db: params.db.clone(),
verification_service: std::sync::Arc::new(verification_service),
map: params.map.clone(),
};
download_block_pieces(download_save_params).await?;
let result = combine_pieces(&params.db, height, &torrent.info.info_hash).await?;
if result.len() != torrent.info.length as usize {
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Downloaded candidate length does not match torrent metadata.".to_string());
}
if skein_128_hash_bytes(&result) != torrent.info.info_hash {
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Hash validation failed for complete block".to_string());
}
let loaded_block = load_block_from_binary(&result)
.await
.map_err(|err| format!("Failed to load block from binary: {err}"))?;
let header_hash = loaded_block.vrf_block.hash().await;
if header_hash != torrent.info.block_hash {
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Candidate header hash does not match torrent metadata.".to_string());
}
if height > 0 {
let parent_height = height - 1;
let parent_header = load_block_header(parent_height).await?;
let parent_hash = parent_header.hash().await;
if loaded_block.vrf_block.unmined_block.previous_hash != parent_hash {
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Incorrect previous_block_hash.".to_string());
} }
} }
preferred Ok(())
} }
pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), String> { pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), String> {
@ -89,41 +162,69 @@ pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), Stri
let local_torrent = load_torrent(&params.db, height).await?; let local_torrent = load_torrent(&params.db, height).await?;
let staged_candidates = staged_candidates_for_height(height).await; let staged_candidates = staged_candidates_for_height(height).await;
if let Some(competing_torrent) = let ordered_candidates =
best_competing_candidate(height, &local_torrent, &staged_candidates).await ordered_competing_candidates(height, &local_torrent, &staged_candidates).await;
{
let competing_info_hash = competing_torrent.info.info_hash.clone();
// If the best staged torrent wins this height, rollback starts
// here and replay rebuilds forward from staged candidates.
let undo_transactions_params = UndoTransactions {
start_height: height,
db: params.db.clone(),
stream: params.stream.clone(),
map: params.map.clone(),
node_syncing: params.node_syncing,
connections_key: params.connections_key.clone(),
};
if torrent_beats(&competing_torrent, &local_torrent) { for competing_torrent in ordered_candidates {
set_torrent_status(height, &competing_info_hash, TorrentStatus::Valid).await; let competing_info_hash = competing_torrent.info.info_hash.clone();
if !params.node_syncing {
begin_reorg_lock().await; if !torrent_beats(&competing_torrent, &local_torrent) {
// The local block remains the winner at this height. Since
// candidates are sorted best-first, every remaining staged
// competitor has also lost to the local block.
for staged_torrent in &staged_candidates {
if staged_torrent.info.info_hash != local_torrent.info.info_hash {
set_torrent_status(
height,
&staged_torrent.info.info_hash,
TorrentStatus::Invalid,
)
.await;
}
} }
info!("[orphan] adopting competing staged chain from height {height}"); break;
undo_transactions(undo_transactions_params, wallet_key).await?;
return Ok(());
} }
// The local block remains the winner at this height, so every match candidate_attaches_before_rollback(
// staged competitor for the same height has now been checked. &params,
for staged_torrent in staged_candidates { height,
if staged_torrent.info.info_hash != local_torrent.info.info_hash { &competing_torrent,
set_torrent_status( wallet_key,
height, )
&staged_torrent.info.info_hash, .await
TorrentStatus::Invalid, {
) Ok(()) => {
.await; let undo_transactions_params = UndoTransactions {
start_height: height,
db: params.db.clone(),
stream: params.stream.clone(),
map: params.map.clone(),
node_syncing: params.node_syncing,
connections_key: params.connections_key.clone(),
};
set_torrent_status(height, &competing_info_hash, TorrentStatus::Valid).await;
if !params.node_syncing {
begin_reorg_lock().await;
}
info!("[orphan] adopting proven staged chain from height {height}");
undo_transactions(undo_transactions_params, wallet_key).await?;
return Ok(());
}
Err(err) => {
let status = if should_retry_staged_candidate(&err) {
TorrentStatus::Pending
} else {
TorrentStatus::Invalid
};
set_torrent_status(height, &competing_info_hash, status).await;
warn!(
"[orphan] staged candidate failed pre-rollback proof: height={height} err={err}"
);
if status == TorrentStatus::Pending {
break;
}
} }
} }
} }

View File

@ -5,8 +5,10 @@ pub fn should_retry_staged_candidate(error: &str) -> bool {
|| error.contains("piece not found") || error.contains("piece not found")
|| error.contains("Requested candidate not found") || error.contains("Requested candidate not found")
|| error.contains("Block not found") || error.contains("Block not found")
|| (error.contains("Block ") && error.contains(" not found"))
|| error.contains("Timed out waiting for piece") || error.contains("Timed out waiting for piece")
|| error.contains("Timed out waiting for replacement torrent") || error.contains("Timed out waiting for replacement torrent")
|| error.contains("No replacement torrent received") || error.contains("No replacement torrent received")
|| error.contains("Piece reply channel closed") || error.contains("Piece reply channel closed")
|| error.contains("Replay waiting for block pieces")
} }

View File

@ -1,5 +1,6 @@
use crate::orphans::structs::UndoTransactions;
use crate::orphans::replay_errors::should_retry_staged_candidate; use crate::orphans::replay_errors::should_retry_staged_candidate;
use crate::orphans::structs::UndoTransactions;
use crate::orphans::torrent_candidates::hydrate_torrent_candidates;
use crate::records::block_height::get_block_height::get_height; use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::response_channels::reserve_entry; use crate::records::memory::response_channels::reserve_entry;
use crate::records::memory::torrent_status::{ use crate::records::memory::torrent_status::{
@ -13,6 +14,7 @@ use crate::torrent::torrenting_system::torrent_requests::{
handle_response_and_save_torrent, send_request_torrent_message, handle_response_and_save_torrent, send_request_torrent_message,
}; };
use crate::{timeout, Duration}; use crate::{timeout, Duration};
use std::collections::HashSet;
pub async fn save_new_blocks( pub async fn save_new_blocks(
params: &UndoTransactions, params: &UndoTransactions,
@ -22,6 +24,7 @@ pub async fn save_new_blocks(
) -> Result<(), String> { ) -> Result<(), String> {
// after rollback, request and save each remote block from the // after rollback, request and save each remote block from the
// divergence point up to the height we need to restore // divergence point up to the height we need to restore
let mut hydrated_heights = HashSet::new();
loop { loop {
let mut resolved_from_staging = false; let mut resolved_from_staging = false;
let staged_candidates = list_staged_torrents_for_height(true_start_height).await?; let staged_candidates = list_staged_torrents_for_height(true_start_height).await?;
@ -101,12 +104,7 @@ pub async fn save_new_blocks(
} else { } else {
TorrentStatus::Invalid TorrentStatus::Invalid
}; };
set_torrent_status( set_torrent_status(true_start_height, &torrent_info_hash, status).await;
true_start_height,
&torrent_info_hash,
status,
)
.await;
} }
} }
} }
@ -124,6 +122,22 @@ pub async fn save_new_blocks(
} }
} }
if hydrated_heights.insert(true_start_height) {
let imported = hydrate_torrent_candidates(
params.stream.clone(),
params.map.clone(),
params.connections_key.clone(),
)
.await?;
if imported > 0 {
// Peer candidate hydration can add staged torrents that
// are not canonical on the peer yet. Restart this height
// so those candidates are tried before canonical fallback.
continue;
}
}
// 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(params.map.clone()).await;

View File

@ -47,11 +47,8 @@ pub async fn update_snapshot(db: &Db, current_height: u32) -> Result<(), String>
let hash = header.hash().await; let hash = header.hash().await;
let value = format!("{snapshot_height}:{hash}"); let value = format!("{snapshot_height}:{hash}");
let key = b"snapshot"; let key = b"snapshot";
db.insert(key, value.as_bytes()).map_err(|e| { db.insert(key, value.as_bytes())
format!( .map_err(|e| format!("Failed to store snapshot at height {snapshot_height}: {e}"))?;
"Failed to store snapshot at height {snapshot_height}: {e}"
)
})?;
Ok(()) Ok(())
} }
@ -97,7 +94,9 @@ pub async fn snapshot_verified(params: UndoTransactions, wallet_key: &str) -> bo
} }
} }
_ => { _ => {
error!("Unable to verify remote snapshot torrent at height {snap_height}"); error!(
"Unable to verify remote snapshot torrent at height {snap_height}"
);
return true; return true;
} }
} }

View File

@ -104,6 +104,7 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re
} }
let mut advanced_height = false; let mut advanced_height = false;
let mut retryable_pending = false;
for torrent in ordered_candidates { for torrent in ordered_candidates {
let torrent_info_hash = torrent.info.info_hash.clone(); let torrent_info_hash = torrent.info.info_hash.clone();
@ -137,6 +138,7 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re
} }
Err(err) => { Err(err) => {
if should_retry_staged_candidate(&err) { if should_retry_staged_candidate(&err) {
retryable_pending = true;
// Piece availability is not proof that the candidate // Piece availability is not proof that the candidate
// lost the block fight; leave it pending so a later // lost the block fight; leave it pending so a later
// orphan pass can retry after more peers stage it. // orphan pass can retry after more peers stage it.
@ -163,6 +165,11 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re
if !advanced_height { if !advanced_height {
// Every staged candidate for the current expected height was // Every staged candidate for the current expected height was
// exhausted without extending the chain, so stop replay here. // exhausted without extending the chain, so stop replay here.
if retryable_pending {
return Err(format!(
"Replay waiting for block pieces at height {expected_height}"
));
}
return Ok(()); return Ok(());
} }
} }
@ -196,9 +203,16 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<()
if !snapshot_verified(undo_transactions_params, wallet_key).await { if !snapshot_verified(undo_transactions_params, wallet_key).await {
// A snapshot rollback already happened, so replay staged torrents and // A snapshot rollback already happened, so replay staged torrents and
// exit instead of running the near-tip rules against stale heights. // exit instead of running the near-tip rules against stale heights.
let mut replay_waiting = false;
match replay_staged_torrents(&params, wallet_key).await { match replay_staged_torrents(&params, wallet_key).await {
Ok(()) => {} Ok(()) => {}
Err(err) => error!("[orphan] staged torrent replay error: {err}"), Err(err) => {
replay_waiting = should_retry_staged_candidate(&err);
error!("[orphan] staged torrent replay error: {err}");
}
}
if replay_waiting && !params.node_syncing {
return Err("orphan replay is waiting for block data".to_string());
} }
if !params.node_syncing { if !params.node_syncing {
end_reorg_lock(); end_reorg_lock();
@ -218,14 +232,33 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<()
}; };
deep_sync_rollback(checkup_params.clone(), wallet_key).await; deep_sync_rollback(checkup_params.clone(), wallet_key).await;
let mut replay_waiting = false;
let height_before_window_check = get_height(&params.db);
match orphan_window_check(checkup_params, wallet_key).await { match orphan_window_check(checkup_params, wallet_key).await {
Ok(()) => {} Ok(()) => {}
Err(err) => error!("[orphan] orphan window check error: {err}"), Err(err) => {
if should_retry_staged_candidate(&err)
&& get_height(&params.db) < height_before_window_check
{
replay_waiting = true;
}
error!("[orphan] orphan window check error: {err}");
}
} }
let height_before_replay = get_height(&params.db);
match replay_staged_torrents(&params, wallet_key).await { match replay_staged_torrents(&params, wallet_key).await {
Ok(()) => {} Ok(()) => {}
Err(err) => error!("[orphan] staged torrent replay error: {err}"), Err(err) => {
replay_waiting |= should_retry_staged_candidate(&err);
error!("[orphan] staged torrent replay error: {err}");
}
}
if get_height(&params.db) > height_before_replay {
replay_waiting = false;
}
if replay_waiting && !params.node_syncing {
return Err("orphan replay is waiting for block data".to_string());
} }
if !params.node_syncing { if !params.node_syncing {
end_reorg_lock(); end_reorg_lock();

View File

@ -39,10 +39,17 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
for transaction in transactions.into_iter().rev() { for transaction in transactions.into_iter().rev() {
match transaction { match transaction {
Transaction::Rewards(rewards_tx) => { Transaction::Rewards(rewards_tx) => {
undo_rewards_transaction(rewards_tx, &mining_receiver, &params.db, current_height).await undo_rewards_transaction(
rewards_tx,
&mining_receiver,
&params.db,
current_height,
)
.await
} }
Transaction::Transfer(transfer_tx) => { Transaction::Transfer(transfer_tx) => {
undo_transfer_transaction(transfer_tx.clone(), &mining_receiver, &params.db).await; undo_transfer_transaction(transfer_tx.clone(), &mining_receiver, &params.db)
.await;
rolled_back_transactions.push(Transaction::Transfer(transfer_tx)); rolled_back_transactions.push(Transaction::Transfer(transfer_tx));
} }
Transaction::Burn(burn_tx) => { Transaction::Burn(burn_tx) => {
@ -50,20 +57,35 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
rolled_back_transactions.push(Transaction::Burn(burn_tx)); rolled_back_transactions.push(Transaction::Burn(burn_tx));
} }
Transaction::Token(create_token_tx) => { Transaction::Token(create_token_tx) => {
undo_create_token_transaction(create_token_tx.clone(), &mining_receiver, &params.db) undo_create_token_transaction(
.await; create_token_tx.clone(),
&mining_receiver,
&params.db,
)
.await;
rolled_back_transactions.push(Transaction::Token(create_token_tx)); rolled_back_transactions.push(Transaction::Token(create_token_tx));
} }
Transaction::IssueToken(issue_token_tx) => { Transaction::IssueToken(issue_token_tx) => {
undo_issue_token_transaction(issue_token_tx.clone(), &mining_receiver, &params.db).await; undo_issue_token_transaction(
issue_token_tx.clone(),
&mining_receiver,
&params.db,
)
.await;
rolled_back_transactions.push(Transaction::IssueToken(issue_token_tx)); rolled_back_transactions.push(Transaction::IssueToken(issue_token_tx));
} }
Transaction::Nft(create_nft_tx) => { Transaction::Nft(create_nft_tx) => {
undo_create_nft_transaction(create_nft_tx.clone(), &mining_receiver, &params.db).await; undo_create_nft_transaction(
create_nft_tx.clone(),
&mining_receiver,
&params.db,
)
.await;
rolled_back_transactions.push(Transaction::Nft(create_nft_tx)); rolled_back_transactions.push(Transaction::Nft(create_nft_tx));
} }
Transaction::Marketing(marketing_tx) => { Transaction::Marketing(marketing_tx) => {
undo_marketing_transaction(marketing_tx.clone(), &mining_receiver, &params.db).await; undo_marketing_transaction(marketing_tx.clone(), &mining_receiver, &params.db)
.await;
rolled_back_transactions.push(Transaction::Marketing(marketing_tx)); rolled_back_transactions.push(Transaction::Marketing(marketing_tx));
} }
Transaction::Swap(swap_tx) => { Transaction::Swap(swap_tx) => {
@ -71,15 +93,22 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
rolled_back_transactions.push(Transaction::Swap(swap_tx)); rolled_back_transactions.push(Transaction::Swap(swap_tx));
} }
Transaction::Lender(loan_tx) => { Transaction::Lender(loan_tx) => {
undo_loan_creation_transaction(loan_tx.clone(), &mining_receiver, &params.db).await; undo_loan_creation_transaction(loan_tx.clone(), &mining_receiver, &params.db)
.await;
rolled_back_transactions.push(Transaction::Lender(loan_tx)); rolled_back_transactions.push(Transaction::Lender(loan_tx));
} }
Transaction::Borrower(borrower_tx) => { Transaction::Borrower(borrower_tx) => {
undo_borrower_transaction(borrower_tx.clone(), &mining_receiver, &params.db).await?; undo_borrower_transaction(borrower_tx.clone(), &mining_receiver, &params.db)
.await?;
rolled_back_transactions.push(Transaction::Borrower(borrower_tx)); rolled_back_transactions.push(Transaction::Borrower(borrower_tx));
} }
Transaction::Collateral(collateral_tx) => { Transaction::Collateral(collateral_tx) => {
undo_collateral_transaction(collateral_tx.clone(), &mining_receiver, &params.db).await?; undo_collateral_transaction(
collateral_tx.clone(),
&mining_receiver,
&params.db,
)
.await?;
rolled_back_transactions.push(Transaction::Collateral(collateral_tx)); rolled_back_transactions.push(Transaction::Collateral(collateral_tx));
} }
Transaction::Genesis(_) => { Transaction::Genesis(_) => {
@ -87,10 +116,11 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
// the requested rollback boundary is invalid. // the requested rollback boundary is invalid.
return Err( return Err(
"Genesis transaction cannot be undone by orphan rollback".to_string() "Genesis transaction cannot be undone by orphan rollback".to_string()
) );
} }
Transaction::Vanity(vanity_tx) => { Transaction::Vanity(vanity_tx) => {
undo_vanity_transaction(vanity_tx.clone(), &mining_receiver, &params.db).await?; undo_vanity_transaction(vanity_tx.clone(), &mining_receiver, &params.db)
.await?;
rolled_back_transactions.push(Transaction::Vanity(vanity_tx)); rolled_back_transactions.push(Transaction::Vanity(vanity_tx));
} }
} }

View File

@ -11,12 +11,12 @@ use crate::blocks::transfer::TransferTransaction;
use crate::blocks::vanity::VanityAddressTransaction; use crate::blocks::vanity::VanityAddressTransaction;
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::records::memory::mempool::{restore_processed_by_signatures, BASECOIN}; use crate::records::memory::mempool::{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;
use crate::decode;
async fn restore_if_spendable<F, Fut>(signatures: &[String], spendable: bool, insert: F) async fn restore_if_spendable<F, Fut>(signatures: &[String], spendable: bool, insert: F)
where where
@ -107,8 +107,14 @@ pub async fn restore_create_nft(transaction: &CreateNftTransaction, db: &Db) {
pub async fn restore_marketing(transaction: &MarketingTransaction, db: &Db) { pub async fn restore_marketing(transaction: &MarketingTransaction, db: &Db) {
let marketing = &transaction.unsigned_marketing; let marketing = &transaction.unsigned_marketing;
let spendable = let spendable = balance_checkup(
balance_checkup(db, 0, marketing.txfee, BASECOIN.clone(), &marketing.advertiser).await; db,
0,
marketing.txfee,
BASECOIN.clone(),
&marketing.advertiser,
)
.await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable(&[signature], spendable, || async {
@ -123,31 +129,53 @@ pub async fn restore_swap(transaction: &SwapTransaction, db: &Db) {
let asset2 = nft_asset_name(&swap.ticker2, swap.nft_series2); let asset2 = nft_asset_name(&swap.ticker2, swap.nft_series2);
let value1 = swap.value1.saturating_add(swap.tip1); let value1 = swap.value1.saturating_add(swap.tip1);
let value2 = swap.value2.saturating_add(swap.tip2); let value2 = swap.value2.saturating_add(swap.tip2);
let sender1_spendable = let sender1_spendable = balance_checkup(db, value1, swap.txfee1, asset1, &swap.sender1).await;
balance_checkup(db, value1, swap.txfee1, asset1, &swap.sender1).await; let sender2_spendable = balance_checkup(db, value2, swap.txfee2, asset2, &swap.sender2).await;
let sender2_spendable = let signatures = vec![
balance_checkup(db, value2, swap.txfee2, asset2, &swap.sender2).await; transaction.signature1.clone(),
let signatures = vec![transaction.signature1.clone(), transaction.signature2.clone()]; transaction.signature2.clone(),
];
restore_if_spendable(&signatures, sender1_spendable && sender2_spendable, || async { restore_if_spendable(
let _ = transaction.add_to_memory().await; &signatures,
}) sender1_spendable && sender2_spendable,
|| async {
let _ = transaction.add_to_memory().await;
},
)
.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; let loan = &transaction.unsigned_loan_contract;
let lender_spendable = let lender_spendable = balance_checkup(
balance_checkup(db, loan.loan_amount, loan.txfee, loan.loan_coin.clone(), &loan.lender) db,
.await; loan.loan_amount,
let borrower_spendable = loan.txfee,
balance_checkup(db, loan.collateral_amount, 0, loan.collateral.clone(), &loan.borrower) loan.loan_coin.clone(),
.await; &loan.lender,
let signatures = vec![transaction.signature1.clone(), transaction.signature2.clone()]; )
.await;
let borrower_spendable = balance_checkup(
db,
loan.collateral_amount,
0,
loan.collateral.clone(),
&loan.borrower,
)
.await;
let signatures = vec![
transaction.signature1.clone(),
transaction.signature2.clone(),
];
restore_if_spendable(&signatures, lender_spendable && borrower_spendable, || async { restore_if_spendable(
let _ = transaction.add_to_memory().await; &signatures,
}) lender_spendable && borrower_spendable,
|| async {
let _ = transaction.add_to_memory().await;
},
)
.await; .await;
} }
@ -168,8 +196,14 @@ pub async fn restore_borrower(transaction: &ContractPaymentTransaction, db: &Db)
pub async fn restore_collateral(transaction: &CollateralClaimTransaction, db: &Db) { pub async fn restore_collateral(transaction: &CollateralClaimTransaction, db: &Db) {
let collateral = &transaction.unsigned_collateral_claim; let collateral = &transaction.unsigned_collateral_claim;
let spendable = let spendable = balance_checkup(
balance_checkup(db, 0, collateral.txfee, BASECOIN.clone(), &collateral.address).await; db,
0,
collateral.txfee,
BASECOIN.clone(),
&collateral.address,
)
.await;
let signature = transaction.signature.clone(); let signature = transaction.signature.clone();
restore_if_spendable(&[signature], spendable, || async { restore_if_spendable(&[signature], spendable, || async {

View File

@ -1,93 +1,92 @@
use crate::blocks::burn::BurnTransaction; use crate::blocks::burn::BurnTransaction;
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::nft_assets::nft_asset_name; use crate::common::nft_assets::nft_asset_name;
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::records::record_chain::token_provenance::remove_token_history_entry; use crate::records::record_chain::token_provenance::remove_token_history_entry;
use crate::sled::Db; use crate::sled::Db;
pub async fn undo_burn_transaction(transaction: BurnTransaction, mining_receiver: &str, db: &Db) { pub async fn undo_burn_transaction(transaction: BurnTransaction, mining_receiver: &str, db: &Db) {
// Reverse the burn fee and burned-asset balance movement before // Reverse the burn fee and burned-asset balance movement before
// restoring the live token or NFT state back into the active chain. // restoring the live token or NFT state back into the active chain.
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();
let burned_asset = nft_asset_name( let burned_asset = nft_asset_name(
&transaction.unsigned_burn.coin, &transaction.unsigned_burn.coin,
transaction.unsigned_burn.nft_series, transaction.unsigned_burn.nft_series,
); );
// Remove the miner fee, refund the burner fee, and return the burned asset // Remove the miner fee, refund the burner fee, and return the burned asset
// to the burner balance. // to the burner balance.
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
mining_receiver, mining_receiver,
transaction.unsigned_burn.txfee, transaction.unsigned_burn.txfee,
&type_str, &type_str,
operand_subtraction, operand_subtraction,
); );
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
&transaction.unsigned_burn.address, &transaction.unsigned_burn.address,
transaction.unsigned_burn.txfee, transaction.unsigned_burn.txfee,
&type_str, &type_str,
operand_addition, operand_addition,
); );
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
&transaction.unsigned_burn.address, &transaction.unsigned_burn.address,
transaction.unsigned_burn.value, transaction.unsigned_burn.value,
&burned_asset, &burned_asset,
operand_addition, operand_addition,
); );
let hash_binary = decode(&transaction.unsigned_burn.hash().await).unwrap(); let hash_binary = decode(&transaction.unsigned_burn.hash().await).unwrap();
// Delete the txid lookup inserted when the burn was saved. // Delete the txid lookup inserted when the burn was saved.
let txid_tree = db.open_tree("txid").unwrap(); let txid_tree = db.open_tree("txid").unwrap();
txid_tree.remove(hash_binary.clone()).unwrap(); txid_tree.remove(hash_binary.clone()).unwrap();
// Restore NFT rows directly, or add the burned amount back into the // Restore NFT rows directly, or add the burned amount back into the
// fungible token supply if this burn targeted a token asset. // fungible token supply if this burn targeted a token asset.
let nft_tree = db.open_tree("nfts").unwrap(); let nft_tree = db.open_tree("nfts").unwrap();
let nft_origin_tree = db.open_tree("nft_origins").unwrap(); let nft_origin_tree = db.open_tree("nft_origins").unwrap();
if nft_origin_tree if nft_origin_tree
.contains_key(burned_asset.as_bytes()) .contains_key(burned_asset.as_bytes())
.unwrap_or(false) .unwrap_or(false)
{ {
// NFT burns remove the live NFT row; rollback restores the row and // NFT burns remove the live NFT row; rollback restores the row and
// removes the burn from NFT history. // removes the burn from NFT history.
let _ = remove_nft_history_entry(db, &burned_asset, &hash_binary); let _ = remove_nft_history_entry(db, &burned_asset, &hash_binary);
let _ = nft_tree.insert(burned_asset.as_bytes(), b"1"); let _ = nft_tree.insert(burned_asset.as_bytes(), b"1");
} else { } else {
// Token burns reduce supply; rollback adds the burned amount back. // Token burns reduce supply; rollback adds the burned amount back.
let _ = remove_token_history_entry(db, &transaction.unsigned_burn.coin, &hash_binary); let _ = remove_token_history_entry(db, &transaction.unsigned_burn.coin, &hash_binary);
let token_tree = db.open_tree("tokens").unwrap(); let token_tree = db.open_tree("tokens").unwrap();
let current_supply = token_tree let current_supply = token_tree
.get(transaction.unsigned_burn.coin.as_bytes()) .get(transaction.unsigned_burn.coin.as_bytes())
.unwrap() .unwrap()
.map(|bytes| { .map(|bytes| {
let mut supply_bytes = [0u8; 8]; let mut supply_bytes = [0u8; 8];
supply_bytes.copy_from_slice(bytes.as_ref()); supply_bytes.copy_from_slice(bytes.as_ref());
u64::from_le_bytes(supply_bytes) u64::from_le_bytes(supply_bytes)
}) })
.unwrap_or(0); .unwrap_or(0);
let restored_supply = current_supply.saturating_add(transaction.unsigned_burn.value); let restored_supply = current_supply.saturating_add(transaction.unsigned_burn.value);
let _ = token_tree.insert( let _ = token_tree.insert(
transaction.unsigned_burn.coin.as_bytes(), transaction.unsigned_burn.coin.as_bytes(),
&restored_supply.to_le_bytes(), &restored_supply.to_le_bytes(),
); );
} }
} }

View File

@ -1,81 +1,80 @@
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
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::nft_assets::nft_asset_name; use crate::common::nft_assets::nft_asset_name;
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, remove_nft_origin}; use crate::records::record_chain::nft_provenance::{remove_nft_history_entry, remove_nft_origin};
use crate::sled::Db; use crate::sled::Db;
const NFT_UNIT: u64 = 100_000_000; const NFT_UNIT: u64 = 100_000_000;
pub async fn undo_create_nft_transaction( pub async fn undo_create_nft_transaction(
transaction: CreateNftTransaction, transaction: CreateNftTransaction,
mining_receiver: &str, mining_receiver: &str,
db: &Db, db: &Db,
) { ) {
// remove the created nft state and restore the creator balances // remove the created nft state and restore the creator balances
// when a create-nft transaction is rolled back // when a create-nft transaction is rolled back
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();
let (txfee, creator) = ( let (txfee, creator) = (
&transaction.unsigned_create_nft.txfee, &transaction.unsigned_create_nft.txfee,
&transaction.unsigned_create_nft.creator, &transaction.unsigned_create_nft.creator,
); );
// Remove the miner fee and refund the creator's base-coin fee. // Remove the miner fee and refund the creator's base-coin fee.
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
mining_receiver, mining_receiver,
*txfee, *txfee,
&type_str, &type_str,
operand_subtraction, operand_subtraction,
); );
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition); let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
let hash_binary = decode(&transaction.unsigned_create_nft.hash().await).unwrap(); let hash_binary = decode(&transaction.unsigned_create_nft.hash().await).unwrap();
// Remove the create-NFT transaction lookup from the txid tree. // Remove the create-NFT transaction lookup from the txid tree.
let tree = db.open_tree("txid").unwrap(); let tree = db.open_tree("txid").unwrap();
let key = hash_binary.clone(); let key = hash_binary.clone();
tree.remove(key).unwrap(); tree.remove(key).unwrap();
// remove each created nft item and clear the // remove each created nft item and clear the
// associated provenance entries // associated provenance entries
let tree = db.open_tree("nfts").unwrap(); let tree = db.open_tree("nfts").unwrap();
if transaction.unsigned_create_nft.series == 1 { if transaction.unsigned_create_nft.series == 1 {
// Series creation mints numbered items, so each item is removed // Series creation mints numbered items, so each item is removed
// individually from balances, provenance, origins, and the NFT tree. // individually from balances, provenance, origins, and the NFT tree.
for item_number in 1..=transaction.unsigned_create_nft.count { for item_number in 1..=transaction.unsigned_create_nft.count {
let nft_name = nft_asset_name(&transaction.unsigned_create_nft.nft_name, item_number); let nft_name = nft_asset_name(&transaction.unsigned_create_nft.nft_name, item_number);
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
creator, creator,
NFT_UNIT, NFT_UNIT,
&nft_name, &nft_name,
operand_subtraction, operand_subtraction,
); );
let _ = remove_nft_history_entry(db, &nft_name, &hash_binary); let _ = remove_nft_history_entry(db, &nft_name, &hash_binary);
let _ = remove_nft_origin(db, &nft_name); let _ = remove_nft_origin(db, &nft_name);
tree.remove(nft_name.as_bytes()).unwrap(); tree.remove(nft_name.as_bytes()).unwrap();
} }
} else { } else {
// Single NFT creation only writes the base NFT name. // Single NFT creation only writes the base NFT name.
let nft_name = &transaction.unsigned_create_nft.nft_name; let nft_name = &transaction.unsigned_create_nft.nft_name;
let _ = let _ =
balance_sheet_operation_with_db(db, creator, NFT_UNIT, nft_name, operand_subtraction); balance_sheet_operation_with_db(db, creator, NFT_UNIT, nft_name, operand_subtraction);
let _ = remove_nft_history_entry(db, nft_name, &hash_binary); let _ = remove_nft_history_entry(db, nft_name, &hash_binary);
let _ = remove_nft_origin(db, nft_name); let _ = remove_nft_origin(db, nft_name);
tree.remove(nft_name.as_bytes()).unwrap(); tree.remove(nft_name.as_bytes()).unwrap();
} }
} }

View File

@ -1,69 +1,68 @@
use crate::blocks::token::CreateTokenTransaction; use crate::blocks::token::CreateTokenTransaction;
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::token_provenance::clear_token_history; use crate::records::record_chain::token_provenance::clear_token_history;
use crate::sled::Db; use crate::sled::Db;
pub async fn undo_create_token_transaction( pub async fn undo_create_token_transaction(
transaction: CreateTokenTransaction, transaction: CreateTokenTransaction,
mining_receiver: &str, mining_receiver: &str,
db: &Db, db: &Db,
) { ) {
// remove the created token state and restore the creator balances // remove the created token state and restore the creator balances
// when a create-token transaction is rolled back // when a create-token transaction is rolled back
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();
let (txfee, creator, ticker, number) = ( let (txfee, creator, ticker, number) = (
&transaction.unsigned_create_token.txfee, &transaction.unsigned_create_token.txfee,
&transaction.unsigned_create_token.creator, &transaction.unsigned_create_token.creator,
&transaction.unsigned_create_token.ticker, &transaction.unsigned_create_token.ticker,
&transaction.unsigned_create_token.number, &transaction.unsigned_create_token.number,
); );
// Remove the miner fee, refund the creator fee, and remove the created // Remove the miner fee, refund the creator fee, and remove the created
// supply from the creator balance. // supply from the creator balance.
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
mining_receiver, mining_receiver,
*txfee, *txfee,
&type_str, &type_str,
operand_subtraction, operand_subtraction,
); );
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition); let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
let _ = balance_sheet_operation_with_db(db, creator, *number, ticker, operand_subtraction); let _ = balance_sheet_operation_with_db(db, creator, *number, ticker, operand_subtraction);
let ticker_binary = &transaction.unsigned_create_token.ticker.as_bytes(); let ticker_binary = &transaction.unsigned_create_token.ticker.as_bytes();
let hash_binary = decode(&transaction.unsigned_create_token.hash().await).unwrap(); let hash_binary = decode(&transaction.unsigned_create_token.hash().await).unwrap();
// Remove the create-token transaction lookup from the txid tree. // Remove the create-token transaction lookup from the txid tree.
let tree = db.open_tree("txid").unwrap(); let tree = db.open_tree("txid").unwrap();
let key = hash_binary.clone(); let key = hash_binary.clone();
tree.remove(key).unwrap(); tree.remove(key).unwrap();
// remove the token definition and origin entry // remove the token definition and origin entry
// created when the token was first saved // created when the token was first saved
let tree = db.open_tree("tokens").unwrap(); let tree = db.open_tree("tokens").unwrap();
let key = ticker_binary; let key = ticker_binary;
tree.remove(key).unwrap(); tree.remove(key).unwrap();
let origin_tree = db.open_tree("token_origins").unwrap(); let origin_tree = db.open_tree("token_origins").unwrap();
origin_tree.remove(key).unwrap(); origin_tree.remove(key).unwrap();
let limit_tree = db.open_tree("token_limits").unwrap(); let limit_tree = db.open_tree("token_limits").unwrap();
limit_tree.remove(key).unwrap(); limit_tree.remove(key).unwrap();
// Token history is cleared because the token itself no longer exists. // Token history is cleared because the token itself no longer exists.
let _ = clear_token_history(db, ticker); let _ = clear_token_history(db, ticker);
} }

View File

@ -1,62 +1,61 @@
use crate::blocks::issue_token::IssueTokenTransaction; use crate::blocks::issue_token::IssueTokenTransaction;
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::token_provenance::remove_token_history_entry; use crate::records::record_chain::token_provenance::remove_token_history_entry;
use crate::sled::Db; use crate::sled::Db;
pub async fn undo_issue_token_transaction( pub async fn undo_issue_token_transaction(
transaction: IssueTokenTransaction, transaction: IssueTokenTransaction,
mining_receiver: &str, mining_receiver: &str,
db: &Db, db: &Db,
) { ) {
// Reverse the issued supply and fee movements so rollback restores // Reverse the issued supply and fee movements so rollback restores
// both the creator balance and the stored token supply. // both the creator balance and the stored token supply.
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();
let txfee = &transaction.unsigned_issue_token.txfee; let txfee = &transaction.unsigned_issue_token.txfee;
let creator = &transaction.unsigned_issue_token.creator; let creator = &transaction.unsigned_issue_token.creator;
let ticker = &transaction.unsigned_issue_token.ticker; let ticker = &transaction.unsigned_issue_token.ticker;
let number = &transaction.unsigned_issue_token.number; let number = &transaction.unsigned_issue_token.number;
// Remove the miner fee, refund the issuer fee, and take the issued amount // Remove the miner fee, refund the issuer fee, and take the issued amount
// back out of the issuer balance. // back out of the issuer balance.
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
mining_receiver, mining_receiver,
*txfee, *txfee,
&type_str, &type_str,
operand_subtraction, operand_subtraction,
); );
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition); let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
let _ = balance_sheet_operation_with_db(db, creator, *number, ticker, operand_subtraction); let _ = balance_sheet_operation_with_db(db, creator, *number, ticker, operand_subtraction);
let hash_binary = decode(&transaction.unsigned_issue_token.hash().await).unwrap(); let hash_binary = decode(&transaction.unsigned_issue_token.hash().await).unwrap();
// Delete the issued-token transaction lookup and provenance record. // Delete the issued-token transaction lookup and provenance record.
let txid_tree = db.open_tree("txid").unwrap(); let txid_tree = db.open_tree("txid").unwrap();
txid_tree.remove(hash_binary.clone()).unwrap(); txid_tree.remove(hash_binary.clone()).unwrap();
let _ = remove_token_history_entry(db, ticker, &hash_binary); let _ = remove_token_history_entry(db, ticker, &hash_binary);
// Restore the previous live token supply by subtracting the issued amount. // Restore the previous live token supply by subtracting the issued amount.
let token_tree = db.open_tree("tokens").unwrap(); let token_tree = db.open_tree("tokens").unwrap();
if let Ok(Some(existing_supply)) = token_tree.get(ticker.as_bytes()) { if let Ok(Some(existing_supply)) = token_tree.get(ticker.as_bytes()) {
let mut supply_bytes = [0u8; 8]; let mut supply_bytes = [0u8; 8];
supply_bytes.copy_from_slice(existing_supply.as_ref()); supply_bytes.copy_from_slice(existing_supply.as_ref());
let current_supply = u64::from_le_bytes(supply_bytes); let current_supply = u64::from_le_bytes(supply_bytes);
let restored_supply = current_supply.saturating_sub(*number); let restored_supply = current_supply.saturating_sub(*number);
let _ = token_tree.insert(ticker.as_bytes(), &restored_supply.to_le_bytes()); let _ = token_tree.insert(ticker.as_bytes(), &restored_supply.to_le_bytes());
} }
} }

View File

@ -1,93 +1,92 @@
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::sled::Db; use crate::sled::Db;
pub async fn undo_loan_creation_transaction( pub async fn undo_loan_creation_transaction(
transaction: LoanContractTransaction, transaction: LoanContractTransaction,
mining_receiver: &str, mining_receiver: &str,
db: &Db, db: &Db,
) { ) {
// remove a loan contract and restore both sides of the // remove a loan contract and restore both sides of the
// contract balances when the block is rolled back // contract balances when the block is rolled back
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();
let (txfee, loan_coin, loan_amount, lender, collateral, collateral_amount, borrower, hash) = ( let (txfee, loan_coin, loan_amount, lender, collateral, collateral_amount, borrower, hash) = (
&transaction.unsigned_loan_contract.txfee, &transaction.unsigned_loan_contract.txfee,
&transaction.unsigned_loan_contract.loan_coin.clone(), &transaction.unsigned_loan_contract.loan_coin.clone(),
&transaction.unsigned_loan_contract.loan_amount, &transaction.unsigned_loan_contract.loan_amount,
&transaction.unsigned_loan_contract.lender.clone(), &transaction.unsigned_loan_contract.lender.clone(),
&transaction.unsigned_loan_contract.collateral.clone(), &transaction.unsigned_loan_contract.collateral.clone(),
&transaction.unsigned_loan_contract.collateral_amount, &transaction.unsigned_loan_contract.collateral_amount,
&transaction.unsigned_loan_contract.borrower.clone(), &transaction.unsigned_loan_contract.borrower.clone(),
&transaction.hash.clone(), &transaction.hash.clone(),
); );
let collateral_wallet = format!("collateral_{hash}"); let collateral_wallet = format!("collateral_{hash}");
// undo the fee, loan distribution, and collateral // undo the fee, loan distribution, and collateral
// movement that were applied when the contract was saved // movement that were applied when the contract was saved
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
mining_receiver, mining_receiver,
*txfee, *txfee,
&type_str, &type_str,
operand_subtraction, operand_subtraction,
); );
let _ = balance_sheet_operation_with_db(db, lender, *txfee, &type_str, operand_addition); let _ = balance_sheet_operation_with_db(db, lender, *txfee, &type_str, operand_addition);
let _ = let _ =
balance_sheet_operation_with_db(db, borrower, *loan_amount, loan_coin, operand_subtraction); balance_sheet_operation_with_db(db, borrower, *loan_amount, loan_coin, operand_subtraction);
let _ = balance_sheet_operation_with_db(db, lender, *loan_amount, loan_coin, operand_addition); let _ = balance_sheet_operation_with_db(db, lender, *loan_amount, loan_coin, operand_addition);
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
&collateral_wallet, &collateral_wallet,
*collateral_amount, *collateral_amount,
collateral, collateral,
operand_subtraction, operand_subtraction,
); );
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
borrower, borrower,
*collateral_amount, *collateral_amount,
collateral, collateral,
operand_addition, operand_addition,
); );
let hash_binary = decode(hash).unwrap(); let hash_binary = decode(hash).unwrap();
// delete the txid and remove the active loan record // delete the txid and remove the active loan record
// so the contract no longer exists on-chain // so the contract no longer exists on-chain
let tree = db.open_tree("txid").unwrap(); let tree = db.open_tree("txid").unwrap();
let key = hash_binary.clone(); let key = hash_binary.clone();
tree.remove(key).unwrap(); tree.remove(key).unwrap();
let tree = db.open_tree("loan").unwrap(); let tree = db.open_tree("loan").unwrap();
let loan_key = decode(&transaction.unsigned_loan_contract.hash().await).unwrap(); let loan_key = decode(&transaction.unsigned_loan_contract.hash().await).unwrap();
tree.remove(loan_key).unwrap(); tree.remove(loan_key).unwrap();
let nft_tree = db.open_tree("nfts").unwrap(); let nft_tree = db.open_tree("nfts").unwrap();
// Loan creation can move NFT collateral or NFT loan assets, so clear // Loan creation can move NFT collateral or NFT loan assets, so clear
// provenance entries for whichever side is NFT-backed. // provenance entries for whichever side is NFT-backed.
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, &hash_binary); let _ = remove_nft_history_entry(db, collateral, &hash_binary);
} }
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, &hash_binary); let _ = remove_nft_history_entry(db, loan_coin, &hash_binary);
} }
} }

View File

@ -1,49 +1,48 @@
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
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::sled::Db; use crate::sled::Db;
pub async fn undo_marketing_transaction( pub async fn undo_marketing_transaction(
transaction: MarketingTransaction, transaction: MarketingTransaction,
mining_receiver: &str, mining_receiver: &str,
db: &Db, db: &Db,
) { ) {
// reverse the fee payment and remove the marketing txid // reverse the fee payment and remove the marketing txid
// when a marketing transaction is rolled back // when a marketing transaction is rolled back
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();
let (txfee, advertiser) = ( let (txfee, advertiser) = (
&transaction.unsigned_marketing.txfee, &transaction.unsigned_marketing.txfee,
&transaction.unsigned_marketing.advertiser, &transaction.unsigned_marketing.advertiser,
); );
// Remove the miner fee and refund the advertiser fee. // Remove the miner fee and refund the advertiser fee.
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
mining_receiver, mining_receiver,
*txfee, *txfee,
&type_str, &type_str,
operand_subtraction, operand_subtraction,
); );
let _ = balance_sheet_operation_with_db(db, advertiser, *txfee, &type_str, operand_addition); let _ = balance_sheet_operation_with_db(db, advertiser, *txfee, &type_str, operand_addition);
let hash = decode(&transaction.unsigned_marketing.hash().await).unwrap(); let hash = decode(&transaction.unsigned_marketing.hash().await).unwrap();
// Remove the marketing transaction lookup from the txid tree. // Remove the marketing transaction lookup from the txid tree.
let tree = db.open_tree("txid").unwrap(); let tree = db.open_tree("txid").unwrap();
let key = hash; let key = hash;
tree.remove(key).unwrap(); tree.remove(key).unwrap();
} }

View File

@ -1,8 +1,10 @@
use crate::blocks::rewards::RewardsTransaction; use crate::blocks::rewards::RewardsTransaction;
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::rewards_tx::{remove_reward_credit_marker, reward_credit_applied}; use crate::records::record_chain::rewards_tx::{
remove_reward_credit_marker, reward_credit_applied,
};
use crate::sled::Db; use crate::sled::Db;
pub async fn undo_rewards_transaction( pub async fn undo_rewards_transaction(
@ -11,34 +13,34 @@ pub async fn undo_rewards_transaction(
db: &Db, db: &Db,
block_height: u32, block_height: u32,
) { ) {
// remove the miner reward and delete the reward txid // remove the miner reward and delete the reward txid
// when the block that minted it is rolled back // when the block that minted it is rolled back
let operand = "subtraction"; let operand = "subtraction";
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();
let value = transaction.unsigned.value; let value = transaction.unsigned.value;
// Rewards are only spendable after finalization, so rollback subtracts // Rewards are only spendable after finalization, so rollback subtracts
// them only when that delayed credit has actually been applied. // them only when that delayed credit has actually been applied.
if reward_credit_applied(db, block_height) { if reward_credit_applied(db, block_height) {
let _ = balance_sheet_operation_with_db(db, mining_receiver, value, &type_str, operand); let _ = balance_sheet_operation_with_db(db, mining_receiver, value, &type_str, operand);
remove_reward_credit_marker(db, block_height); remove_reward_credit_marker(db, block_height);
} }
let hash = decode(transaction.unsigned.hash().await).unwrap(); let hash = decode(transaction.unsigned.hash().await).unwrap();
// Remove the reward transaction lookup from the txid tree. // Remove the reward transaction lookup from the txid tree.
let tree = db.open_tree("txid").unwrap(); let tree = db.open_tree("txid").unwrap();
let key = hash; let key = hash;
tree.remove(key).unwrap(); tree.remove(key).unwrap();
} }

View File

@ -1,103 +1,102 @@
use crate::blocks::swap::SwapTransaction; use crate::blocks::swap::SwapTransaction;
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::nft_assets::nft_asset_name; use crate::common::nft_assets::nft_asset_name;
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::sled::Db; use crate::sled::Db;
pub async fn undo_swap_transaction(transaction: SwapTransaction, mining_receiver: &str, db: &Db) { pub async fn undo_swap_transaction(transaction: SwapTransaction, mining_receiver: &str, db: &Db) {
// reverse both sides of the asset exchange and remove the // reverse both sides of the asset exchange and remove the
// swap transaction from chain state during rollback // swap transaction from chain state during 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();
let ( let (
txfee1, txfee1,
txfee2, txfee2,
value1, value1,
value2, value2,
ticker1, ticker1,
ticker2, ticker2,
nft_series1, nft_series1,
nft_series2, nft_series2,
sender1, sender1,
sender2, sender2,
tip1, tip1,
tip2, tip2,
) = ( ) = (
&transaction.unsigned_swap.txfee1, &transaction.unsigned_swap.txfee1,
&transaction.unsigned_swap.txfee2, &transaction.unsigned_swap.txfee2,
&transaction.unsigned_swap.value1, &transaction.unsigned_swap.value1,
&transaction.unsigned_swap.value2, &transaction.unsigned_swap.value2,
&transaction.unsigned_swap.ticker1, &transaction.unsigned_swap.ticker1,
&transaction.unsigned_swap.ticker2, &transaction.unsigned_swap.ticker2,
&transaction.unsigned_swap.nft_series1, &transaction.unsigned_swap.nft_series1,
&transaction.unsigned_swap.nft_series2, &transaction.unsigned_swap.nft_series2,
&transaction.unsigned_swap.sender1, &transaction.unsigned_swap.sender1,
&transaction.unsigned_swap.sender2, &transaction.unsigned_swap.sender2,
&transaction.unsigned_swap.tip1, &transaction.unsigned_swap.tip1,
&transaction.unsigned_swap.tip2, &transaction.unsigned_swap.tip2,
); );
let asset1 = nft_asset_name(ticker1, *nft_series1); let asset1 = nft_asset_name(ticker1, *nft_series1);
let asset2 = nft_asset_name(ticker2, *nft_series2); let asset2 = nft_asset_name(ticker2, *nft_series2);
// Refund both base-coin fees and remove those fees from the miner balance. // Refund both base-coin fees and remove those fees from the miner balance.
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
mining_receiver, mining_receiver,
*txfee1, *txfee1,
&type_str, &type_str,
operand_subtraction, operand_subtraction,
); );
let _ = balance_sheet_operation_with_db(db, sender1, *txfee1, &type_str, operand_addition); let _ = balance_sheet_operation_with_db(db, sender1, *txfee1, &type_str, operand_addition);
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
mining_receiver, mining_receiver,
*txfee2, *txfee2,
&type_str, &type_str,
operand_subtraction, operand_subtraction,
); );
let _ = balance_sheet_operation_with_db(db, sender2, *txfee2, &type_str, operand_addition); let _ = balance_sheet_operation_with_db(db, sender2, *txfee2, &type_str, operand_addition);
// Tips are paid in the swapped assets, so they must be reversed per asset. // Tips are paid in the swapped assets, so they must be reversed per asset.
let _ = let _ =
balance_sheet_operation_with_db(db, mining_receiver, *tip1, &asset1, operand_subtraction); balance_sheet_operation_with_db(db, mining_receiver, *tip1, &asset1, operand_subtraction);
let _ = balance_sheet_operation_with_db(db, sender1, *tip1, &asset1, operand_addition); let _ = balance_sheet_operation_with_db(db, sender1, *tip1, &asset1, operand_addition);
let _ = let _ =
balance_sheet_operation_with_db(db, mining_receiver, *tip2, &asset2, operand_subtraction); balance_sheet_operation_with_db(db, mining_receiver, *tip2, &asset2, operand_subtraction);
let _ = balance_sheet_operation_with_db(db, sender2, *tip2, &asset2, operand_addition); let _ = balance_sheet_operation_with_db(db, sender2, *tip2, &asset2, operand_addition);
// Reverse the actual two-sided asset exchange. // Reverse the actual two-sided asset exchange.
let _ = balance_sheet_operation_with_db(db, sender1, *value2, &asset2, operand_subtraction); let _ = balance_sheet_operation_with_db(db, sender1, *value2, &asset2, operand_subtraction);
let _ = balance_sheet_operation_with_db(db, sender2, *value2, &asset2, operand_addition); let _ = balance_sheet_operation_with_db(db, sender2, *value2, &asset2, operand_addition);
let _ = balance_sheet_operation_with_db(db, sender2, *value1, &asset1, operand_subtraction); let _ = balance_sheet_operation_with_db(db, sender2, *value1, &asset1, operand_subtraction);
let _ = balance_sheet_operation_with_db(db, sender1, *value1, &asset1, operand_addition); let _ = balance_sheet_operation_with_db(db, sender1, *value1, &asset1, operand_addition);
// Convert the txid hash back to bytes for tree lookup/removal. // Convert the txid hash back to bytes for tree lookup/removal.
let hash = decode(&transaction.unsigned_swap.hash().await).unwrap(); let hash = decode(&transaction.unsigned_swap.hash().await).unwrap();
// Remove the txid lookup for the rolled-back swap. // Remove the txid lookup for the rolled-back swap.
let tree = db.open_tree("txid").unwrap(); let tree = db.open_tree("txid").unwrap();
let key = hash.clone(); let key = hash.clone();
tree.remove(key).unwrap(); tree.remove(key).unwrap();
let nft_tree = db.open_tree("nfts").unwrap(); let nft_tree = db.open_tree("nfts").unwrap();
// If either side of the swap was an NFT, remove this swap from that asset's // If either side of the swap was an NFT, remove this swap from that asset's
// provenance history as well. // provenance history as well.
if nft_tree.contains_key(asset1.as_bytes()).unwrap_or(false) { if nft_tree.contains_key(asset1.as_bytes()).unwrap_or(false) {
let _ = remove_nft_history_entry(db, &asset1, &hash); let _ = remove_nft_history_entry(db, &asset1, &hash);
} }
if nft_tree.contains_key(asset2.as_bytes()).unwrap_or(false) { if nft_tree.contains_key(asset2.as_bytes()).unwrap_or(false) {
let _ = remove_nft_history_entry(db, &asset2, &hash); let _ = remove_nft_history_entry(db, &asset2, &hash);
} }
} }

View File

@ -1,71 +1,70 @@
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
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::nft_assets::nft_asset_name; use crate::common::nft_assets::nft_asset_name;
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::sled::Db; use crate::sled::Db;
pub async fn undo_transfer_transaction( pub async fn undo_transfer_transaction(
transaction: TransferTransaction, transaction: TransferTransaction,
mining_receiver: &str, mining_receiver: &str,
db: &Db, db: &Db,
) { ) {
// reverse the transfer and fee movements, then remove the // reverse the transfer and fee movements, then remove the
// transfer transaction from chain state during rollback // transfer transaction from chain state during 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();
let (coin, nft_series, receiver, sender, value, txfee) = ( let (coin, nft_series, receiver, sender, value, txfee) = (
&transaction.unsigned_transfer.coin, &transaction.unsigned_transfer.coin,
&transaction.unsigned_transfer.nft_series, &transaction.unsigned_transfer.nft_series,
&transaction.unsigned_transfer.receiver, &transaction.unsigned_transfer.receiver,
&transaction.unsigned_transfer.sender, &transaction.unsigned_transfer.sender,
&transaction.unsigned_transfer.value, &transaction.unsigned_transfer.value,
&transaction.unsigned_transfer.txfee, &transaction.unsigned_transfer.txfee,
); );
let transfer_asset = nft_asset_name(coin, *nft_series); let transfer_asset = nft_asset_name(coin, *nft_series);
// Remove the miner fee, return the transferred asset to the sender, and // Remove the miner fee, return the transferred asset to the sender, and
// refund the sender's base-coin fee. // refund the sender's base-coin fee.
let _ = balance_sheet_operation_with_db( let _ = balance_sheet_operation_with_db(
db, db,
mining_receiver, mining_receiver,
*txfee, *txfee,
&type_str, &type_str,
operand_subtraction, operand_subtraction,
); );
let _ = let _ =
balance_sheet_operation_with_db(db, receiver, *value, &transfer_asset, operand_subtraction); balance_sheet_operation_with_db(db, receiver, *value, &transfer_asset, operand_subtraction);
let _ = balance_sheet_operation_with_db(db, sender, *value, &transfer_asset, operand_addition); let _ = balance_sheet_operation_with_db(db, sender, *value, &transfer_asset, operand_addition);
let _ = balance_sheet_operation_with_db(db, sender, *txfee, &type_str, operand_addition); let _ = balance_sheet_operation_with_db(db, sender, *txfee, &type_str, operand_addition);
let hash = decode(&transaction.unsigned_transfer.hash().await).unwrap(); let hash = decode(&transaction.unsigned_transfer.hash().await).unwrap();
// Remove the txid lookup so the rolled-back transfer no longer resolves as // Remove the txid lookup so the rolled-back transfer no longer resolves as
// an on-chain transaction. // an on-chain transaction.
let tree = db.open_tree("txid").unwrap(); let tree = db.open_tree("txid").unwrap();
let key = hash.clone(); let key = hash.clone();
tree.remove(key).unwrap(); tree.remove(key).unwrap();
// NFT transfers also write provenance, so remove this transfer from the // NFT transfers also write provenance, so remove this transfer from the
// asset history if the transferred asset is an NFT. // asset history if the transferred asset is an NFT.
let nft_tree = db.open_tree("nfts").unwrap(); let nft_tree = db.open_tree("nfts").unwrap();
if nft_tree if nft_tree
.contains_key(transfer_asset.as_bytes()) .contains_key(transfer_asset.as_bytes())
.unwrap_or(false) .unwrap_or(false)
{ {
let _ = remove_nft_history_entry(db, &transfer_asset, &hash); let _ = remove_nft_history_entry(db, &transfer_asset, &hash);
} }
} }

View File

@ -46,11 +46,7 @@ pub async fn get_balance_with_db(
// Balance queries accept vanity/registered aliases, but storage always // Balance queries accept vanity/registered aliases, but storage always
// resolves back to the canonical short address. // resolves back to the canonical short address.
let canonical_address = resolve_canonical_registered_short_address(db, address) let canonical_address = resolve_canonical_registered_short_address(db, address)
.map_err(|err| { .map_err(|err| io::Error::other(format!("Wallet registry lookup failed: {err}")))?
io::Error::other(
format!("Wallet registry lookup failed: {err}"),
)
})?
.unwrap_or_else(|| address.to_string()); .unwrap_or_else(|| address.to_string());
get_balance(&canonical_address, coin_type).await get_balance(&canonical_address, coin_type).await

View File

@ -69,9 +69,7 @@ pub fn balance_sheet_operation(
let mut file_balance = if file_exists { let mut file_balance = if file_exists {
// Existing balances are stored as a single little-endian u64. // Existing balances are stored as a single little-endian u64.
file.read_exact(&mut buffer).map_err(|e| { file.read_exact(&mut buffer).map_err(|e| {
eprintln!( eprintln!("Error reading file balance_sheet address {address}: {e}");
"Error reading file balance_sheet address {address}: {e}"
);
e e
})?; })?;
u64::from_le_bytes(buffer) u64::from_le_bytes(buffer)
@ -142,11 +140,7 @@ pub fn balance_sheet_operation_with_db(
// Vanity or alternate registered addresses resolve to the canonical short // Vanity or alternate registered addresses resolve to the canonical short
// address before the filesystem balance is updated. // address before the filesystem balance is updated.
let canonical_address = resolve_canonical_registered_short_address(db, address) let canonical_address = resolve_canonical_registered_short_address(db, address)
.map_err(|err| { .map_err(|err| io::Error::other(format!("Wallet registry lookup failed: {err}")))?
io::Error::other(
format!("Wallet registry lookup failed: {err}"),
)
})?
.unwrap_or_else(|| address.to_string()); .unwrap_or_else(|| address.to_string());
balance_sheet_operation(&canonical_address, balance, coin_type, operand) balance_sheet_operation(&canonical_address, balance, coin_type, operand)

View File

@ -1,101 +1,97 @@
use crate::common::binary_conversions::{binary_to_ip, ip_to_binary}; use crate::common::binary_conversions::{binary_to_ip, ip_to_binary};
use crate::lazy_static; use crate::lazy_static;
use crate::log::{info, warn}; use crate::log::{info, warn};
use crate::records::memory::enums::{ClientType, ConnectionType}; use crate::records::memory::enums::{ClientType, ConnectionType};
use crate::records::memory::response_channels::{delete_entry, reserve_entry, Command}; use crate::records::memory::response_channels::{delete_entry, reserve_entry, Command};
use crate::records::memory::structs::{Connection, StoreConnectionParams}; use crate::records::memory::structs::{Connection, StoreConnectionParams};
use crate::rpc::client::handshake_processing::{bootstrap_peer_discovery, BootstrapParams}; use crate::rpc::client::handshake_processing::{bootstrap_peer_discovery, BootstrapParams};
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::sled::Db; use crate::sled::Db;
use crate::sleep; use crate::sleep;
use crate::thread_rng; use crate::thread_rng;
use crate::timeout; use crate::timeout;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::Arc; use crate::Arc;
use crate::AsyncWriteExt; use crate::AsyncWriteExt;
use crate::AtomicBool; use crate::AtomicBool;
use crate::AtomicOrdering; use crate::AtomicOrdering;
use crate::Duration; use crate::Duration;
use crate::IteratorRandom; use crate::IteratorRandom;
use crate::Mutex; use crate::Mutex;
use crate::RwLock; use crate::RwLock;
use crate::TcpStream; use crate::TcpStream;
fn split_ip_port_key(value: &str) -> Option<(String, u16)> { fn split_ip_port_key(value: &str) -> Option<(String, u16)> {
// Connection keys are stored as ip:port strings; IPv6 addresses may arrive // Connection keys are stored as ip:port strings; IPv6 addresses may arrive
// bracketed, so strip brackets before parsing the port. // bracketed, so strip brackets before parsing the port.
let (ip_part, port_part) = value.rsplit_once(':')?; let (ip_part, port_part) = value.rsplit_once(':')?;
let ip = ip_part let ip = ip_part
.strip_prefix('[') .strip_prefix('[')
.and_then(|inner| inner.strip_suffix(']')) .and_then(|inner| inner.strip_suffix(']'))
.unwrap_or(ip_part) .unwrap_or(ip_part)
.to_string(); .to_string();
let port = port_part.parse::<u16>().ok()?; let port = port_part.parse::<u16>().ok()?;
Some((ip, port)) Some((ip, port))
} }
use crate::records::memory::structs::{ConnectionInfo, ConnectionKey}; use crate::records::memory::structs::{ConnectionInfo, ConnectionKey};
#[derive(Clone)] #[derive(Clone)]
struct ReconnectContext { struct ReconnectContext {
db: Db, db: Db,
wallet_key: String, wallet_key: String,
map: Arc<Mutex<Command>>, map: Arc<Mutex<Command>>,
} }
lazy_static! { lazy_static! {
static ref RECONNECT_CONTEXT: Mutex<Option<ReconnectContext>> = Mutex::new(None); static ref RECONNECT_CONTEXT: Mutex<Option<ReconnectContext>> = Mutex::new(None);
static ref RECONNECT_IN_PROGRESS: AtomicBool = AtomicBool::new(false); static ref RECONNECT_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
} }
fn try_start_reconnect() -> bool { fn try_start_reconnect() -> bool {
// Only one reconnect path should run at a time, whether it came from // Only one reconnect path should run at a time, whether it came from
// liveness failure or bootstrap recovery. // liveness failure or bootstrap recovery.
RECONNECT_IN_PROGRESS RECONNECT_IN_PROGRESS
.compare_exchange(false, true, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst) .compare_exchange(false, true, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst)
.is_ok() .is_ok()
} }
fn finish_reconnect() { fn finish_reconnect() {
// Release the reconnect gate after the async reconnect attempt finishes. // Release the reconnect gate after the async reconnect attempt finishes.
RECONNECT_IN_PROGRESS.store(false, AtomicOrdering::SeqCst); RECONNECT_IN_PROGRESS.store(false, AtomicOrdering::SeqCst);
} }
pub async fn set_reconnect_context( pub async fn set_reconnect_context(db: Db, wallet_key: String, map: Arc<Mutex<Command>>) {
db: Db, let mut context = RECONNECT_CONTEXT.lock().await;
wallet_key: String, // Store enough state for later liveness checks to reconnect without
map: Arc<Mutex<Command>>, // needing the original startup stack.
) { *context = Some(ReconnectContext {
let mut context = RECONNECT_CONTEXT.lock().await; db,
// Store enough state for later liveness checks to reconnect without wallet_key,
// needing the original startup stack. map,
*context = Some(ReconnectContext { });
db, }
wallet_key,
map, async fn reconnect_dropped_outgoing(excluded_ip: &str) {
}); if !try_start_reconnect() {
} warn!("[reconnect] replacement attempt already in progress, skipping duplicate request");
return;
async fn reconnect_dropped_outgoing(excluded_ip: &str) { }
if !try_start_reconnect() {
warn!("[reconnect] replacement attempt already in progress, skipping duplicate request"); async {
return; // When an outgoing peer disappears, try to replace it with another
} // active node that is not already connected and is not the failed IP.
let context = {
async { let guard = RECONNECT_CONTEXT.lock().await;
// When an outgoing peer disappears, try to replace it with another guard.clone()
// active node that is not already connected and is not the failed IP. };
let context = {
let guard = RECONNECT_CONTEXT.lock().await; let Some(context) = context else {
guard.clone() warn!("[reconnect] no reconnect context configured");
}; return;
};
let Some(context) = context else {
warn!("[reconnect] no reconnect context configured");
return;
};
let excluded_ip_bytes = ip_to_binary(excluded_ip); let excluded_ip_bytes = ip_to_binary(excluded_ip);
let live_connection = { let live_connection = {
let guard = CONNECTIONS.read().await; let guard = CONNECTIONS.read().await;
@ -131,36 +127,36 @@ async fn reconnect_dropped_outgoing(excluded_ip: &str) {
if let Err(err) = bootstrap_peer_discovery(bootstrap_params).await { if let Err(err) = bootstrap_peer_discovery(bootstrap_params).await {
warn!("[reconnect] bootstrap recovery failed: {err}"); warn!("[reconnect] bootstrap recovery failed: {err}");
} }
} }
.await; .await;
finish_reconnect(); finish_reconnect();
} }
pub fn spawn_reconnect_bootstrap(params: BootstrapParams) { pub fn spawn_reconnect_bootstrap(params: BootstrapParams) {
if !try_start_reconnect() { if !try_start_reconnect() {
warn!("[reconnect] bootstrap recovery already in progress, skipping duplicate request"); warn!("[reconnect] bootstrap recovery already in progress, skipping duplicate request");
return; return;
} }
// Bootstrap discovery can perform network requests, so it runs detached // Bootstrap discovery can perform network requests, so it runs detached
// from the caller that noticed the connection problem. // from the caller that noticed the connection problem.
tokio::spawn(async move { tokio::spawn(async move {
if let Err(err) = bootstrap_peer_discovery(params).await { if let Err(err) = bootstrap_peer_discovery(params).await {
warn!("[reconnect] bootstrap recovery failed: {err}"); warn!("[reconnect] bootstrap recovery failed: {err}");
} }
finish_reconnect(); finish_reconnect();
}); });
} }
impl Connection { impl Connection {
// Initialize the in-memory connection manager state. // Initialize the in-memory connection manager state.
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
// Store a live socket in memory along with its role, peer identity, // Store a live socket in memory along with its role, peer identity,
// and session metadata used by the RPC and peer-management paths. // and session metadata used by the RPC and peer-management paths.
pub fn store_connection(&mut self, params: StoreConnectionParams) -> bool { pub fn store_connection(&mut self, params: StoreConnectionParams) -> bool {
let StoreConnectionParams { let StoreConnectionParams {
connection_type, connection_type,
@ -172,101 +168,101 @@ impl Connection {
command_map, command_map,
} = params; } = params;
let ip_bytes = ip_to_binary(&ip); let ip_bytes = ip_to_binary(&ip);
let connection_key = ConnectionKey { let connection_key = ConnectionKey {
connection_type: connection_type.as_bytes(), connection_type: connection_type.as_bytes(),
ip: ip_bytes.clone(), ip: ip_bytes.clone(),
port, port,
}; };
let connection_key2 = ConnectionKey { let connection_key2 = ConnectionKey {
connection_type: connection_type.opposite().as_bytes(), connection_type: connection_type.opposite().as_bytes(),
ip: ip_bytes.clone(), ip: ip_bytes.clone(),
port, port,
}; };
// Miner nodes are identified by IP, not by port. A second node // Miner nodes are identified by IP, not by port. A second node
// announcing the same IP is rejected even if it uses another // announcing the same IP is rejected even if it uses another
// socket port. // socket port.
if client_type == ClientType::Miner if client_type == ClientType::Miner
&& self.connection_map.iter().any(|(key, info)| { && self.connection_map.iter().any(|(key, info)| {
key.ip == ip_bytes key.ip == ip_bytes
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
}) })
{ {
return false; return false;
} }
// Non-miner RPC clients still use the full socket key so short // Non-miner RPC clients still use the full socket key so short
// request/response connections do not collide unnecessarily. // request/response connections do not collide unnecessarily.
if self.connection_map.contains_key(&connection_key) if self.connection_map.contains_key(&connection_key)
|| self.connection_map.contains_key(&connection_key2) || self.connection_map.contains_key(&connection_key2)
{ {
return false; return false;
} }
let address = Wallet::long_address_to_bytes(wallet_address); let address = Wallet::long_address_to_bytes(wallet_address);
if address.len() != Wallet::ADDRESS_BYTES_LENGTH { if address.len() != Wallet::ADDRESS_BYTES_LENGTH {
return false; return false;
} }
let connection_info = ConnectionInfo::new( let connection_info = ConnectionInfo::new(
connection_type.as_bytes(), connection_type.as_bytes(),
ip_bytes, ip_bytes,
port, port,
stream.clone(), stream.clone(),
client_type.as_bytes(), client_type.as_bytes(),
address, address,
); );
self.connection_map.insert(connection_key, connection_info); self.connection_map.insert(connection_key, connection_info);
if client_type == ClientType::Miner { if client_type == ClientType::Miner {
Connection::client_checkup(stream, connection_type, ip, port, command_map); Connection::client_checkup(stream, connection_type, ip, port, command_map);
} }
true true
} }
// Remove a specific connection entry by direction, IP, and port. // Remove a specific connection entry by direction, IP, and port.
pub fn drop_connection( pub fn drop_connection(
&mut self, &mut self,
connection_type: ConnectionType, connection_type: ConnectionType,
ip: String, ip: String,
port: u16, port: u16,
) -> Option<ConnectionInfo> { ) -> Option<ConnectionInfo> {
let ip_bytes = ip_to_binary(&ip); let ip_bytes = ip_to_binary(&ip);
let connection_key = ConnectionKey { let connection_key = ConnectionKey {
connection_type: connection_type.as_bytes(), connection_type: connection_type.as_bytes(),
ip: ip_bytes, ip: ip_bytes,
port, port,
}; };
let removed = self.connection_map.remove(&connection_key); let removed = self.connection_map.remove(&connection_key);
if let Some(connection_info) = removed.as_ref() { if let Some(connection_info) = removed.as_ref() {
let stream = Arc::clone(&connection_info.stream); let stream = Arc::clone(&connection_info.stream);
tokio::spawn(async move { tokio::spawn(async move {
let mut stream_guard = stream.lock().await; let mut stream_guard = stream.lock().await;
let _ = stream_guard.shutdown().await; let _ = stream_guard.shutdown().await;
}); });
} }
if let Some(connection_info) = removed.as_ref() { if let Some(connection_info) = removed.as_ref() {
let client_role = ClientType::from_bytes(&connection_info.client_type) let client_role = ClientType::from_bytes(&connection_info.client_type)
.map(|client_type| client_type.as_str()) .map(|client_type| client_type.as_str())
.unwrap_or("unknown"); .unwrap_or("unknown");
info!( info!(
"[connection_manager] connection dropped: role={} direction={} peer={}:{}", "[connection_manager] connection dropped: role={} direction={} peer={}:{}",
client_role, client_role,
connection_type.as_str(), connection_type.as_str(),
ip, ip,
port port
); );
} }
removed removed
} }
pub fn client_checkup( pub fn client_checkup(
stream: Arc<Mutex<TcpStream>>, stream: Arc<Mutex<TcpStream>>,
connection_type: ConnectionType, connection_type: ConnectionType,
ip: String, ip: String,
port: u16, port: u16,
command_map: Arc<Mutex<Command>>, command_map: Arc<Mutex<Command>>,
) { ) {
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
@ -274,39 +270,39 @@ impl Connection {
let still_registered = { let still_registered = {
let guard = CONNECTIONS.read().await; let guard = CONNECTIONS.read().await;
guard guard
.as_ref() .as_ref()
.map(|conn| { .map(|conn| {
let connection_key = ConnectionKey { let connection_key = ConnectionKey {
connection_type: connection_type.as_bytes(), connection_type: connection_type.as_bytes(),
ip: ip_to_binary(&ip), ip: ip_to_binary(&ip),
port, port,
}; };
conn.connection_map.contains_key(&connection_key) conn.connection_map.contains_key(&connection_key)
}) })
.unwrap_or(false) .unwrap_or(false)
}; };
if !still_registered { if !still_registered {
break; break;
} }
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(command_map.clone()).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.
let mut message: Vec<u8> = Vec::with_capacity(4); let mut message: Vec<u8> = Vec::with_capacity(4);
message.push(message_type); message.push(message_type);
message.extend_from_slice(&checkup_key); message.extend_from_slice(&checkup_key);
RpcResponse::send_raw(&stream, None, &message).await; RpcResponse::send_raw(&stream, None, &message).await;
let response_result = { let response_result = {
let mut checkup_rx = checkup_rx_mutex.lock().await; let mut checkup_rx = checkup_rx_mutex.lock().await;
timeout(Duration::from_secs(30), checkup_rx.recv()).await timeout(Duration::from_secs(30), checkup_rx.recv()).await
}; };
match response_result { match response_result {
Ok(Some(_reply)) => { Ok(Some(_reply)) => {
info!( info!(
@ -314,26 +310,26 @@ impl Connection {
connection_type.as_str(), connection_type.as_str(),
ip, ip,
port port
); );
} }
_ => { _ => {
let still_registered = { let still_registered = {
let guard = CONNECTIONS.read().await; let guard = CONNECTIONS.read().await;
guard guard
.as_ref() .as_ref()
.map(|conn| { .map(|conn| {
let connection_key = ConnectionKey { let connection_key = ConnectionKey {
connection_type: connection_type.as_bytes(), connection_type: connection_type.as_bytes(),
ip: ip_to_binary(&ip), ip: ip_to_binary(&ip),
port, port,
}; };
conn.connection_map.contains_key(&connection_key) conn.connection_map.contains_key(&connection_key)
}) })
.unwrap_or(false) .unwrap_or(false)
}; };
if !still_registered { if !still_registered {
delete_entry(command_map.clone(), checkup_key).await; delete_entry(command_map.clone(), checkup_key).await;
break; break;
} }
@ -350,77 +346,75 @@ impl Connection {
if let Some(conn) = guard.as_mut() { if let Some(conn) = guard.as_mut() {
conn.drop_connection(connection_type, ip.clone(), port); conn.drop_connection(connection_type, ip.clone(), port);
} }
drop(guard); drop(guard);
if connection_type == ConnectionType::Outgoing { if connection_type == ConnectionType::Outgoing {
reconnect_dropped_outgoing(&ip).await; reconnect_dropped_outgoing(&ip).await;
} }
break; break;
} }
} }
} }
}); });
} }
// Count active incoming peer connections. // Count active incoming peer connections.
pub fn count_incoming_connections(&self) -> usize { pub fn count_incoming_connections(&self) -> usize {
self.connection_map self.connection_map
.values() .values()
.filter(|info| { .filter(|info| {
ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Incoming) ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Incoming)
}) })
.count() .count()
} }
// Count active outgoing peer connections. // Count active outgoing peer connections.
pub fn count_outgoing_connections(&self) -> usize { pub fn count_outgoing_connections(&self) -> usize {
self.connection_map self.connection_map
.values() .values()
.filter(|info| { .filter(|info| {
ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Outgoing) ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Outgoing)
}) })
.count() .count()
} }
// Return all live peer streams so broadcast-style paths can fan out // Return all live peer streams so broadcast-style paths can fan out
// messages without caring whether a peer was incoming or outgoing. // messages without caring whether a peer was incoming or outgoing.
pub fn get_all_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> { pub fn get_all_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> {
self.connection_map self.connection_map
.values() .values()
.filter(|connection_info| { .filter(|connection_info| {
ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner) ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner)
}) })
.map(|connection_info| Arc::clone(&connection_info.stream)) .map(|connection_info| Arc::clone(&connection_info.stream))
.collect() .collect()
} }
// Return all non-client peer streams so network-wide broadcasts can // Return all non-client peer streams so network-wide broadcasts can
// reach every reachable chain peer. // reach every reachable chain peer.
pub fn get_all_peer_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> { pub fn get_all_peer_streams(&self) -> Vec<Arc<Mutex<TcpStream>>> {
self.connection_map self.connection_map
.values() .values()
.filter(|connection_info| { .filter(|connection_info| {
ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner) ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner)
}) })
.map(|connection_info| Arc::clone(&connection_info.stream)) .map(|connection_info| Arc::clone(&connection_info.stream))
.collect() .collect()
} }
// Resolve a stored outgoing node connection back to its live stream. // Resolve a stored outgoing node connection back to its live stream.
pub fn get_stream_for_outgoing(&self, ip: &str, port: u16) -> Option<Arc<Mutex<TcpStream>>> { pub fn get_stream_for_outgoing(&self, ip: &str, port: u16) -> Option<Arc<Mutex<TcpStream>>> {
let ip_bytes = ip_to_binary(ip); let ip_bytes = ip_to_binary(ip);
let connection_key = ConnectionKey { let connection_key = ConnectionKey {
connection_type: ConnectionType::Outgoing.as_bytes(), connection_type: ConnectionType::Outgoing.as_bytes(),
ip: ip_bytes, ip: ip_bytes,
port, port,
}; };
self.connection_map self.connection_map
.get(&connection_key) .get(&connection_key)
.filter(|info| { .filter(|info| ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner))
ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) .map(|info| Arc::clone(&info.stream))
}) }
.map(|info| Arc::clone(&info.stream))
}
// Look up a live miner stream by the exact ip:port connection key. // Look up a live miner stream by the exact ip:port connection key.
// Network-map records only store bare IPs, so they must not be used // Network-map records only store bare IPs, so they must not be used
// to select an arbitrary live socket. // to select an arbitrary live socket.
@ -430,16 +424,18 @@ impl Connection {
let conn = lock.as_ref()?; let conn = lock.as_ref()?;
let ip_bytes = ip_to_binary(&ip); let ip_bytes = ip_to_binary(&ip);
conn.connection_map.iter().find_map(|(connection_key, info)| { conn.connection_map
if connection_key.ip == ip_bytes .iter()
&& connection_key.port == port .find_map(|(connection_key, info)| {
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) if connection_key.ip == ip_bytes
{ && connection_key.port == port
Some(Arc::clone(&info.stream)) && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
} else { {
None Some(Arc::clone(&info.stream))
} } else {
}) None
}
})
} }
// Build the serialized connection key for a live stream when only // Build the serialized connection key for a live stream when only
@ -456,101 +452,101 @@ impl Connection {
} }
}) })
} }
// Find the first stored connection record for the requested IP. // Find the first stored connection record for the requested IP.
pub fn find_connection_info(&self, ip: &str) -> Option<(ConnectionType, u16)> { pub fn find_connection_info(&self, ip: &str) -> Option<(ConnectionType, u16)> {
let ip_bytes = ip_to_binary(ip); let ip_bytes = ip_to_binary(ip);
for (key, _info) in self.connection_map.iter() { for (key, _info) in self.connection_map.iter() {
if key.ip == ip_bytes { if key.ip == ip_bytes {
let connection_type = ConnectionType::from_bytes(&key.connection_type)?; let connection_type = ConnectionType::from_bytes(&key.connection_type)?;
return Some((connection_type, key.port)); return Some((connection_type, key.port));
} }
} }
None None
} }
// Find a stored connection by IP, constrained to a specific client role. // Find a stored connection by IP, constrained to a specific client role.
pub fn find_connection_info_by_client_type( pub fn find_connection_info_by_client_type(
&self, &self,
ip: &str, ip: &str,
client_type: ClientType, client_type: ClientType,
) -> Option<(ConnectionType, u16)> { ) -> Option<(ConnectionType, u16)> {
let ip_bytes = ip_to_binary(ip); let ip_bytes = ip_to_binary(ip);
let client_type_bytes = client_type.as_bytes(); let client_type_bytes = client_type.as_bytes();
for (key, info) in self.connection_map.iter() { for (key, info) in self.connection_map.iter() {
if key.ip == ip_bytes && info.client_type == client_type_bytes { if key.ip == ip_bytes && info.client_type == client_type_bytes {
let connection_type = ConnectionType::from_bytes(&key.connection_type)?; let connection_type = ConnectionType::from_bytes(&key.connection_type)?;
return Some((connection_type, key.port)); return Some((connection_type, key.port));
} }
} }
None None
} }
// Find the stored outgoing port for a peer IP so reconnect and // Find the stored outgoing port for a peer IP so reconnect and
// cleanup logic can target the correct connection entry. // cleanup logic can target the correct connection entry.
pub fn find_outgoing_port(&self, ip: &str) -> Option<u16> { pub fn find_outgoing_port(&self, ip: &str) -> Option<u16> {
let ip_bytes = ip_to_binary(ip); let ip_bytes = ip_to_binary(ip);
self.connection_map self.connection_map
.iter() .iter()
.find(|(key, _)| { .find(|(key, _)| {
key.connection_type == ConnectionType::Outgoing.as_bytes() && key.ip == ip_bytes key.connection_type == ConnectionType::Outgoing.as_bytes() && key.ip == ip_bytes
}) })
.map(|(key, _)| key.port) .map(|(key, _)| key.port)
} }
// Prefer a random incoming node connection, falling back to an // Prefer a random incoming node connection, falling back to an
// outgoing node connection when no incoming peer is available. // outgoing node connection when no incoming peer is available.
pub fn get_random_connection(&self, excluded_key: Option<&str>) -> Option<(Vec<u8>, u16)> { pub fn get_random_connection(&self, excluded_key: Option<&str>) -> Option<(Vec<u8>, u16)> {
let mut rng = thread_rng(); let mut rng = thread_rng();
let excluded = excluded_key.and_then(split_ip_port_key); let excluded = excluded_key.and_then(split_ip_port_key);
if let Some((key, _info)) = self if let Some((key, _info)) = self
.connection_map .connection_map
.iter() .iter()
.filter(|(key, info)| { .filter(|(key, info)| {
ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Incoming) ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Incoming)
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
&& excluded && excluded
.as_ref() .as_ref()
.map(|(ip, _)| key.ip != ip_to_binary(ip)) .map(|(ip, _)| key.ip != ip_to_binary(ip))
.unwrap_or(true) .unwrap_or(true)
}) })
.choose(&mut rng) .choose(&mut rng)
{ {
return Some((key.ip.clone(), key.port)); return Some((key.ip.clone(), key.port));
} }
if let Some((key, _info)) = self if let Some((key, _info)) = self
.connection_map .connection_map
.iter() .iter()
.filter(|(key, info)| { .filter(|(key, info)| {
ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Outgoing) ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Outgoing)
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
&& excluded && excluded
.as_ref() .as_ref()
.map(|(ip, _)| key.ip != ip_to_binary(ip)) .map(|(ip, _)| key.ip != ip_to_binary(ip))
.unwrap_or(true) .unwrap_or(true)
}) })
.choose(&mut rng) .choose(&mut rng)
{ {
return Some((key.ip.clone(), key.port)); return Some((key.ip.clone(), key.port));
} }
None None
} }
} }
lazy_static! { lazy_static! {
pub static ref CONNECTIONS: Arc<RwLock<Option<Connection>>> = Arc::new(RwLock::new(None)); pub static ref CONNECTIONS: Arc<RwLock<Option<Connection>>> = Arc::new(RwLock::new(None));
} }
pub async fn initialize_connection() { pub async fn initialize_connection() {
// Lazily create the singleton connection manager the first time the // Lazily create the singleton connection manager the first time the
// node starts accepting or opening peer connections. // node starts accepting or opening peer connections.
let mut connection_instance = CONNECTIONS.write().await; let mut connection_instance = CONNECTIONS.write().await;
if connection_instance.is_none() { if connection_instance.is_none() {
*connection_instance = Some(Connection::new()); *connection_instance = Some(Connection::new());
} }
} }
@ -565,19 +561,19 @@ pub async fn outgoing_connection_count() -> usize {
} }
pub async fn get_client_type_from_memory(key: &str) -> Option<ClientType> { pub async fn get_client_type_from_memory(key: &str) -> Option<ClientType> {
// Recover the stored client role from the serialized connection key // Recover the stored client role from the serialized connection key
// used throughout the RPC layer. // used throughout the RPC layer.
let (ip, port) = split_ip_port_key(key)?; let (ip, port) = split_ip_port_key(key)?;
let ip_bytes = ip_to_binary(&ip); let ip_bytes = ip_to_binary(&ip);
let guard = CONNECTIONS.read().await; let guard = CONNECTIONS.read().await;
let conn = guard.as_ref()?; let conn = guard.as_ref()?;
for (connection_key, info) in conn.connection_map.iter() { for (connection_key, info) in conn.connection_map.iter() {
if connection_key.ip == ip_bytes && connection_key.port == port { if connection_key.ip == ip_bytes && connection_key.port == port {
return ClientType::from_bytes(&info.client_type); return ClientType::from_bytes(&info.client_type);
} }
} }
None None
} }

View File

@ -1,89 +1,89 @@
use super::*; use super::*;
pub async fn signature_exists(signature: &str, hash: &str) -> Result<bool> { pub async fn signature_exists(signature: &str, hash: &str) -> Result<bool> {
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
// Check every mempool table because the signature column names differ by // Check every mempool table because the signature column names differ by
// transaction type, especially for two-party swaps and loans. // transaction type, especially for two-party swaps and loans.
let row = client let row = client
.query_one( .query_one(
r#" r#"
SELECT SELECT
CASE CASE
WHEN EXISTS (SELECT 1 FROM transfer a WHERE a.signature = $1 AND a.hash = $2 AND a.processed = false) WHEN EXISTS (SELECT 1 FROM transfer a WHERE a.signature = $1 AND a.hash = $2 AND a.processed = false)
OR EXISTS (SELECT 1 FROM token b WHERE b.signature = $1 AND b.hash = $2 AND b.processed = false) OR EXISTS (SELECT 1 FROM token b WHERE b.signature = $1 AND b.hash = $2 AND b.processed = false)
OR EXISTS (SELECT 1 FROM issue_token c WHERE c.signature = $1 AND c.hash = $2 AND c.processed = false) OR EXISTS (SELECT 1 FROM issue_token c WHERE c.signature = $1 AND c.hash = $2 AND c.processed = false)
OR EXISTS (SELECT 1 FROM burn d WHERE d.signature = $1 AND d.hash = $2 AND d.processed = false) OR EXISTS (SELECT 1 FROM burn d WHERE d.signature = $1 AND d.hash = $2 AND d.processed = false)
OR EXISTS (SELECT 1 FROM nft e WHERE e.signature = $1 AND e.hash = $2 AND e.processed = false) OR EXISTS (SELECT 1 FROM nft e WHERE e.signature = $1 AND e.hash = $2 AND e.processed = false)
OR EXISTS (SELECT 1 FROM marketing f WHERE f.signature = $1 AND f.hash = $2 AND f.processed = false) OR EXISTS (SELECT 1 FROM marketing f WHERE f.signature = $1 AND f.hash = $2 AND f.processed = false)
OR EXISTS (SELECT 1 FROM vanity_address va WHERE va.signature = $1 AND va.hash = $2 AND va.processed = false) OR EXISTS (SELECT 1 FROM vanity_address va WHERE va.signature = $1 AND va.hash = $2 AND va.processed = false)
OR EXISTS (SELECT 1 FROM swap g WHERE g.signature1 = $1 AND g.hash = $2 AND g.processed = false) OR EXISTS (SELECT 1 FROM swap g WHERE g.signature1 = $1 AND g.hash = $2 AND g.processed = false)
OR EXISTS (SELECT 1 FROM swap h WHERE h.signature2 = $1 AND h.hash = $2 AND h.processed = false) OR EXISTS (SELECT 1 FROM swap h WHERE h.signature2 = $1 AND h.hash = $2 AND h.processed = false)
OR EXISTS (SELECT 1 FROM loan_contract i WHERE i.signature1 = $1 AND i.hash = $2 AND i.processed = false) OR EXISTS (SELECT 1 FROM loan_contract i WHERE i.signature1 = $1 AND i.hash = $2 AND i.processed = false)
OR EXISTS (SELECT 1 FROM loan_contract j WHERE j.signature2 = $1 AND j.hash = $2 AND j.processed = false) OR EXISTS (SELECT 1 FROM loan_contract j WHERE j.signature2 = $1 AND j.hash = $2 AND j.processed = false)
OR EXISTS (SELECT 1 FROM loan_payment k WHERE k.signature = $1 AND k.hash = $2 AND k.processed = false) OR EXISTS (SELECT 1 FROM loan_payment k WHERE k.signature = $1 AND k.hash = $2 AND k.processed = false)
OR EXISTS (SELECT 1 FROM collateral_claim l WHERE l.signature = $1 AND l.hash = $2 AND l.processed = false) OR EXISTS (SELECT 1 FROM collateral_claim l WHERE l.signature = $1 AND l.hash = $2 AND l.processed = false)
THEN 1 THEN 1
ELSE 0 ELSE 0
END AS signature_found; END AS signature_found;
"#, "#,
&[&signature, &hash], &[&signature, &hash],
) )
.await?; .await?;
let found: i32 = row.get(0); let found: i32 = row.get(0);
Ok(found == 1) Ok(found == 1)
} }
pub async fn transaction_by_signature(signature: &str) -> RpcResponse { pub async fn transaction_by_signature(signature: &str) -> RpcResponse {
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
// Return the original serialized transaction bytes, not a reconstructed // Return the original serialized transaction bytes, not a reconstructed
// row, so RPC callers receive the same payload that would enter a block. // row, so RPC callers receive the same payload that would enter a block.
let result = client let result = client
.query_opt( .query_opt(
r#" r#"
SELECT original FROM ( SELECT original FROM (
SELECT original FROM transfer WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM transfer WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM token WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM token WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM issue_token WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM issue_token WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM burn WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM burn WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM nft WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM nft WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM marketing WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM marketing WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM vanity_address WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM vanity_address WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM swap WHERE signature1 = $1 AND processed = false LIMIT 1 SELECT original FROM swap WHERE signature1 = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM swap WHERE signature2 = $1 AND processed = false LIMIT 1 SELECT original FROM swap WHERE signature2 = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM loan_contract WHERE signature1 = $1 AND processed = false LIMIT 1 SELECT original FROM loan_contract WHERE signature1 = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM loan_contract WHERE signature2 = $1 AND processed = false LIMIT 1 SELECT original FROM loan_contract WHERE signature2 = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM loan_payment WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM loan_payment WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM collateral_claim WHERE signature = $1 AND processed = false LIMIT 1 SELECT original FROM collateral_claim WHERE signature = $1 AND processed = false LIMIT 1
) AS subquery LIMIT 1 ) AS subquery LIMIT 1
"#, "#,
&[&signature], &[&signature],
) )
.await; .await;
match result { match result {
Ok(Some(row)) => { Ok(Some(row)) => {
let bytes: Vec<u8> = row.get(0); let bytes: Vec<u8> = row.get(0);
RpcResponse::Binary(bytes) RpcResponse::Binary(bytes)
} }
_ => RpcResponse::Binary(Vec::new()), _ => RpcResponse::Binary(Vec::new()),
} }
} }
pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse { pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse {
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
// Canonicalize vanity aliases before querying pending rows. // Canonicalize vanity aliases before querying pending rows.
@ -92,97 +92,97 @@ pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse {
// Concatenate original transaction bytes; the RPC/bin caller can split the // Concatenate original transaction bytes; the RPC/bin caller can split the
// stream by transaction type and fixed byte length. // stream by transaction type and fixed byte length.
let rows = match client let rows = match client
.query( .query(
r#" r#"
SELECT original FROM ( SELECT original FROM (
SELECT original FROM transfer WHERE receiver = ANY($1) AND processed = false SELECT original FROM transfer WHERE receiver = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM token WHERE creator = ANY($1) AND processed = false SELECT original FROM token WHERE creator = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM issue_token WHERE creator = ANY($1) AND processed = false SELECT original FROM issue_token WHERE creator = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM burn WHERE address = ANY($1) AND processed = false SELECT original FROM burn WHERE address = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM nft WHERE creator = ANY($1) AND processed = false SELECT original FROM nft WHERE creator = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM marketing WHERE advertiser = ANY($1) AND processed = false SELECT original FROM marketing WHERE advertiser = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM vanity_address WHERE address = ANY($1) AND processed = false SELECT original FROM vanity_address WHERE address = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM swap WHERE sender1 = ANY($1) AND processed = false SELECT original FROM swap WHERE sender1 = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM swap WHERE sender2 = ANY($1) AND processed = false SELECT original FROM swap WHERE sender2 = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM loan_contract WHERE lender = ANY($1) AND processed = false SELECT original FROM loan_contract WHERE lender = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM loan_contract WHERE borrower = ANY($1) AND processed = false SELECT original FROM loan_contract WHERE borrower = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM loan_payment WHERE address = ANY($1) AND processed = false SELECT original FROM loan_payment WHERE address = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM collateral_claim WHERE address = ANY($1) AND processed = false SELECT original FROM collateral_claim WHERE address = ANY($1) AND processed = false
) AS subquery; ) AS subquery;
"#, "#,
&[&addresses], &[&addresses],
) )
.await .await
{ {
Ok(r) => r, Ok(r) => r,
Err(_) => return RpcResponse::Binary(Vec::new()), Err(_) => return RpcResponse::Binary(Vec::new()),
}; };
let mut bytes = Vec::new(); let mut bytes = Vec::new();
for row in rows { for row in rows {
let chunk: Vec<u8> = row.get(0); let chunk: Vec<u8> = row.get(0);
bytes.extend(chunk); bytes.extend(chunk);
} }
RpcResponse::Binary(bytes) RpcResponse::Binary(bytes)
} }
pub async fn largest_fee() -> RpcResponse { pub async fn largest_fee() -> RpcResponse {
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
// Swaps have two possible fees, so both sides are included in the max. // Swaps have two possible fees, so both sides are included in the max.
let row = match client let row = match client
.query_one( .query_one(
r#" r#"
SELECT MAX(fee) AS largest_txid FROM ( SELECT MAX(fee) AS largest_txid FROM (
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM transfer SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM transfer
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM token SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM token
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM issue_token SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM issue_token
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM burn SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM burn
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM nft SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM nft
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM marketing SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM marketing
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM vanity_address SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM vanity_address
UNION ALL UNION ALL
SELECT CAST(MAX(fee1) AS BIGINT) AS fee FROM swap SELECT CAST(MAX(fee1) AS BIGINT) AS fee FROM swap
UNION ALL UNION ALL
SELECT CAST(MAX(fee2) AS BIGINT) AS fee FROM swap SELECT CAST(MAX(fee2) AS BIGINT) AS fee FROM swap
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_contract SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_contract
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_payment SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_payment
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM collateral_claim SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM collateral_claim
) AS combined_max_txids; ) AS combined_max_txids;
"#, "#,
&[], &[],
) )
.await .await
{ {
Ok(r) => r, Ok(r) => r,
Err(_) => return RpcResponse::Binary(0u32.to_le_bytes().to_vec()), Err(_) => return RpcResponse::Binary(0u32.to_le_bytes().to_vec()),
}; };
let max_fee: Option<i64> = row.get(0); let max_fee: Option<i64> = row.get(0);
let fee = (max_fee.unwrap_or(0) as u64).to_le_bytes().to_vec(); let fee = (max_fee.unwrap_or(0) as u64).to_le_bytes().to_vec();
RpcResponse::Binary(fee) RpcResponse::Binary(fee)
} }
@ -242,49 +242,49 @@ async fn pending_saved_loan_payment_balance(
pub async fn get_coin_balance( pub async fn get_coin_balance(
db: &Db, db: &Db,
address: &str, address: &str,
coin: &str, coin: &str,
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
// Pending-balance checks use canonical addresses so vanity and short // Pending-balance checks use canonical addresses so vanity and short
// address inputs see the same outgoing obligations. // address inputs see the same outgoing obligations.
let addresses = canonical_mempool_addresses(db, address); let addresses = canonical_mempool_addresses(db, address);
let (asset_name, nft_series) = nft_asset_parts(coin); let (asset_name, nft_series) = nft_asset_parts(coin);
let nft_series = nft_series as i32; let nft_series = nft_series as i32;
let row = client let row = client
.query_one( .query_one(
r#" r#"
SELECT CAST(( SELECT CAST((
COALESCE((SELECT SUM(t.value) COALESCE((SELECT SUM(t.value)
FROM transfer t FROM transfer t
WHERE t.sender = ANY($1) AND t.coin = $2 AND t.nft_series = $3 AND t.processed = false), 0) WHERE t.sender = ANY($1) AND t.coin = $2 AND t.nft_series = $3 AND t.processed = false), 0)
+ COALESCE((SELECT SUM(tok.number) + COALESCE((SELECT SUM(tok.number)
FROM token tok FROM token tok
WHERE tok.creator = ANY($1) AND tok.ticker = $2 AND tok.processed = false), 0) WHERE tok.creator = ANY($1) AND tok.ticker = $2 AND tok.processed = false), 0)
+ COALESCE((SELECT SUM(it.number) + COALESCE((SELECT SUM(it.number)
FROM issue_token it FROM issue_token it
WHERE it.creator = ANY($1) AND it.ticker = $2 AND it.processed = false), 0) WHERE it.creator = ANY($1) AND it.ticker = $2 AND it.processed = false), 0)
+ COALESCE((SELECT SUM(b.value) + COALESCE((SELECT SUM(b.value)
FROM burn b FROM burn b
WHERE b.address = ANY($1) AND b.coin = $2 AND b.nft_series = $3 AND b.processed = false), 0) WHERE b.address = ANY($1) AND b.coin = $2 AND b.nft_series = $3 AND b.processed = false), 0)
+ COALESCE((SELECT SUM(s.value1) + COALESCE((SELECT SUM(s.value1)
FROM swap s FROM swap s
WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0) WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0)
+ COALESCE((SELECT SUM(s.tip1) + COALESCE((SELECT SUM(s.tip1)
FROM swap s FROM swap s
WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0) WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0)
+ COALESCE((SELECT SUM(s.value2) + COALESCE((SELECT SUM(s.value2)
FROM swap s FROM swap s
WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0) WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0)
+ COALESCE((SELECT SUM(s.tip2) + COALESCE((SELECT SUM(s.tip2)
FROM swap s FROM swap s
WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0) WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0)
+ COALESCE((SELECT SUM(lc.loan_amount) + COALESCE((SELECT SUM(lc.loan_amount)
FROM loan_contract lc FROM loan_contract lc
WHERE lc.lender = ANY($1) AND lc.loan_coin = $2 AND lc.processed = false), 0) WHERE lc.lender = ANY($1) AND lc.loan_coin = $2 AND lc.processed = false), 0)
@ -292,25 +292,25 @@ pub async fn get_coin_balance(
+ COALESCE((SELECT SUM(lc.collateral_amount) + COALESCE((SELECT SUM(lc.collateral_amount)
FROM loan_contract lc FROM loan_contract lc
WHERE lc.borrower = ANY($1) AND lc.collateral = $2 AND lc.processed = false), 0) WHERE lc.borrower = ANY($1) AND lc.collateral = $2 AND lc.processed = false), 0)
+ COALESCE(( + COALESCE((
SELECT SUM(lp.payback_amount) SELECT SUM(lp.payback_amount)
FROM loan_payment lp FROM loan_payment lp
JOIN loan_contract lc ON lc.txid = lp.contract_hash JOIN loan_contract lc ON lc.txid = lp.contract_hash
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
), 0) ), 0)
+ COALESCE(( + COALESCE((
SELECT SUM(lp.tip) SELECT SUM(lp.tip)
FROM loan_payment lp FROM loan_payment lp
JOIN loan_contract lc ON lc.txid = lp.contract_hash JOIN loan_contract lc ON lc.txid = lp.contract_hash
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
), 0) ), 0)
) AS BIGINT) AS total ) AS BIGINT) AS total
"#, "#,
&[&addresses, &asset_name, &nft_series], &[&addresses, &asset_name, &nft_series],
) )
.await?; .await?;
// Negative projections are clamped because callers only need the amount // Negative projections are clamped because callers only need the amount
@ -319,10 +319,10 @@ pub async fn get_coin_balance(
let chain_loan_payments = pending_saved_loan_payment_balance(db, &addresses, coin).await?; let chain_loan_payments = pending_saved_loan_payment_balance(db, &addresses, coin).await?;
Ok((total.max(0) as u64).saturating_add(chain_loan_payments)) Ok((total.max(0) as u64).saturating_add(chain_loan_payments))
} }
pub async fn get_basecoin_balance( pub async fn get_basecoin_balance(
db: &Db, db: &Db,
address: &str, address: &str,
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
let addresses = canonical_mempool_addresses(db, address); let addresses = canonical_mempool_addresses(db, address);
@ -330,9 +330,9 @@ pub async fn get_basecoin_balance(
// Base coin projection includes direct base transfers plus all fees and // Base coin projection includes direct base transfers plus all fees and
// any pending loan/swap movements denominated in the base coin. // any pending loan/swap movements denominated in the base coin.
let row = client let row = client
.query_one( .query_one(
r#" r#"
SELECT CAST(( SELECT CAST((
COALESCE((SELECT SUM(t.value) COALESCE((SELECT SUM(t.value)
FROM transfer t FROM transfer t
WHERE t.sender = ANY($1) AND t.coin = $2 AND t.processed = false), 0) WHERE t.sender = ANY($1) AND t.coin = $2 AND t.processed = false), 0)
@ -341,50 +341,49 @@ pub async fn get_basecoin_balance(
FROM burn b FROM burn b
WHERE b.address = ANY($1) AND b.coin = $2 AND b.processed = false), 0) WHERE b.address = ANY($1) AND b.coin = $2 AND b.processed = false), 0)
+ COALESCE((SELECT SUM(x.fee) FROM token x WHERE x.creator = ANY($1) AND x.processed = false), 0) + COALESCE((SELECT SUM(x.fee) FROM token x WHERE x.creator = ANY($1) AND x.processed = false), 0)
+ COALESCE((SELECT SUM(it.fee) FROM issue_token it WHERE it.creator = ANY($1) AND it.processed = false), 0) + COALESCE((SELECT SUM(it.fee) FROM issue_token it WHERE it.creator = ANY($1) AND it.processed = false), 0)
+ COALESCE((SELECT SUM(b.fee) FROM burn b WHERE b.address = ANY($1) AND b.processed = false), 0) + COALESCE((SELECT SUM(b.fee) FROM burn b WHERE b.address = ANY($1) AND b.processed = false), 0)
+ COALESCE((SELECT SUM(n.fee) FROM nft n WHERE n.creator = ANY($1) AND n.processed = false), 0) + COALESCE((SELECT SUM(n.fee) FROM nft n WHERE n.creator = ANY($1) AND n.processed = false), 0)
+ COALESCE((SELECT SUM(m.fee) FROM marketing m WHERE m.advertiser = ANY($1) AND m.processed = false), 0) + COALESCE((SELECT SUM(m.fee) FROM marketing m WHERE m.advertiser = ANY($1) AND m.processed = false), 0)
+ COALESCE((SELECT SUM(v.fee) FROM vanity_address v WHERE v.address = ANY($1) AND v.processed = false), 0) + COALESCE((SELECT SUM(v.fee) FROM vanity_address v WHERE v.address = ANY($1) AND v.processed = false), 0)
+ COALESCE((SELECT SUM(cc.fee) FROM collateral_claim cc WHERE cc.address = ANY($1) AND cc.processed = false), 0) + COALESCE((SELECT SUM(cc.fee) FROM collateral_claim cc WHERE cc.address = ANY($1) AND cc.processed = false), 0)
+ COALESCE((SELECT SUM(lc.fee) FROM loan_contract lc WHERE lc.lender = ANY($1) AND lc.processed = false), 0) + COALESCE((SELECT SUM(lc.fee) FROM loan_contract lc WHERE lc.lender = ANY($1) AND lc.processed = false), 0)
+ COALESCE((SELECT SUM(lp.fee) FROM loan_payment lp WHERE lp.address = ANY($1) AND lp.processed = false), 0) + COALESCE((SELECT SUM(lp.fee) FROM loan_payment lp WHERE lp.address = ANY($1) AND lp.processed = false), 0)
+ COALESCE((SELECT SUM(lc.loan_amount) + COALESCE((SELECT SUM(lc.loan_amount)
FROM loan_contract lc FROM loan_contract lc
WHERE lc.lender = ANY($1) AND lc.loan_coin = $2 AND lc.processed = false), 0) WHERE lc.lender = ANY($1) AND lc.loan_coin = $2 AND lc.processed = false), 0)
+ COALESCE((SELECT SUM(lc.collateral_amount) + COALESCE((SELECT SUM(lc.collateral_amount)
FROM loan_contract lc FROM loan_contract lc
WHERE lc.borrower = ANY($1) AND lc.collateral = $2 AND lc.processed = false), 0) WHERE lc.borrower = ANY($1) AND lc.collateral = $2 AND lc.processed = false), 0)
+ COALESCE(( + COALESCE((
SELECT SUM(lp.payback_amount) SELECT SUM(lp.payback_amount)
FROM loan_payment lp FROM loan_payment lp
JOIN loan_contract lc ON lc.txid = lp.contract_hash JOIN loan_contract lc ON lc.txid = lp.contract_hash
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
), 0) ), 0)
+ COALESCE(( + COALESCE((
SELECT SUM(lp.tip) SELECT SUM(lp.tip)
FROM loan_payment lp FROM loan_payment lp
JOIN loan_contract lc ON lc.txid = lp.contract_hash JOIN loan_contract lc ON lc.txid = lp.contract_hash
WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false WHERE lp.address = ANY($1) AND lc.loan_coin = $2 AND lp.processed = false AND lc.processed = false
), 0) ), 0)
+ COALESCE((SELECT SUM(s.fee1) FROM swap s WHERE s.sender1 = ANY($1) AND s.processed = false), 0) + COALESCE((SELECT SUM(s.fee1) FROM swap s WHERE s.sender1 = ANY($1) AND s.processed = false), 0)
+ COALESCE((SELECT SUM(s.fee2) FROM swap s WHERE s.sender2 = ANY($1) AND s.processed = false), 0) + COALESCE((SELECT SUM(s.fee2) FROM swap s WHERE s.sender2 = ANY($1) AND s.processed = false), 0)
+ COALESCE((SELECT SUM(s.value1) FROM swap s WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.processed = false), 0) + COALESCE((SELECT SUM(s.value1) FROM swap s WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.processed = false), 0)
+ COALESCE((SELECT SUM(s.tip1) FROM swap s WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.processed = false), 0) + COALESCE((SELECT SUM(s.tip1) FROM swap s WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.processed = false), 0)
+ COALESCE((SELECT SUM(s.value2) FROM swap s WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.processed = false), 0) + COALESCE((SELECT SUM(s.value2) FROM swap s WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.processed = false), 0)
+ COALESCE((SELECT SUM(s.tip2) FROM swap s WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.processed = false), 0) + COALESCE((SELECT SUM(s.tip2) FROM swap s WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.processed = false), 0)
) AS BIGINT) AS total ) AS BIGINT) AS total
"#, "#,
&[&addresses, &*BASECOIN], &[&addresses, &*BASECOIN],
) )
.await?; .await?;
let total: i64 = row.get(0); let total: i64 = row.get(0);
let chain_loan_payments = let chain_loan_payments = pending_saved_loan_payment_balance(db, &addresses, &BASECOIN).await?;
pending_saved_loan_payment_balance(db, &addresses, &BASECOIN).await?;
Ok((total.max(0) as u64).saturating_add(chain_loan_payments)) Ok((total.max(0) as u64).saturating_add(chain_loan_payments))
} }
pub async fn get_pending_payments_for_contract( pub async fn get_pending_payments_for_contract(
contract_hash: &str, contract_hash: &str,
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
@ -393,63 +392,62 @@ pub async fn get_pending_payments_for_contract(
// Loan verification uses this to prevent pending payments from exceeding // Loan verification uses this to prevent pending payments from exceeding
// what the contract still owes. // what the contract still owes.
let row = client let row = client
.query_one( .query_one(
r#" r#"
SELECT CAST(COALESCE(SUM(lp.payback_amount), 0) AS BIGINT) AS total SELECT CAST(COALESCE(SUM(lp.payback_amount), 0) AS BIGINT) AS total
FROM loan_payment lp FROM loan_payment lp
WHERE lp.contract_hash = $1 WHERE lp.contract_hash = $1
AND lp.processed = false AND lp.processed = false
"#, "#,
&[&contract_hash], &[&contract_hash],
) )
.await?; .await?;
let total: i64 = row.get(0); let total: i64 = row.get(0);
Ok(total.max(0) as u64) Ok(total.max(0) as u64)
} }
pub async fn total_transactions() -> RpcResponse { pub async fn total_transactions() -> RpcResponse {
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
// Count rows across all mempool tables, including processed rows that may // Count rows across all mempool tables, including processed rows that may
// still be retained briefly for orphan rollback. // still be retained briefly for orphan rollback.
let row = match client let row = match client
.query_one( .query_one(
r#" r#"
SELECT CAST(SUM(row_count) AS BIGINT) AS total_rows FROM ( SELECT CAST(SUM(row_count) AS BIGINT) AS total_rows FROM (
SELECT COUNT(*) AS row_count FROM transfer SELECT COUNT(*) AS row_count FROM transfer
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM token SELECT COUNT(*) AS row_count FROM token
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM issue_token SELECT COUNT(*) AS row_count FROM issue_token
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM burn SELECT COUNT(*) AS row_count FROM burn
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM nft SELECT COUNT(*) AS row_count FROM nft
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM marketing SELECT COUNT(*) AS row_count FROM marketing
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM vanity_address SELECT COUNT(*) AS row_count FROM vanity_address
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM swap SELECT COUNT(*) AS row_count FROM swap
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM loan_contract SELECT COUNT(*) AS row_count FROM loan_contract
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM loan_payment SELECT COUNT(*) AS row_count FROM loan_payment
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM collateral_claim SELECT COUNT(*) AS row_count FROM collateral_claim
) AS combined; ) AS combined;
"#, "#,
&[], &[],
) )
.await .await
{ {
Ok(r) => r, Ok(r) => r,
Err(_) => return RpcResponse::Binary(vec![0; 8]), Err(_) => return RpcResponse::Binary(vec![0; 8]),
}; };
let total: Option<i64> = row.get(0); let total: Option<i64> = row.get(0);
let result = (total.unwrap_or(0) as u32).to_le_bytes().to_vec(); let result = (total.unwrap_or(0) as u32).to_le_bytes().to_vec();
RpcResponse::Binary(result) RpcResponse::Binary(result)
} }

View File

@ -1,330 +1,326 @@
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
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::nft_assets::{nft_asset_name, nft_asset_parts}; use crate::common::nft_assets::{nft_asset_name, nft_asset_parts};
use crate::config::SETTINGS; use crate::config::SETTINGS;
use crate::decode; use crate::decode;
use crate::lazy_static; use crate::lazy_static;
use crate::records::memory::structs::BalanceKey; use crate::records::memory::structs::BalanceKey;
use crate::records::wallet_registry::{ use crate::records::wallet_registry::resolve_canonical_registered_short_address;
resolve_canonical_registered_short_address, use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::HashMap;
use crate::NoTls;
use crate::{task, AtomicBool};
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use std::fs::File;
use std::io::Write;
use tokio_postgres::Client;
lazy_static! {
pub static ref BASECOIN: String = {
let (
_network_name,
base_coin,
_suffix,
_torrentpath,
_wallet_path,
_blockpath,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
format!("{base_coin:<15}")
};
static ref CLEANUP_RUNNING: AtomicBool = AtomicBool::new(false);
}
pub static DB: OnceCell<Client> = OnceCell::new();
pub const EPOCH_ROW_CAP: i64 = 100_000;
const NFT_UNIT: i64 = 100_000_000;
const CLEANUP_DEPTH: u32 = 10;
const CLEANUP_BATCH_LIMIT: i64 = 1000;
#[derive(Clone)]
enum SelectedMempoolTransaction {
// These variants hold the minimal fields needed to score, mark, and
// later apply selected mempool transactions into a saved block.
Transfer {
id: i64,
fee: i64,
sender: String,
value: i64,
coin: String,
nft_series: i32,
receiver: String,
hash: String,
},
Token {
id: i64,
fee: i64,
creator: String,
number: i64,
ticker: String,
hash: String,
},
IssueToken {
id: i64,
fee: i64,
creator: String,
number: i64,
ticker: String,
hash: String,
},
Burn {
id: i64,
fee: i64,
address: String,
coin: String,
nft_series: i32,
value: i64,
hash: String,
},
Nft {
id: i64,
fee: i64,
creator: String,
nft_name: String,
series: i16,
count: i64,
hash: String,
},
Marketing {
id: i64,
fee: i64,
advertiser: String,
hash: String,
},
Vanity {
id: i64,
fee: i64,
address: String,
vanity_address: String,
hash: String,
},
Swap {
id: i64,
fee1: i64,
fee2: i64,
ticker1: String,
nft_series1: i32,
ticker2: String,
nft_series2: i32,
value1: i64,
value2: i64,
sender1: String,
tip1: i64,
tip2: i64,
sender2: String,
hash: String,
},
Lender {
id: i64,
fee: i64,
loan_coin: String,
loan_amount: i64,
lender: String,
collateral: String,
collateral_amount: i64,
borrower: String,
txid: String,
hash: String,
},
Borrower {
id: i64,
fee: i64,
payback_amount: i64,
contract_hash: String,
address: String,
tip: i64,
hash: String,
},
Collateral {
id: i64,
fee: i64,
address: String,
contract_hash: String,
hash: String,
},
}
#[derive(Clone, Default)]
pub struct SelectedMempoolBatch {
// The selected transaction view is kept separate from the original
// serialized bytes so save paths can stream the original payloads.
transactions: Vec<SelectedMempoolTransaction>,
originals: Vec<Vec<u8>>,
}
impl SelectedMempoolTransaction {
fn table_name(&self) -> &'static str {
match self {
SelectedMempoolTransaction::Transfer { .. } => "transfer",
SelectedMempoolTransaction::Token { .. } => "token",
SelectedMempoolTransaction::IssueToken { .. } => "issue_token",
SelectedMempoolTransaction::Burn { .. } => "burn",
SelectedMempoolTransaction::Nft { .. } => "nft",
SelectedMempoolTransaction::Marketing { .. } => "marketing",
SelectedMempoolTransaction::Vanity { .. } => "vanity_address",
SelectedMempoolTransaction::Swap { .. } => "swap",
SelectedMempoolTransaction::Lender { .. } => "loan_contract",
SelectedMempoolTransaction::Borrower { .. } => "loan_payment",
SelectedMempoolTransaction::Collateral { .. } => "collateral_claim",
}
}
fn id(&self) -> i64 {
match self {
SelectedMempoolTransaction::Transfer { id, .. }
| SelectedMempoolTransaction::Token { id, .. }
| SelectedMempoolTransaction::IssueToken { id, .. }
| SelectedMempoolTransaction::Burn { id, .. }
| SelectedMempoolTransaction::Nft { id, .. }
| SelectedMempoolTransaction::Marketing { id, .. }
| SelectedMempoolTransaction::Vanity { id, .. }
| SelectedMempoolTransaction::Swap { id, .. }
| SelectedMempoolTransaction::Lender { id, .. }
| SelectedMempoolTransaction::Borrower { id, .. }
| SelectedMempoolTransaction::Collateral { id, .. } => *id,
}
}
}
impl SelectedMempoolBatch {
pub fn is_empty(&self) -> bool {
self.transactions.is_empty()
}
}
mod lookups;
mod processing;
mod schema;
mod selection;
pub use lookups::{
get_basecoin_balance, get_coin_balance, get_pending_payments_for_contract, largest_fee,
signature_exists, total_transactions, transaction_by_signature, transactions_by_address,
}; };
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::HashMap;
use crate::NoTls;
use crate::{task, AtomicBool};
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use std::fs::File;
use std::io::Write;
use tokio_postgres::Client;
lazy_static! {
pub static ref BASECOIN: String = {
let (
_network_name,
base_coin,
_suffix,
_torrentpath,
_wallet_path,
_blockpath,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
format!("{base_coin:<15}")
};
static ref CLEANUP_RUNNING: AtomicBool = AtomicBool::new(false);
}
pub static DB: OnceCell<Client> = OnceCell::new();
pub const EPOCH_ROW_CAP: i64 = 100_000;
const NFT_UNIT: i64 = 100_000_000;
const CLEANUP_DEPTH: u32 = 10;
const CLEANUP_BATCH_LIMIT: i64 = 1000;
#[derive(Clone)]
enum SelectedMempoolTransaction {
// These variants hold the minimal fields needed to score, mark, and
// later apply selected mempool transactions into a saved block.
Transfer {
id: i64,
fee: i64,
sender: String,
value: i64,
coin: String,
nft_series: i32,
receiver: String,
hash: String,
},
Token {
id: i64,
fee: i64,
creator: String,
number: i64,
ticker: String,
hash: String,
},
IssueToken {
id: i64,
fee: i64,
creator: String,
number: i64,
ticker: String,
hash: String,
},
Burn {
id: i64,
fee: i64,
address: String,
coin: String,
nft_series: i32,
value: i64,
hash: String,
},
Nft {
id: i64,
fee: i64,
creator: String,
nft_name: String,
series: i16,
count: i64,
hash: String,
},
Marketing {
id: i64,
fee: i64,
advertiser: String,
hash: String,
},
Vanity {
id: i64,
fee: i64,
address: String,
vanity_address: String,
hash: String,
},
Swap {
id: i64,
fee1: i64,
fee2: i64,
ticker1: String,
nft_series1: i32,
ticker2: String,
nft_series2: i32,
value1: i64,
value2: i64,
sender1: String,
tip1: i64,
tip2: i64,
sender2: String,
hash: String,
},
Lender {
id: i64,
fee: i64,
loan_coin: String,
loan_amount: i64,
lender: String,
collateral: String,
collateral_amount: i64,
borrower: String,
txid: String,
hash: String,
},
Borrower {
id: i64,
fee: i64,
payback_amount: i64,
contract_hash: String,
address: String,
tip: i64,
hash: String,
},
Collateral {
id: i64,
fee: i64,
address: String,
contract_hash: String,
hash: String,
},
}
#[derive(Clone, Default)]
pub struct SelectedMempoolBatch {
// The selected transaction view is kept separate from the original
// serialized bytes so save paths can stream the original payloads.
transactions: Vec<SelectedMempoolTransaction>,
originals: Vec<Vec<u8>>,
}
impl SelectedMempoolTransaction {
fn table_name(&self) -> &'static str {
match self {
SelectedMempoolTransaction::Transfer { .. } => "transfer",
SelectedMempoolTransaction::Token { .. } => "token",
SelectedMempoolTransaction::IssueToken { .. } => "issue_token",
SelectedMempoolTransaction::Burn { .. } => "burn",
SelectedMempoolTransaction::Nft { .. } => "nft",
SelectedMempoolTransaction::Marketing { .. } => "marketing",
SelectedMempoolTransaction::Vanity { .. } => "vanity_address",
SelectedMempoolTransaction::Swap { .. } => "swap",
SelectedMempoolTransaction::Lender { .. } => "loan_contract",
SelectedMempoolTransaction::Borrower { .. } => "loan_payment",
SelectedMempoolTransaction::Collateral { .. } => "collateral_claim",
}
}
fn id(&self) -> i64 {
match self {
SelectedMempoolTransaction::Transfer { id, .. }
| SelectedMempoolTransaction::Token { id, .. }
| SelectedMempoolTransaction::IssueToken { id, .. }
| SelectedMempoolTransaction::Burn { id, .. }
| SelectedMempoolTransaction::Nft { id, .. }
| SelectedMempoolTransaction::Marketing { id, .. }
| SelectedMempoolTransaction::Vanity { id, .. }
| SelectedMempoolTransaction::Swap { id, .. }
| SelectedMempoolTransaction::Lender { id, .. }
| SelectedMempoolTransaction::Borrower { id, .. }
| SelectedMempoolTransaction::Collateral { id, .. } => *id,
}
}
}
impl SelectedMempoolBatch {
pub fn is_empty(&self) -> bool {
self.transactions.is_empty()
}
}
mod lookups;
mod processing;
mod schema;
mod selection;
pub use lookups::{
get_basecoin_balance, get_coin_balance, get_pending_payments_for_contract, largest_fee,
signature_exists, total_transactions, transaction_by_signature, transactions_by_address,
};
pub use processing::{ pub use processing::{
mark_processed_by_signatures, mark_selected_transactions_processed, delete_by_signatures, mark_processed_by_signatures, mark_selected_transactions_processed,
restore_processed_by_signatures, restore_selected_transactions_processed, restore_processed_by_signatures, restore_selected_transactions_processed,
spawn_processed_cleanup, delete_by_signatures, spawn_processed_cleanup,
}; };
pub use schema::{clear_mempool, init_db, setup_mempool}; pub use schema::{clear_mempool, init_db, setup_mempool};
pub use selection::{ pub use selection::{
apply_selected_transaction_math, clear_selected_transaction_sql, delete_selected_transactions, apply_selected_transaction_math, clear_selected_transaction_sql, delete_selected_transactions,
select_transactions_for_block, stream_selected_transaction_originals, select_transactions_for_block, stream_selected_transaction_originals,
}; };
fn required_string(row: &tokio_postgres::Row, column: &str) -> Result<String> { fn required_string(row: &tokio_postgres::Row, column: &str) -> Result<String> {
row.try_get::<_, Option<String>>(column)? row.try_get::<_, Option<String>>(column)?
.ok_or_else(|| anyhow!("Missing required column {column}")) .ok_or_else(|| anyhow!("Missing required column {column}"))
} }
fn add_balance_change( fn add_balance_change(
db: &Db, db: &Db,
balance_changes: &mut HashMap<BalanceKey, i64>, balance_changes: &mut HashMap<BalanceKey, i64>,
address: &str, address: &str,
coin: &str, coin: &str,
delta: i64, delta: i64,
) { ) {
add_balance_change_bytes( add_balance_change_bytes(
balance_changes, balance_changes,
address_key_bytes(db, address), address_key_bytes(db, address),
coin.as_bytes().to_vec(), coin.as_bytes().to_vec(),
delta, delta,
); );
} }
fn add_balance_change_bytes( fn add_balance_change_bytes(
balance_changes: &mut HashMap<BalanceKey, i64>, balance_changes: &mut HashMap<BalanceKey, i64>,
address: Vec<u8>, address: Vec<u8>,
coin: Vec<u8>, coin: Vec<u8>,
delta: i64, delta: i64,
) { ) {
*balance_changes *balance_changes
.entry(BalanceKey { address, coin }) .entry(BalanceKey { address, coin })
.or_insert(0) += delta; .or_insert(0) += delta;
} }
fn address_key_bytes(db: &Db, address: &str) -> Vec<u8> { fn address_key_bytes(db: &Db, address: &str) -> Vec<u8> {
resolve_canonical_registered_short_address(db, address) resolve_canonical_registered_short_address(db, address)
.ok() .ok()
.flatten() .flatten()
.or_else(|| Wallet::normalize_to_short_address(address)) .or_else(|| Wallet::normalize_to_short_address(address))
.map(|normalized| normalized.as_bytes().to_vec()) .map(|normalized| normalized.as_bytes().to_vec())
.unwrap_or_else(|| address.as_bytes().to_vec()) .unwrap_or_else(|| address.as_bytes().to_vec())
} }
fn canonical_mempool_addresses(db: &Db, address: &str) -> Vec<String> { fn canonical_mempool_addresses(db: &Db, address: &str) -> Vec<String> {
let canonical = resolve_canonical_registered_short_address(db, address) let canonical = resolve_canonical_registered_short_address(db, address)
.ok() .ok()
.flatten() .flatten()
.or_else(|| Wallet::normalize_to_short_address(address)) .or_else(|| Wallet::normalize_to_short_address(address))
.unwrap_or_else(|| address.to_string()); .unwrap_or_else(|| address.to_string());
vec![canonical] vec![canonical]
} }
async fn resolve_loan_details(db: &Db, contract_hash: &str) -> Result<(Vec<u8>, Vec<u8>)> { async fn resolve_loan_details(db: &Db, contract_hash: &str) -> Result<(Vec<u8>, Vec<u8>)> {
let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await; let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await;
if bytes.is_empty() || bytes[0] != 7 { if bytes.is_empty() || bytes[0] != 7 {
return Ok((Vec::new(), Vec::new())); return Ok((Vec::new(), Vec::new()));
} }
match LoanContractTransaction::from_bytes(7, &bytes[1..]).await { match LoanContractTransaction::from_bytes(7, &bytes[1..]).await {
Ok(loan) => Ok(( Ok(loan) => Ok((
loan.unsigned_loan_contract.loan_coin.as_bytes().to_vec(), loan.unsigned_loan_contract.loan_coin.as_bytes().to_vec(),
address_key_bytes(db, &loan.unsigned_loan_contract.lender), address_key_bytes(db, &loan.unsigned_loan_contract.lender),
)), )),
Err(_) => Ok((Vec::new(), Vec::new())), Err(_) => Ok((Vec::new(), Vec::new())),
} }
} }
async fn resolve_collateral_details(db: &Db, contract_hash: &str) -> Result<(Vec<u8>, i64)> { async fn resolve_collateral_details(db: &Db, contract_hash: &str) -> Result<(Vec<u8>, i64)> {
let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await; let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await;
if bytes.is_empty() || bytes[0] != 7 { if bytes.is_empty() || bytes[0] != 7 {
return Ok((Vec::new(), 0)); return Ok((Vec::new(), 0));
} }
match LoanContractTransaction::from_bytes(7, &bytes[1..]).await { match LoanContractTransaction::from_bytes(7, &bytes[1..]).await {
Ok(loan) => Ok(( Ok(loan) => Ok((
loan.unsigned_loan_contract.collateral.as_bytes().to_vec(), loan.unsigned_loan_contract.collateral.as_bytes().to_vec(),
loan.unsigned_loan_contract.collateral_amount as i64, loan.unsigned_loan_contract.collateral_amount as i64,
)), )),
Err(_) => Ok((Vec::new(), 0)), Err(_) => Ok((Vec::new(), 0)),
} }
} }
fn ids_for_table(batch: &SelectedMempoolBatch, table: &str) -> Vec<i64> { fn ids_for_table(batch: &SelectedMempoolBatch, table: &str) -> Vec<i64> {
batch batch
.transactions .transactions
.iter() .iter()
.filter(|tx| tx.table_name() == table) .filter(|tx| tx.table_name() == table)
.map(SelectedMempoolTransaction::id) .map(SelectedMempoolTransaction::id)
.collect() .collect()
} }
async fn mark_rows_by_ids( async fn mark_rows_by_ids(
client: &Client, client: &Client,
table: &str, table: &str,
ids: &[i64], ids: &[i64],
block_number: i32, block_number: i32,
) -> Result<()> { ) -> Result<()> {
if ids.is_empty() { if ids.is_empty() {
return Ok(()); return Ok(());
} }
let statement = format!( let statement =
"UPDATE {table} SET processed=true, processed_block_number=$1 WHERE id = ANY($2)" format!("UPDATE {table} SET processed=true, processed_block_number=$1 WHERE id = ANY($2)");
); client.execute(&statement, &[&block_number, &ids]).await?;
client.execute(&statement, &[&block_number, &ids]).await?; Ok(())
Ok(())
} }
async fn unmark_rows_by_ids(client: &Client, table: &str, ids: &[i64]) -> Result<()> { async fn unmark_rows_by_ids(client: &Client, table: &str, ids: &[i64]) -> Result<()> {
@ -332,75 +328,76 @@ async fn unmark_rows_by_ids(client: &Client, table: &str, ids: &[i64]) -> Result
return Ok(()); return Ok(());
} }
let statement = let statement = format!(
format!("UPDATE {table} SET processed=false, processed_block_number=NULL WHERE id = ANY($1)"); "UPDATE {table} SET processed=false, processed_block_number=NULL WHERE id = ANY($1)"
);
client.execute(&statement, &[&ids]).await?; client.execute(&statement, &[&ids]).await?;
Ok(()) Ok(())
} }
async fn delete_rows(client: &Client, table: &str, ids: &[i64]) -> Result<()> { async fn delete_rows(client: &Client, table: &str, ids: &[i64]) -> Result<()> {
if ids.is_empty() { if ids.is_empty() {
return Ok(()); return Ok(());
} }
let statement = format!("DELETE FROM {table} WHERE id = ANY($1)"); let statement = format!("DELETE FROM {table} WHERE id = ANY($1)");
client.execute(&statement, &[&ids]).await?; client.execute(&statement, &[&ids]).await?;
Ok(()) Ok(())
} }
async fn unmark_by_signatures( async fn unmark_by_signatures(
client: &Client, client: &Client,
table: &str, table: &str,
signature_column: &str, signature_column: &str,
signatures: &[String], signatures: &[String],
) -> Result<u64> { ) -> Result<u64> {
let statement = format!( let statement = format!(
"UPDATE {table} SET processed=false, processed_block_number=NULL WHERE {signature_column} = ANY($1) AND processed = true" "UPDATE {table} SET processed=false, processed_block_number=NULL WHERE {signature_column} = ANY($1) AND processed = true"
); );
Ok(client.execute(&statement, &[&signatures]).await?) Ok(client.execute(&statement, &[&signatures]).await?)
} }
async fn delete_processed_before_or_at(block_number: u32, limit: i64) -> Result<()> { async fn delete_processed_before_or_at(block_number: u32, limit: i64) -> Result<()> {
// Periodic cleanup deletes processed mempool rows in bounded batches // Periodic cleanup deletes processed mempool rows in bounded batches
// so long-lived nodes do not accumulate infinite processed history. // so long-lived nodes do not accumulate infinite processed history.
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
let bn = block_number as i32; let bn = block_number as i32;
delete_processed_rows_limited(client, "transfer", bn, limit).await?; delete_processed_rows_limited(client, "transfer", bn, limit).await?;
delete_processed_rows_limited(client, "token", bn, limit).await?; delete_processed_rows_limited(client, "token", bn, limit).await?;
delete_processed_rows_limited(client, "issue_token", bn, limit).await?; delete_processed_rows_limited(client, "issue_token", bn, limit).await?;
delete_processed_rows_limited(client, "burn", bn, limit).await?; delete_processed_rows_limited(client, "burn", bn, limit).await?;
delete_processed_rows_limited(client, "nft", bn, limit).await?; delete_processed_rows_limited(client, "nft", bn, limit).await?;
delete_processed_rows_limited(client, "marketing", bn, limit).await?; delete_processed_rows_limited(client, "marketing", bn, limit).await?;
delete_processed_rows_limited(client, "vanity_address", bn, limit).await?; delete_processed_rows_limited(client, "vanity_address", bn, limit).await?;
delete_processed_rows_limited(client, "swap", bn, limit).await?; delete_processed_rows_limited(client, "swap", bn, limit).await?;
delete_processed_rows_limited(client, "loan_contract", bn, limit).await?; delete_processed_rows_limited(client, "loan_contract", bn, limit).await?;
delete_processed_rows_limited(client, "loan_payment", bn, limit).await?; delete_processed_rows_limited(client, "loan_payment", bn, limit).await?;
delete_processed_rows_limited(client, "collateral_claim", bn, limit).await?; delete_processed_rows_limited(client, "collateral_claim", bn, limit).await?;
Ok(()) Ok(())
} }
async fn delete_processed_rows_limited( async fn delete_processed_rows_limited(
client: &Client, client: &Client,
table: &str, table: &str,
block_number: i32, block_number: i32,
limit: i64, limit: i64,
) -> Result<u64> { ) -> Result<u64> {
let statement = format!( let statement = format!(
r#" r#"
DELETE FROM {table} DELETE FROM {table}
WHERE id IN ( WHERE id IN (
SELECT id SELECT id
FROM {table} FROM {table}
WHERE processed = true WHERE processed = true
AND processed_block_number IS NOT NULL AND processed_block_number IS NOT NULL
AND processed_block_number <= $1 AND processed_block_number <= $1
ORDER BY processed_block_number ASC, id ASC ORDER BY processed_block_number ASC, id ASC
LIMIT $2 LIMIT $2
) )
"# "#
); );
Ok(client.execute(&statement, &[&block_number, &limit]).await?) Ok(client.execute(&statement, &[&block_number, &limit]).await?)
} }

View File

@ -1,57 +1,57 @@
use super::*; use super::*;
pub async fn mark_selected_transactions_processed( pub async fn mark_selected_transactions_processed(
batch: &SelectedMempoolBatch, batch: &SelectedMempoolBatch,
block_number: u32, block_number: u32,
) -> Result<()> { ) -> Result<()> {
// Mark each selected mempool row as processed under the saved block // Mark each selected mempool row as processed under the saved block
// number so it can be cleaned up or restored later if needed. // number so it can be cleaned up or restored later if needed.
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
let bn = block_number as i32; let bn = block_number as i32;
// Selected batches are grouped by table, then marked with one UPDATE per // Selected batches are grouped by table, then marked with one UPDATE per
// table instead of touching rows one at a time. // table instead of touching rows one at a time.
mark_rows_by_ids(client, "transfer", &ids_for_table(batch, "transfer"), bn).await?; mark_rows_by_ids(client, "transfer", &ids_for_table(batch, "transfer"), bn).await?;
mark_rows_by_ids(client, "token", &ids_for_table(batch, "token"), bn).await?; mark_rows_by_ids(client, "token", &ids_for_table(batch, "token"), bn).await?;
mark_rows_by_ids( mark_rows_by_ids(
client, client,
"issue_token", "issue_token",
&ids_for_table(batch, "issue_token"), &ids_for_table(batch, "issue_token"),
bn, bn,
) )
.await?; .await?;
mark_rows_by_ids(client, "burn", &ids_for_table(batch, "burn"), bn).await?; mark_rows_by_ids(client, "burn", &ids_for_table(batch, "burn"), bn).await?;
mark_rows_by_ids(client, "nft", &ids_for_table(batch, "nft"), bn).await?; mark_rows_by_ids(client, "nft", &ids_for_table(batch, "nft"), bn).await?;
mark_rows_by_ids(client, "marketing", &ids_for_table(batch, "marketing"), bn).await?; mark_rows_by_ids(client, "marketing", &ids_for_table(batch, "marketing"), bn).await?;
mark_rows_by_ids( mark_rows_by_ids(
client, client,
"vanity_address", "vanity_address",
&ids_for_table(batch, "vanity_address"), &ids_for_table(batch, "vanity_address"),
bn, bn,
) )
.await?; .await?;
mark_rows_by_ids(client, "swap", &ids_for_table(batch, "swap"), bn).await?; mark_rows_by_ids(client, "swap", &ids_for_table(batch, "swap"), bn).await?;
mark_rows_by_ids( mark_rows_by_ids(
client, client,
"loan_contract", "loan_contract",
&ids_for_table(batch, "loan_contract"), &ids_for_table(batch, "loan_contract"),
bn, bn,
) )
.await?; .await?;
mark_rows_by_ids( mark_rows_by_ids(
client, client,
"loan_payment", "loan_payment",
&ids_for_table(batch, "loan_payment"), &ids_for_table(batch, "loan_payment"),
bn, bn,
) )
.await?; .await?;
mark_rows_by_ids( mark_rows_by_ids(
client, client,
"collateral_claim", "collateral_claim",
&ids_for_table(batch, "collateral_claim"), &ids_for_table(batch, "collateral_claim"),
bn, bn,
) )
.await?; .await?;
Ok(()) Ok(())
} }
@ -63,12 +63,7 @@ pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatc
unmark_rows_by_ids(client, "transfer", &ids_for_table(batch, "transfer")).await?; unmark_rows_by_ids(client, "transfer", &ids_for_table(batch, "transfer")).await?;
unmark_rows_by_ids(client, "token", &ids_for_table(batch, "token")).await?; unmark_rows_by_ids(client, "token", &ids_for_table(batch, "token")).await?;
unmark_rows_by_ids( unmark_rows_by_ids(client, "issue_token", &ids_for_table(batch, "issue_token")).await?;
client,
"issue_token",
&ids_for_table(batch, "issue_token"),
)
.await?;
unmark_rows_by_ids(client, "burn", &ids_for_table(batch, "burn")).await?; unmark_rows_by_ids(client, "burn", &ids_for_table(batch, "burn")).await?;
unmark_rows_by_ids(client, "nft", &ids_for_table(batch, "nft")).await?; unmark_rows_by_ids(client, "nft", &ids_for_table(batch, "nft")).await?;
unmark_rows_by_ids(client, "marketing", &ids_for_table(batch, "marketing")).await?; unmark_rows_by_ids(client, "marketing", &ids_for_table(batch, "marketing")).await?;
@ -102,233 +97,231 @@ pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatc
} }
pub async fn restore_processed_by_signatures(signatures: &[String]) -> Result<bool> { pub async fn restore_processed_by_signatures(signatures: &[String]) -> Result<bool> {
// Orphan correction can revive recently processed mempool rows by // Orphan correction can revive recently processed mempool rows by
// signature when a saved block is rolled back out of the chain. // signature when a saved block is rolled back out of the chain.
if signatures.is_empty() { if signatures.is_empty() {
return Ok(false); return Ok(false);
} }
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
let mut restored = 0_u64; let mut restored = 0_u64;
// Each table keeps its own signature columns, so rollback unmarks every // Each table keeps its own signature columns, so rollback unmarks every
// column that could contain one of the rolled-back signatures. // column that could contain one of the rolled-back signatures.
restored += unmark_by_signatures(client, "transfer", "signature", signatures).await?; restored += unmark_by_signatures(client, "transfer", "signature", signatures).await?;
restored += unmark_by_signatures(client, "token", "signature", signatures).await?; restored += unmark_by_signatures(client, "token", "signature", signatures).await?;
restored += unmark_by_signatures(client, "issue_token", "signature", signatures).await?; restored += unmark_by_signatures(client, "issue_token", "signature", signatures).await?;
restored += unmark_by_signatures(client, "burn", "signature", signatures).await?; restored += unmark_by_signatures(client, "burn", "signature", signatures).await?;
restored += unmark_by_signatures(client, "nft", "signature", signatures).await?; restored += unmark_by_signatures(client, "nft", "signature", signatures).await?;
restored += unmark_by_signatures(client, "marketing", "signature", signatures).await?; restored += unmark_by_signatures(client, "marketing", "signature", signatures).await?;
restored += unmark_by_signatures(client, "vanity_address", "signature", signatures).await?; restored += unmark_by_signatures(client, "vanity_address", "signature", signatures).await?;
restored += unmark_by_signatures(client, "swap", "signature1", signatures).await?; restored += unmark_by_signatures(client, "swap", "signature1", signatures).await?;
restored += unmark_by_signatures(client, "swap", "signature2", signatures).await?; restored += unmark_by_signatures(client, "swap", "signature2", signatures).await?;
restored += unmark_by_signatures(client, "loan_contract", "signature1", signatures).await?; restored += unmark_by_signatures(client, "loan_contract", "signature1", signatures).await?;
restored += unmark_by_signatures(client, "loan_contract", "signature2", signatures).await?; restored += unmark_by_signatures(client, "loan_contract", "signature2", signatures).await?;
restored += unmark_by_signatures(client, "loan_payment", "signature", signatures).await?; restored += unmark_by_signatures(client, "loan_payment", "signature", signatures).await?;
restored += unmark_by_signatures(client, "collateral_claim", "signature", signatures).await?; restored += unmark_by_signatures(client, "collateral_claim", "signature", signatures).await?;
Ok(restored > 0) Ok(restored > 0)
} }
pub fn spawn_processed_cleanup(saved_block_number: u32) { pub fn spawn_processed_cleanup(saved_block_number: u32) {
// Cleanup trails the chain tip by a small depth so recent processed // Cleanup trails the chain tip by a small depth so recent processed
// mempool rows can still be restored during short orphan events. // mempool rows can still be restored during short orphan events.
if saved_block_number <= CLEANUP_DEPTH { if saved_block_number <= CLEANUP_DEPTH {
return; return;
} }
if CLEANUP_RUNNING if CLEANUP_RUNNING
.compare_exchange( .compare_exchange(
false, false,
true, true,
crate::AtomicOrdering::SeqCst, crate::AtomicOrdering::SeqCst,
crate::AtomicOrdering::SeqCst, crate::AtomicOrdering::SeqCst,
) )
.is_err() .is_err()
{ {
return; return;
} }
task::spawn(async move { task::spawn(async move {
let safe_block = saved_block_number.saturating_sub(CLEANUP_DEPTH); let safe_block = saved_block_number.saturating_sub(CLEANUP_DEPTH);
// Cleanup is deliberately delayed behind the tip so short reorgs can // Cleanup is deliberately delayed behind the tip so short reorgs can
// still restore recently processed rows. // still restore recently processed rows.
if let Err(err) = delete_processed_before_or_at(safe_block, CLEANUP_BATCH_LIMIT).await { if let Err(err) = delete_processed_before_or_at(safe_block, CLEANUP_BATCH_LIMIT).await {
eprintln!( eprintln!(
"[mempool_cleanup] failed: saved_block={saved_block_number} safe_block={safe_block} err={err}" "[mempool_cleanup] failed: saved_block={saved_block_number} safe_block={safe_block} err={err}"
); );
} }
CLEANUP_RUNNING.store(false, crate::AtomicOrdering::SeqCst); CLEANUP_RUNNING.store(false, crate::AtomicOrdering::SeqCst);
}); });
} }
pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u32) -> Result<()> {
pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u32) -> Result<()> { // Synced blocks arrive with signatures instead of selected-row IDs,
// Synced blocks arrive with signatures instead of selected-row IDs, // so processed marking on the updating path works by signature.
// so processed marking on the updating path works by signature. if signatures.is_empty() {
if signatures.is_empty() { return Ok(());
return Ok(()); }
}
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
let bn = block_number as i32; let bn = block_number as i32;
// Remote/synced blocks do not know local row IDs, so they mark by // Remote/synced blocks do not know local row IDs, so they mark by
// transaction signatures instead. // transaction signatures instead.
client client
.execute( .execute(
"UPDATE transfer SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE transfer SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE issue_token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE issue_token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE burn SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE burn SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE nft SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE nft SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE marketing SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE marketing SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE vanity_address SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE vanity_address SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)", "UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)", "UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)", "UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)", "UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE loan_payment SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE loan_payment SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"UPDATE collateral_claim SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", "UPDATE collateral_claim SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)",
&[&bn, &signatures], &[&bn, &signatures],
) )
.await?; .await?;
Ok(()) Ok(())
} }
pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> { pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> {
// Some validation failures need to remove mempool rows directly by // Some validation failures need to remove mempool rows directly by
// signature regardless of which table the transaction lives in. // signature regardless of which table the transaction lives in.
if signatures.is_empty() { if signatures.is_empty() {
return Ok(()); return Ok(());
} }
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
// Failed validation removes every matching pending row no matter which // Failed validation removes every matching pending row no matter which
// transaction table currently owns the signature. // transaction table currently owns the signature.
client client
.execute( .execute(
"DELETE FROM transfer WHERE signature = ANY($1)", "DELETE FROM transfer WHERE signature = ANY($1)",
&[&signatures], &[&signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"DELETE FROM token WHERE signature = ANY($1)", "DELETE FROM token WHERE signature = ANY($1)",
&[&signatures], &[&signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"DELETE FROM issue_token WHERE signature = ANY($1)", "DELETE FROM issue_token WHERE signature = ANY($1)",
&[&signatures], &[&signatures],
) )
.await?; .await?;
client client
.execute("DELETE FROM burn WHERE signature = ANY($1)", &[&signatures]) .execute("DELETE FROM burn WHERE signature = ANY($1)", &[&signatures])
.await?; .await?;
client client
.execute("DELETE FROM nft WHERE signature = ANY($1)", &[&signatures]) .execute("DELETE FROM nft WHERE signature = ANY($1)", &[&signatures])
.await?; .await?;
client client
.execute( .execute(
"DELETE FROM marketing WHERE signature = ANY($1)", "DELETE FROM marketing WHERE signature = ANY($1)",
&[&signatures], &[&signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"DELETE FROM vanity_address WHERE signature = ANY($1)", "DELETE FROM vanity_address WHERE signature = ANY($1)",
&[&signatures], &[&signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"DELETE FROM swap WHERE signature1 = ANY($1) OR signature2 = ANY($1)", "DELETE FROM swap WHERE signature1 = ANY($1) OR signature2 = ANY($1)",
&[&signatures], &[&signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"DELETE FROM loan_contract WHERE signature1 = ANY($1) OR signature2 = ANY($1)", "DELETE FROM loan_contract WHERE signature1 = ANY($1) OR signature2 = ANY($1)",
&[&signatures], &[&signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"DELETE FROM loan_payment WHERE signature = ANY($1)", "DELETE FROM loan_payment WHERE signature = ANY($1)",
&[&signatures], &[&signatures],
) )
.await?; .await?;
client client
.execute( .execute(
"DELETE FROM collateral_claim WHERE signature = ANY($1)", "DELETE FROM collateral_claim WHERE signature = ANY($1)",
&[&signatures], &[&signatures],
) )
.await?; .await?;
Ok(()) Ok(())
} }

View File

@ -1,21 +1,21 @@
use super::*; use super::*;
pub async fn init_db() -> Result<()> { pub async fn init_db() -> Result<()> {
// Initialize the shared Postgres client used by the mempool tables. // Initialize the shared Postgres client used by the mempool tables.
if DB.get().is_some() { if DB.get().is_some() {
return Ok(()); return Ok(());
} }
let password = SETTINGS let password = SETTINGS
.pg_password .pg_password
.as_deref() .as_deref()
.expect("Postgres password must be set in settings.ini"); .expect("Postgres password must be set in settings.ini");
let conn_str = format!( let conn_str = format!(
"host={} port={} user={} password={} dbname={}", "host={} port={} user={} password={} dbname={}",
SETTINGS.pg_host, SETTINGS.pg_port, SETTINGS.pg_user, password, SETTINGS.pg_dbname SETTINGS.pg_host, SETTINGS.pg_port, SETTINGS.pg_user, password, SETTINGS.pg_dbname
); );
let (client, connection) = tokio_postgres::connect(&conn_str, NoTls) let (client, connection) = tokio_postgres::connect(&conn_str, NoTls)
.await .await
.map_err(|err| anyhow!("Failed to connect to Postgres: {err}"))?; .map_err(|err| anyhow!("Failed to connect to Postgres: {err}"))?;
@ -23,228 +23,228 @@ pub async fn init_db() -> Result<()> {
// Keep the Postgres connection driver alive in the background for the // Keep the Postgres connection driver alive in the background for the
// lifetime of the shared client. // lifetime of the shared client.
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = connection.await { if let Err(e) = connection.await {
eprintln!("Postgres connection error: {e}"); eprintln!("Postgres connection error: {e}");
} }
}); });
DB.set(client) DB.set(client)
.map_err(|_| anyhow!("DB already initialized"))?; .map_err(|_| anyhow!("DB already initialized"))?;
Ok(()) Ok(())
} }
pub async fn setup_mempool() -> Result<()> { pub async fn setup_mempool() -> Result<()> {
// Create or migrate the mempool schema, deduplicate any stale rows, // Create or migrate the mempool schema, deduplicate any stale rows,
// add the selection indexes, and start from an empty live mempool. // add the selection indexes, and start from an empty live mempool.
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
let schema = r#" let schema = r#"
CREATE TABLE IF NOT EXISTS transfer ( CREATE TABLE IF NOT EXISTS transfer (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL, time INTEGER NOT NULL,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
sender TEXT, sender TEXT,
value BIGINT, value BIGINT,
coin VARCHAR(15), coin VARCHAR(15),
nft_series INTEGER NOT NULL DEFAULT 0, nft_series INTEGER NOT NULL DEFAULT 0,
receiver TEXT NOT NULL, receiver TEXT NOT NULL,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL, signature TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS token ( CREATE TABLE IF NOT EXISTS token (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL, time INTEGER NOT NULL,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
creator TEXT NOT NULL, creator TEXT NOT NULL,
number BIGINT NOT NULL, number BIGINT NOT NULL,
hard_limit SMALLINT NOT NULL DEFAULT 1, hard_limit SMALLINT NOT NULL DEFAULT 1,
ticker VARCHAR(15) NOT NULL, ticker VARCHAR(15) NOT NULL,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL, signature TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS issue_token ( CREATE TABLE IF NOT EXISTS issue_token (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL, time INTEGER NOT NULL,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
creator TEXT NOT NULL, creator TEXT NOT NULL,
number BIGINT NOT NULL, number BIGINT NOT NULL,
ticker VARCHAR(15) NOT NULL, ticker VARCHAR(15) NOT NULL,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL, signature TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS burn ( CREATE TABLE IF NOT EXISTS burn (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL, time INTEGER NOT NULL,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
coin VARCHAR(15) NOT NULL, coin VARCHAR(15) NOT NULL,
nft_series INTEGER NOT NULL DEFAULT 0, nft_series INTEGER NOT NULL DEFAULT 0,
value BIGINT NOT NULL, value BIGINT NOT NULL,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL, signature TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS nft ( CREATE TABLE IF NOT EXISTS nft (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
time INTEGER NOT NULL, time INTEGER NOT NULL,
creator TEXT NOT NULL, creator TEXT NOT NULL,
nft_name VARCHAR(15), nft_name VARCHAR(15),
series SMALLINT NOT NULL, series SMALLINT NOT NULL,
count BIGINT NOT NULL DEFAULT 1, count BIGINT NOT NULL DEFAULT 1,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL, signature TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS marketing ( CREATE TABLE IF NOT EXISTS marketing (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL, time INTEGER NOT NULL,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
advertiser TEXT NOT NULL, advertiser TEXT NOT NULL,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL, signature TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS vanity_address ( CREATE TABLE IF NOT EXISTS vanity_address (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL, time INTEGER NOT NULL,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
vanity_address TEXT NOT NULL, vanity_address TEXT NOT NULL,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL, signature TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS swap ( CREATE TABLE IF NOT EXISTS swap (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
fee1 BIGINT NOT NULL, fee1 BIGINT NOT NULL,
fee2 BIGINT NOT NULL, fee2 BIGINT NOT NULL,
time INTEGER NOT NULL, time INTEGER NOT NULL,
ticker1 VARCHAR(15), ticker1 VARCHAR(15),
nft_series1 INTEGER NOT NULL DEFAULT 0, nft_series1 INTEGER NOT NULL DEFAULT 0,
ticker2 VARCHAR(15), ticker2 VARCHAR(15),
nft_series2 INTEGER NOT NULL DEFAULT 0, nft_series2 INTEGER NOT NULL DEFAULT 0,
value1 BIGINT NOT NULL, value1 BIGINT NOT NULL,
value2 BIGINT NOT NULL, value2 BIGINT NOT NULL,
sender1 TEXT NOT NULL, sender1 TEXT NOT NULL,
tip1 BIGINT NOT NULL, tip1 BIGINT NOT NULL,
tip2 BIGINT NOT NULL, tip2 BIGINT NOT NULL,
sender2 TEXT NOT NULL, sender2 TEXT NOT NULL,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature1 TEXT NOT NULL, signature1 TEXT NOT NULL,
signature2 TEXT NOT NULL, signature2 TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS loan_contract ( CREATE TABLE IF NOT EXISTS loan_contract (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
time INTEGER NOT NULL, time INTEGER NOT NULL,
loan_coin VARCHAR(15), loan_coin VARCHAR(15),
loan_amount BIGINT NOT NULL, loan_amount BIGINT NOT NULL,
lender TEXT NOT NULL, lender TEXT NOT NULL,
collateral VARCHAR(15), collateral VARCHAR(15),
collateral_amount BIGINT NOT NULL, collateral_amount BIGINT NOT NULL,
borrower TEXT NOT NULL, borrower TEXT NOT NULL,
txid VARCHAR(64) NOT NULL, txid VARCHAR(64) NOT NULL,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature1 TEXT NOT NULL, signature1 TEXT NOT NULL,
signature2 TEXT NOT NULL, signature2 TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS loan_payment ( CREATE TABLE IF NOT EXISTS loan_payment (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
time INTEGER NOT NULL, time INTEGER NOT NULL,
payback_amount BIGINT NOT NULL, payback_amount BIGINT NOT NULL,
contract_hash VARCHAR(64) NOT NULL, contract_hash VARCHAR(64) NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
tip BIGINT NOT NULL, tip BIGINT NOT NULL,
txid VARCHAR(64), txid VARCHAR(64),
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL, signature TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
CREATE TABLE IF NOT EXISTS collateral_claim ( CREATE TABLE IF NOT EXISTS collateral_claim (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
time INTEGER NOT NULL, time INTEGER NOT NULL,
fee BIGINT NOT NULL, fee BIGINT NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
contract_hash VARCHAR(64) NOT NULL, contract_hash VARCHAR(64) NOT NULL,
hash VARCHAR(64) NOT NULL, hash VARCHAR(64) NOT NULL,
signature TEXT NOT NULL, signature TEXT NOT NULL,
processed bool DEFAULT false, processed bool DEFAULT false,
processed_block_number INTEGER DEFAULT NULL, processed_block_number INTEGER DEFAULT NULL,
original BYTEA NOT NULL original BYTEA NOT NULL
); );
ALTER TABLE loan_payment ADD COLUMN IF NOT EXISTS txid VARCHAR(64); ALTER TABLE loan_payment ADD COLUMN IF NOT EXISTS txid VARCHAR(64);
ALTER TABLE transfer ADD COLUMN IF NOT EXISTS nft_series INTEGER NOT NULL DEFAULT 0; ALTER TABLE transfer ADD COLUMN IF NOT EXISTS nft_series INTEGER NOT NULL DEFAULT 0;
ALTER TABLE nft ADD COLUMN IF NOT EXISTS count BIGINT NOT NULL DEFAULT 1; ALTER TABLE nft ADD COLUMN IF NOT EXISTS count BIGINT NOT NULL DEFAULT 1;
ALTER TABLE token ADD COLUMN IF NOT EXISTS hard_limit SMALLINT NOT NULL DEFAULT 1; ALTER TABLE token ADD COLUMN IF NOT EXISTS hard_limit SMALLINT NOT NULL DEFAULT 1;
ALTER TABLE swap ADD COLUMN IF NOT EXISTS nft_series1 INTEGER NOT NULL DEFAULT 0; ALTER TABLE swap ADD COLUMN IF NOT EXISTS nft_series1 INTEGER NOT NULL DEFAULT 0;
ALTER TABLE swap ADD COLUMN IF NOT EXISTS nft_series2 INTEGER NOT NULL DEFAULT 0; ALTER TABLE swap ADD COLUMN IF NOT EXISTS nft_series2 INTEGER NOT NULL DEFAULT 0;
ALTER TABLE transfer ALTER COLUMN sender TYPE TEXT; ALTER TABLE transfer ALTER COLUMN sender TYPE TEXT;
ALTER TABLE transfer ALTER COLUMN receiver TYPE TEXT; ALTER TABLE transfer ALTER COLUMN receiver TYPE TEXT;
ALTER TABLE transfer ALTER COLUMN signature TYPE TEXT; ALTER TABLE transfer ALTER COLUMN signature TYPE TEXT;
ALTER TABLE token ALTER COLUMN creator TYPE TEXT; ALTER TABLE token ALTER COLUMN creator TYPE TEXT;
ALTER TABLE token ALTER COLUMN signature TYPE TEXT; ALTER TABLE token ALTER COLUMN signature TYPE TEXT;
ALTER TABLE issue_token ALTER COLUMN creator TYPE TEXT; ALTER TABLE issue_token ALTER COLUMN creator TYPE TEXT;
ALTER TABLE issue_token ALTER COLUMN signature TYPE TEXT; ALTER TABLE issue_token ALTER COLUMN signature TYPE TEXT;
ALTER TABLE burn ALTER COLUMN address TYPE TEXT; ALTER TABLE burn ALTER COLUMN address TYPE TEXT;
ALTER TABLE burn ALTER COLUMN signature TYPE TEXT; ALTER TABLE burn ALTER COLUMN signature TYPE TEXT;
ALTER TABLE nft ALTER COLUMN creator TYPE TEXT; ALTER TABLE nft ALTER COLUMN creator TYPE TEXT;
ALTER TABLE nft ALTER COLUMN signature TYPE TEXT; ALTER TABLE nft ALTER COLUMN signature TYPE TEXT;
ALTER TABLE marketing ALTER COLUMN advertiser TYPE TEXT; ALTER TABLE marketing ALTER COLUMN advertiser TYPE TEXT;
ALTER TABLE marketing ALTER COLUMN signature TYPE TEXT; ALTER TABLE marketing ALTER COLUMN signature TYPE TEXT;
ALTER TABLE vanity_address ALTER COLUMN address TYPE TEXT; ALTER TABLE vanity_address ALTER COLUMN address TYPE TEXT;
ALTER TABLE vanity_address ALTER COLUMN vanity_address TYPE TEXT; ALTER TABLE vanity_address ALTER COLUMN vanity_address TYPE TEXT;
ALTER TABLE vanity_address ALTER COLUMN signature TYPE TEXT; ALTER TABLE vanity_address ALTER COLUMN signature TYPE TEXT;
ALTER TABLE swap ALTER COLUMN sender1 TYPE TEXT; ALTER TABLE swap ALTER COLUMN sender1 TYPE TEXT;
ALTER TABLE swap ALTER COLUMN sender2 TYPE TEXT; ALTER TABLE swap ALTER COLUMN sender2 TYPE TEXT;
ALTER TABLE swap ALTER COLUMN signature1 TYPE TEXT; ALTER TABLE swap ALTER COLUMN signature1 TYPE TEXT;
ALTER TABLE swap ALTER COLUMN signature2 TYPE TEXT; ALTER TABLE swap ALTER COLUMN signature2 TYPE TEXT;
ALTER TABLE loan_contract ALTER COLUMN lender TYPE TEXT; ALTER TABLE loan_contract ALTER COLUMN lender TYPE TEXT;
ALTER TABLE loan_contract ALTER COLUMN borrower TYPE TEXT; ALTER TABLE loan_contract ALTER COLUMN borrower TYPE TEXT;
ALTER TABLE loan_contract ALTER COLUMN signature1 TYPE TEXT; ALTER TABLE loan_contract ALTER COLUMN signature1 TYPE TEXT;
ALTER TABLE loan_contract ALTER COLUMN signature2 TYPE TEXT; ALTER TABLE loan_contract ALTER COLUMN signature2 TYPE TEXT;
ALTER TABLE loan_payment ALTER COLUMN address TYPE TEXT; ALTER TABLE loan_payment ALTER COLUMN address TYPE TEXT;
ALTER TABLE loan_payment ALTER COLUMN signature TYPE TEXT; ALTER TABLE loan_payment ALTER COLUMN signature TYPE TEXT;
ALTER TABLE collateral_claim ALTER COLUMN address TYPE TEXT; ALTER TABLE collateral_claim ALTER COLUMN address TYPE TEXT;
ALTER TABLE collateral_claim ALTER COLUMN signature TYPE TEXT; ALTER TABLE collateral_claim ALTER COLUMN signature TYPE TEXT;
"#; "#;
// The schema block creates fresh installs and also carries small migrations // The schema block creates fresh installs and also carries small migrations
@ -252,57 +252,57 @@ pub async fn setup_mempool() -> Result<()> {
client.batch_execute(schema).await?; client.batch_execute(schema).await?;
let dedupe = r#" let dedupe = r#"
DELETE FROM transfer a DELETE FROM transfer a
USING transfer b USING transfer b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM token a DELETE FROM token a
USING token b USING token b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM issue_token a DELETE FROM issue_token a
USING issue_token b USING issue_token b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM burn a DELETE FROM burn a
USING burn b USING burn b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM nft a DELETE FROM nft a
USING nft b USING nft b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM marketing a DELETE FROM marketing a
USING marketing b USING marketing b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM vanity_address a DELETE FROM vanity_address a
USING vanity_address b USING vanity_address b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM swap a DELETE FROM swap a
USING swap b USING swap b
WHERE a.id > b.id AND a.signature1 = b.signature1; WHERE a.id > b.id AND a.signature1 = b.signature1;
DELETE FROM swap a DELETE FROM swap a
USING swap b USING swap b
WHERE a.id > b.id AND a.signature2 = b.signature2; WHERE a.id > b.id AND a.signature2 = b.signature2;
DELETE FROM loan_contract a DELETE FROM loan_contract a
USING loan_contract b USING loan_contract b
WHERE a.id > b.id AND a.signature1 = b.signature1; WHERE a.id > b.id AND a.signature1 = b.signature1;
DELETE FROM loan_contract a DELETE FROM loan_contract a
USING loan_contract b USING loan_contract b
WHERE a.id > b.id AND a.signature2 = b.signature2; WHERE a.id > b.id AND a.signature2 = b.signature2;
DELETE FROM loan_payment a DELETE FROM loan_payment a
USING loan_payment b USING loan_payment b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
DELETE FROM collateral_claim a DELETE FROM collateral_claim a
USING collateral_claim b USING collateral_claim b
WHERE a.id > b.id AND a.signature = b.signature; WHERE a.id > b.id AND a.signature = b.signature;
"#; "#;
// Remove duplicate rows before unique indexes are created, otherwise stale // Remove duplicate rows before unique indexes are created, otherwise stale
@ -310,53 +310,53 @@ pub async fn setup_mempool() -> Result<()> {
client.batch_execute(dedupe).await?; client.batch_execute(dedupe).await?;
let indexes = r#" let indexes = r#"
CREATE INDEX IF NOT EXISTS transfer_pick_idx ON transfer (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS transfer_pick_idx ON transfer (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS transfer_sig_idx ON transfer (signature); CREATE INDEX IF NOT EXISTS transfer_sig_idx ON transfer (signature);
CREATE UNIQUE INDEX IF NOT EXISTS transfer_sig_unique_idx ON transfer (signature); CREATE UNIQUE INDEX IF NOT EXISTS transfer_sig_unique_idx ON transfer (signature);
CREATE INDEX IF NOT EXISTS token_pick_idx ON token (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS token_pick_idx ON token (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS token_sig_idx ON token (signature); CREATE INDEX IF NOT EXISTS token_sig_idx ON token (signature);
CREATE UNIQUE INDEX IF NOT EXISTS token_sig_unique_idx ON token (signature); CREATE UNIQUE INDEX IF NOT EXISTS token_sig_unique_idx ON token (signature);
CREATE INDEX IF NOT EXISTS issue_token_pick_idx ON issue_token (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS issue_token_pick_idx ON issue_token (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS issue_token_sig_idx ON issue_token (signature); CREATE INDEX IF NOT EXISTS issue_token_sig_idx ON issue_token (signature);
CREATE UNIQUE INDEX IF NOT EXISTS issue_token_sig_unique_idx ON issue_token (signature); CREATE UNIQUE INDEX IF NOT EXISTS issue_token_sig_unique_idx ON issue_token (signature);
CREATE INDEX IF NOT EXISTS burn_pick_idx ON burn (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS burn_pick_idx ON burn (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS burn_sig_idx ON burn (signature); CREATE INDEX IF NOT EXISTS burn_sig_idx ON burn (signature);
CREATE UNIQUE INDEX IF NOT EXISTS burn_sig_unique_idx ON burn (signature); CREATE UNIQUE INDEX IF NOT EXISTS burn_sig_unique_idx ON burn (signature);
CREATE INDEX IF NOT EXISTS nft_pick_idx ON nft (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS nft_pick_idx ON nft (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS nft_sig_idx ON nft (signature); CREATE INDEX IF NOT EXISTS nft_sig_idx ON nft (signature);
CREATE UNIQUE INDEX IF NOT EXISTS nft_sig_unique_idx ON nft (signature); CREATE UNIQUE INDEX IF NOT EXISTS nft_sig_unique_idx ON nft (signature);
CREATE INDEX IF NOT EXISTS marketing_pick_idx ON marketing (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS marketing_pick_idx ON marketing (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS marketing_sig_idx ON marketing (signature); CREATE INDEX IF NOT EXISTS marketing_sig_idx ON marketing (signature);
CREATE UNIQUE INDEX IF NOT EXISTS marketing_sig_unique_idx ON marketing (signature); CREATE UNIQUE INDEX IF NOT EXISTS marketing_sig_unique_idx ON marketing (signature);
CREATE INDEX IF NOT EXISTS vanity_address_pick_idx ON vanity_address (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS vanity_address_pick_idx ON vanity_address (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS vanity_address_sig_idx ON vanity_address (signature); CREATE INDEX IF NOT EXISTS vanity_address_sig_idx ON vanity_address (signature);
CREATE UNIQUE INDEX IF NOT EXISTS vanity_address_sig_unique_idx ON vanity_address (signature); CREATE UNIQUE INDEX IF NOT EXISTS vanity_address_sig_unique_idx ON vanity_address (signature);
CREATE INDEX IF NOT EXISTS swap_pick_idx ON swap (processed, GREATEST(fee1, fee2) DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS swap_pick_idx ON swap (processed, GREATEST(fee1, fee2) DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS swap_sig1_idx ON swap (signature1); CREATE INDEX IF NOT EXISTS swap_sig1_idx ON swap (signature1);
CREATE INDEX IF NOT EXISTS swap_sig2_idx ON swap (signature2); CREATE INDEX IF NOT EXISTS swap_sig2_idx ON swap (signature2);
CREATE UNIQUE INDEX IF NOT EXISTS swap_sig1_unique_idx ON swap (signature1); CREATE UNIQUE INDEX IF NOT EXISTS swap_sig1_unique_idx ON swap (signature1);
CREATE UNIQUE INDEX IF NOT EXISTS swap_sig2_unique_idx ON swap (signature2); CREATE UNIQUE INDEX IF NOT EXISTS swap_sig2_unique_idx ON swap (signature2);
CREATE INDEX IF NOT EXISTS loan_contract_pick_idx ON loan_contract (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS loan_contract_pick_idx ON loan_contract (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS loan_contract_sig1_idx ON loan_contract (signature1); CREATE INDEX IF NOT EXISTS loan_contract_sig1_idx ON loan_contract (signature1);
CREATE INDEX IF NOT EXISTS loan_contract_sig2_idx ON loan_contract (signature2); CREATE INDEX IF NOT EXISTS loan_contract_sig2_idx ON loan_contract (signature2);
CREATE UNIQUE INDEX IF NOT EXISTS loan_contract_sig1_unique_idx ON loan_contract (signature1); CREATE UNIQUE INDEX IF NOT EXISTS loan_contract_sig1_unique_idx ON loan_contract (signature1);
CREATE UNIQUE INDEX IF NOT EXISTS loan_contract_sig2_unique_idx ON loan_contract (signature2); CREATE UNIQUE INDEX IF NOT EXISTS loan_contract_sig2_unique_idx ON loan_contract (signature2);
CREATE INDEX IF NOT EXISTS loan_payment_pick_idx ON loan_payment (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS loan_payment_pick_idx ON loan_payment (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS loan_payment_sig_idx ON loan_payment (signature); CREATE INDEX IF NOT EXISTS loan_payment_sig_idx ON loan_payment (signature);
CREATE UNIQUE INDEX IF NOT EXISTS loan_payment_sig_unique_idx ON loan_payment (signature); CREATE UNIQUE INDEX IF NOT EXISTS loan_payment_sig_unique_idx ON loan_payment (signature);
CREATE INDEX IF NOT EXISTS collateral_claim_pick_idx ON collateral_claim (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS collateral_claim_pick_idx ON collateral_claim (processed, fee DESC, time ASC, id ASC);
CREATE INDEX IF NOT EXISTS collateral_claim_sig_idx ON collateral_claim (signature); CREATE INDEX IF NOT EXISTS collateral_claim_sig_idx ON collateral_claim (signature);
CREATE UNIQUE INDEX IF NOT EXISTS collateral_claim_sig_unique_idx ON collateral_claim (signature); CREATE UNIQUE INDEX IF NOT EXISTS collateral_claim_sig_unique_idx ON collateral_claim (signature);
"#; "#;
// Pick indexes speed up block selection; signature indexes enforce one // Pick indexes speed up block selection; signature indexes enforce one
// pending copy of each transaction. // pending copy of each transaction.
@ -364,35 +364,34 @@ pub async fn setup_mempool() -> Result<()> {
// Live mempool data is not restored across startup. // Live mempool data is not restored across startup.
clear_mempool().await?; clear_mempool().await?;
Ok(()) Ok(())
} }
pub async fn clear_mempool() -> Result<()> { pub async fn clear_mempool() -> Result<()> {
// Startup clears any leftover mempool rows so a node restart begins // Startup clears any leftover mempool rows so a node restart begins
// from a clean pending-transaction state. // from a clean pending-transaction state.
let client = DB.get().expect("DB not initialized"); let client = DB.get().expect("DB not initialized");
client client
.batch_execute( .batch_execute(
r#" r#"
TRUNCATE TABLE TRUNCATE TABLE
transfer, transfer,
token, token,
issue_token, issue_token,
burn, burn,
nft, nft,
marketing, marketing,
vanity_address, vanity_address,
swap, swap,
loan_contract, loan_contract,
loan_payment, loan_payment,
collateral_claim collateral_claim
RESTART IDENTITY; RESTART IDENTITY;
"#, "#,
) )
.await?; .await?;
Ok(()) Ok(())
} }

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,9 @@
pub mod averages; pub mod averages;
pub mod connections; pub mod connections;
pub mod enums; pub mod enums;
pub mod response_channels;
pub mod mempool; pub mod mempool;
pub mod network_mapping; pub mod network_mapping;
pub mod response_channels;
pub mod structs; pub mod structs;
pub mod torrent_status; pub mod torrent_status;
pub mod torrentmap; pub mod torrentmap;

View File

@ -1,30 +1,30 @@
use super::*; use super::*;
impl NodeInfo { impl NodeInfo {
pub async fn broadcast_node( pub async fn broadcast_node(
map: Arc<Mutex<Command>>, map: Arc<Mutex<Command>>,
edit: &SignedNodeEdit, edit: &SignedNodeEdit,
remote_ip: &str, remote_ip: &str,
edittype: NodeEditType, edittype: NodeEditType,
connections_key: &str, connections_key: &str,
) { ) {
// Re-broadcast signed node-map edits to connected peers while // Re-broadcast signed node-map edits to connected peers while
// skipping the source peer that already sent the update. // skipping the source peer that already sent the update.
let message_type = edittype.message_type(); let message_type = edittype.message_type();
let ip_bytes = ip_to_binary(&edit.ip); let ip_bytes = ip_to_binary(&edit.ip);
let address_bytes = match Wallet::short_address_to_bytes(&edit.address) { let address_bytes = match Wallet::short_address_to_bytes(&edit.address) {
Some(bytes) => bytes, Some(bytes) => bytes,
None => { None => {
warn!( warn!(
"[network_map] skipping broadcast for invalid short node address {}", "[network_map] skipping broadcast for invalid short node address {}",
edit.address edit.address
); );
return; return;
} }
}; };
let modified_by_bytes = Wallet::long_address_to_bytes(edit.modified_by.clone()); let modified_by_bytes = Wallet::long_address_to_bytes(edit.modified_by.clone());
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 connections_lock = CONNECTIONS.read().await; let connections_lock = CONNECTIONS.read().await;
let streams: Option<Vec<Arc<Mutex<TcpStream>>>> = connections_lock let streams: Option<Vec<Arc<Mutex<TcpStream>>>> = connections_lock
.as_ref() .as_ref()
@ -35,12 +35,12 @@ impl NodeInfo {
let (hashmap_key, _hashmap_tx, hashmap_rx) = reserve_entry(map.clone()).await; let (hashmap_key, _hashmap_tx, hashmap_rx) = reserve_entry(map.clone()).await;
let mut message: Vec<u8> = Vec::new(); let mut message: Vec<u8> = Vec::new();
message.push(message_type); message.push(message_type);
message.extend_from_slice(&hashmap_key); message.extend_from_slice(&hashmap_key);
message.extend_from_slice(&address_bytes); message.extend_from_slice(&address_bytes);
message.extend_from_slice(&ip_bytes); message.extend_from_slice(&ip_bytes);
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);
let peer_addr = { let peer_addr = {
let stream = unlocked_stream.lock().await; let stream = unlocked_stream.lock().await;
stream.peer_addr() stream.peer_addr()
@ -65,182 +65,182 @@ impl NodeInfo {
} }
} }
} }
None => { None => {
warn!("No active connections found."); warn!("No active connections found.");
} }
} }
} }
pub async fn add_address(params: AddAddressParams) -> RpcResponse { pub async fn add_address(params: AddAddressParams) -> RpcResponse {
let AddAddressParams { let AddAddressParams {
map, map,
mut edit, mut edit,
mut blocks_mined, mut blocks_mined,
remote_ip, remote_ip,
db, db,
wallet_key, wallet_key,
connections_key, connections_key,
} = params; } = params;
let current_timestamp = Utc::now().timestamp_millis() as u64; let current_timestamp = Utc::now().timestamp_millis() as u64;
let direct_peer_announcement = !remote_ip.is_empty() && edit.ip == remote_ip; let direct_peer_announcement = !remote_ip.is_empty() && edit.ip == remote_ip;
if !is_public_network_address(&edit.ip) { if !is_public_network_address(&edit.ip) {
return RpcResponse::Binary(b"Error: Invalid network address".to_vec()); return RpcResponse::Binary(b"Error: Invalid network address".to_vec());
} }
// Locally initiated edits are re-signed with the local wallet and // Locally initiated edits are re-signed with the local wallet and
// current timestamp so they can be propagated as fresh node events. // current timestamp so they can be propagated as fresh node events.
if edit.ip == remote_ip { if edit.ip == remote_ip {
edit.modified_timestamp = current_timestamp; edit.modified_timestamp = current_timestamp;
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
Ok(wallet) => wallet, Ok(wallet) => wallet,
Err(err) => { Err(err) => {
error!("Wallet decryption failed while adding node address: {err}"); error!("Wallet decryption failed while adding node address: {err}");
return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec()); return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec());
} }
}; };
edit.modified_by = wallet.saved.long_address; edit.modified_by = wallet.saved.long_address;
edit.modified_signature = edit.modified_signature =
Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key) Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key)
.await; .await;
} }
if !remote_ip.is_empty() { if !remote_ip.is_empty() {
blocks_mined = 0; blocks_mined = 0;
} }
let data = format!( let data = format!(
"{}{}{}{}", "{}{}{}{}",
edit.address, edit.ip, edit.modified_by, edit.modified_timestamp edit.address, edit.ip, edit.modified_by, edit.modified_timestamp
); );
let hashed_data = skein_256_hash_data(&data); let hashed_data = skein_256_hash_data(&data);
// Every add/delete edit is signed, so the network map accepts // Every add/delete edit is signed, so the network map accepts
// only node changes backed by a valid wallet signature. // only node changes backed by a valid wallet signature.
if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by) if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by)
.await .await
{ {
return RpcResponse::Binary(b"Error: Could not validate signature".to_vec()); return RpcResponse::Binary(b"Error: Could not validate signature".to_vec());
} }
let mut penalize_duplicate_ip = false; let mut penalize_duplicate_ip = false;
{ {
let mut address_map = ADDRESS_MAP.lock().await; let mut address_map = ADDRESS_MAP.lock().await;
// 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 { if get_height(&db) > 10000 {
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 valid_added_by = signer_node let valid_added_by = signer_node
.map(|node| { .map(|node| {
(current_timestamp - node.added_timestamp) >= 3600 (current_timestamp - node.added_timestamp) >= 3600
&& node.deleted_by.is_empty() && node.deleted_by.is_empty()
}) })
.unwrap_or(false); .unwrap_or(false);
if !valid_added_by { if !valid_added_by {
return RpcResponse::Binary( return RpcResponse::Binary(
b"Error: This address cannot add nodes. It must exist for at least 60 minutes and not be marked for deletion" b"Error: This address cannot add nodes. It must exist for at least 60 minutes and not be marked for deletion"
.to_vec(), .to_vec(),
); );
} }
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 {
return RpcResponse::Binary( return RpcResponse::Binary(
b"Error: This address cannot add nodes. It must mined 100 blocks before adding new nodes to the network" b"Error: This address cannot add nodes. It must mined 100 blocks before adding new nodes to the network"
.to_vec(), .to_vec(),
); );
} }
} }
let added_by_count_in_last_hour = address_map let added_by_count_in_last_hour = address_map
.values() .values()
.filter(|node| { .filter(|node| {
node.added_by == edit.modified_by node.added_by == edit.modified_by
&& (current_timestamp - node.added_timestamp) <= 3600 && (current_timestamp - node.added_timestamp) <= 3600
}) })
.count(); .count();
if added_by_count_in_last_hour >= 10 { if added_by_count_in_last_hour >= 10 {
return RpcResponse::Binary( return RpcResponse::Binary(
b"Error: Cannot add more than 10 nodes in 60 minutes".to_vec(), b"Error: Cannot add more than 10 nodes in 60 minutes".to_vec(),
); );
} }
// Existing deleted entries can be revived in place when the same // Existing deleted entries can be revived in place when the same
// address/IP pair is re-announced, otherwise the older record is // address/IP pair is re-announced, otherwise the older record is
// discarded and replaced. // discarded and replaced.
if let Some(existing_node) = address_map.get_mut(&edit.address) { if let Some(existing_node) = address_map.get_mut(&edit.address) {
if !existing_node.deleted_by.is_empty() { if !existing_node.deleted_by.is_empty() {
if existing_node.ip == edit.ip { if existing_node.ip == edit.ip {
existing_node.deleted_by = "".to_string(); existing_node.deleted_by = "".to_string();
existing_node.deleted_timestamp = 0_u64; existing_node.deleted_timestamp = 0_u64;
existing_node.deleted_block = 0_u32; existing_node.deleted_block = 0_u32;
existing_node.deleted_signature = "".to_string(); existing_node.deleted_signature = "".to_string();
return RpcResponse::Binary(b"Success".to_vec()); return RpcResponse::Binary(b"Success".to_vec());
} else { } else {
address_map.remove(&edit.address); address_map.remove(&edit.address);
} }
} else { } else {
if edit.modified_timestamp < existing_node.added_timestamp { if edit.modified_timestamp < existing_node.added_timestamp {
*existing_node = NodeInfo::new( *existing_node = NodeInfo::new(
edit.ip.clone(), edit.ip.clone(),
blocks_mined, blocks_mined,
edit.modified_by.clone(), edit.modified_by.clone(),
edit.modified_timestamp, edit.modified_timestamp,
edit.modified_signature.clone(), edit.modified_signature.clone(),
); );
} }
return RpcResponse::Binary(b"Success".to_vec()); return RpcResponse::Binary(b"Success".to_vec());
} }
} }
if let Some(existing_node) = address_map.values_mut().find(|node| node.ip == edit.ip) { if let Some(existing_node) = address_map.values_mut().find(|node| node.ip == edit.ip) {
if !existing_node.deleted_by.is_empty() { if !existing_node.deleted_by.is_empty() {
address_map.retain(|_, node| node.ip != edit.ip); address_map.retain(|_, node| node.ip != edit.ip);
} else if edit.ip != GENESIS_IP { } else if edit.ip != GENESIS_IP {
penalize_duplicate_ip = true; 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( NodeInfo::new(
edit.ip.clone(), edit.ip.clone(),
blocks_mined, blocks_mined,
edit.modified_by.clone(), edit.modified_by.clone(),
edit.modified_timestamp, edit.modified_timestamp,
edit.modified_signature.clone(), edit.modified_signature.clone(),
), ),
); );
} }
} }
if penalize_duplicate_ip { if penalize_duplicate_ip {
let now = Utc::now().timestamp() as u32; let now = Utc::now().timestamp() as u32;
let _ = update_ip_score( let _ = update_ip_score(
&remote_ip, &remote_ip,
"miner", "miner",
InfractionType::BadMinerIpUpdate, InfractionType::BadMinerIpUpdate,
now, now,
&db, &db,
&wallet_key, &wallet_key,
) )
.await; .await;
return RpcResponse::Binary(b"Error: Ip Already exists.".to_vec()); return RpcResponse::Binary(b"Error: Ip Already exists.".to_vec());
} }
Self::broadcast_node( Self::broadcast_node(
map.clone(), map.clone(),
&edit, &edit,
&remote_ip, &remote_ip,
NodeEditType::Add, NodeEditType::Add,
&connections_key, &connections_key,
) )
.await; .await;
@ -259,5 +259,4 @@ impl NodeInfo {
RpcResponse::Binary(b"Success".to_vec()) RpcResponse::Binary(b"Success".to_vec())
} }
}
}

View File

@ -1,5 +1,5 @@
use super::*; use super::*;
impl NodeInfo { impl NodeInfo {
pub fn ping(params: PingMonitorParams) { pub fn ping(params: PingMonitorParams) {
tokio::spawn(async move { tokio::spawn(async move {
@ -37,37 +37,37 @@ impl NodeInfo {
} }
let mut failures = 0; let mut failures = 0;
// Periodically ping the node and remove it after repeated // Periodically ping the node and remove it after repeated
// failures, then recursively reattach any inherited children. // failures, then recursively reattach any inherited children.
loop { loop {
sleep(Duration::from_secs(120)).await; sleep(Duration::from_secs(120)).await;
{ {
let monitors = PING_MONITORS.lock().await; let monitors = PING_MONITORS.lock().await;
// Stop this task if another monitor replaced it. // Stop this task if another monitor replaced it.
if !monitors if !monitors
.get(&task_addr) .get(&task_addr)
.map(|current| current == &task_signature) .map(|current| current == &task_signature)
.unwrap_or(false) .unwrap_or(false)
{ {
break; break;
} }
} }
{ {
let map_lock = ADDRESS_MAP.lock().await; let map_lock = ADDRESS_MAP.lock().await;
if let Some(node) = map_lock.get(&task_addr) { if let Some(node) = map_lock.get(&task_addr) {
// Stop monitoring stale node data after the map entry // Stop monitoring stale node data after the map entry
// has been replaced by a newer edit. // has been replaced by a newer edit.
if node.added_by != task_wallet || node.added_signature != task_signature { if node.added_by != task_wallet || node.added_signature != task_signature {
break; break;
} }
} else { } else {
break; break;
} }
} }
if let Some(unlocked_stream) = if let Some(unlocked_stream) =
Connection::get_stream_from_memory(&task_connections_key).await Connection::get_stream_from_memory(&task_connections_key).await
{ {
@ -77,45 +77,45 @@ impl NodeInfo {
// Liveness uses a normal block-height RPC and waits for the // Liveness uses a normal block-height RPC and waits for the
// reserved reply channel to receive any response. // reserved reply channel to receive any response.
let mut message = vec![command]; let mut message = vec![command];
message.extend_from_slice(&ping_key); message.extend_from_slice(&ping_key);
RpcResponse::send_raw(&unlocked_stream, Some(&task_connections_key), &message) RpcResponse::send_raw(&unlocked_stream, Some(&task_connections_key), &message)
.await; .await;
let response = timeout(Duration::from_secs(10), async { let response = timeout(Duration::from_secs(10), async {
let mut rx = ping_rx.lock().await; let mut rx = ping_rx.lock().await;
rx.recv().await rx.recv().await
}) })
.await; .await;
match response { match response {
Ok(Some(_buffer)) => { Ok(Some(_buffer)) => {
failures = 0; failures = 0;
} }
_ => { _ => {
failures += 1; failures += 1;
warn!("[network_map] ping failure: address={task_addr} ip={task_ip} failures={failures}"); warn!("[network_map] ping failure: address={task_addr} ip={task_ip} failures={failures}");
if failures >= 3 { if failures >= 3 {
warn!("[network_map] deleting node after ping failures: address={task_addr} ip={task_ip} responsible_by={task_wallet}"); warn!("[network_map] deleting node after ping failures: address={task_addr} ip={task_ip} responsible_by={task_wallet}");
let _ = Self::delete_address(DeleteAddressParams { let _ = Self::delete_address(DeleteAddressParams {
map: map.clone(), map: map.clone(),
edit: SignedNodeEdit { edit: SignedNodeEdit {
address: task_addr.clone(), address: task_addr.clone(),
ip: task_ip.clone(), ip: task_ip.clone(),
modified_by: task_wallet.clone(), modified_by: task_wallet.clone(),
modified_timestamp: added_timestamp, modified_timestamp: added_timestamp,
modified_signature: task_signature.clone(), modified_signature: task_signature.clone(),
}, },
remote_ip: task_remote_ip.clone(), remote_ip: task_remote_ip.clone(),
db: task_db.clone(), db: task_db.clone(),
wallet_key: task_wallet_key.clone(), wallet_key: task_wallet_key.clone(),
connections_key: task_connections_key.clone(), connections_key: task_connections_key.clone(),
}) })
.await; .await;
break; break;
} }
} }
} }
} else { } else {
// The direct socket disappeared before this monitor fired. // The direct socket disappeared before this monitor fired.
// Connection cleanup is owned by the connection manager, so // Connection cleanup is owned by the connection manager, so
@ -123,151 +123,151 @@ impl NodeInfo {
break; break;
} }
} }
Self::release_ping_monitor(&task_addr, &task_signature).await; Self::release_ping_monitor(&task_addr, &task_signature).await;
}); });
} }
pub async fn delete_address(params: DeleteAddressParams) -> RpcResponse { pub async fn delete_address(params: DeleteAddressParams) -> RpcResponse {
let DeleteAddressParams { let DeleteAddressParams {
map, map,
mut edit, mut edit,
remote_ip, remote_ip,
db, db,
wallet_key, wallet_key,
connections_key, connections_key,
} = params; } = params;
let current_timestamp = Utc::now().timestamp_millis() as u64; let current_timestamp = Utc::now().timestamp_millis() as u64;
// Locally initiated deletions are re-signed with fresh metadata // Locally initiated deletions are re-signed with fresh metadata
// before they are applied and broadcast. // before they are applied and broadcast.
if remote_ip.is_empty() { if remote_ip.is_empty() {
edit.modified_timestamp = current_timestamp; edit.modified_timestamp = current_timestamp;
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
Ok(wallet) => wallet, Ok(wallet) => wallet,
Err(err) => { Err(err) => {
error!("Wallet decryption failed while deleting node address: {err}"); error!("Wallet decryption failed while deleting node address: {err}");
return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec()); return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec());
} }
}; };
edit.modified_by = wallet.saved.long_address; edit.modified_by = wallet.saved.long_address;
edit.modified_signature = edit.modified_signature =
Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key) Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key)
.await; .await;
} }
let data = format!( let data = format!(
"{}{}{}{}", "{}{}{}{}",
edit.address, edit.ip, edit.modified_by, edit.modified_timestamp edit.address, edit.ip, edit.modified_by, edit.modified_timestamp
); );
let hashed_data = skein_256_hash_data(&data); let hashed_data = skein_256_hash_data(&data);
if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by) if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by)
.await .await
{ {
return RpcResponse::Binary(b"Error: Could not validate signature".to_vec()); return RpcResponse::Binary(b"Error: Could not validate signature".to_vec());
} }
{ {
let mut address_map = ADDRESS_MAP.lock().await; let mut address_map = ADDRESS_MAP.lock().await;
if get_height(&db) > 10_000 { if get_height(&db) > 10_000 {
// Mature chains only allow established miners to remove nodes. // Mature chains only allow established miners to remove nodes.
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 valid_added_by = signer_node let valid_added_by = signer_node
.map(|node| { .map(|node| {
(current_timestamp - node.added_timestamp) >= 3600 (current_timestamp - node.added_timestamp) >= 3600
&& node.deleted_by.is_empty() && node.deleted_by.is_empty()
}) })
.unwrap_or(false); .unwrap_or(false);
if !valid_added_by { if !valid_added_by {
return RpcResponse::Binary( return RpcResponse::Binary(
b"Error: Address must exist for 60m and not be deleted".to_vec(), b"Error: Address must exist for 60m and not be deleted".to_vec(),
); );
} }
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 {
return RpcResponse::Binary( return RpcResponse::Binary(
b"Error: Must mine 100 blocks to remove nodes".to_vec(), b"Error: Must mine 100 blocks to remove nodes".to_vec(),
); );
} }
} }
let deleted_count_last_hour = address_map let deleted_count_last_hour = address_map
.values() .values()
.filter(|node| { .filter(|node| {
node.deleted_by == edit.modified_by node.deleted_by == edit.modified_by
&& (current_timestamp - node.deleted_timestamp) <= 3600 && (current_timestamp - node.deleted_timestamp) <= 3600
}) })
.count(); .count();
// Rate limit delete events per signer to prevent churn in the // Rate limit delete events per signer to prevent churn in the
// shared network map. // shared network map.
if deleted_count_last_hour >= 10 { if deleted_count_last_hour >= 10 {
return RpcResponse::Binary(b"Error: Max 10 deletions in 60m".to_vec()); return RpcResponse::Binary(b"Error: Max 10 deletions in 60m".to_vec());
} }
if let Some(existing_node) = address_map.get_mut(&edit.address) { if let Some(existing_node) = address_map.get_mut(&edit.address) {
if !existing_node.deleted_by.is_empty() { if !existing_node.deleted_by.is_empty() {
return RpcResponse::Binary( return RpcResponse::Binary(
b"Error: This address has already been deleted".to_vec(), b"Error: This address has already been deleted".to_vec(),
); );
} }
// Deletion is recorded as metadata rather than immediately // Deletion is recorded as metadata rather than immediately
// removing the node, preserving historical validation context. // removing the node, preserving historical validation context.
existing_node.deleted_by = edit.modified_by.clone(); existing_node.deleted_by = edit.modified_by.clone();
existing_node.deleted_timestamp = current_timestamp; existing_node.deleted_timestamp = current_timestamp;
existing_node.deleted_block = get_height(&db) + 1; existing_node.deleted_block = get_height(&db) + 1;
existing_node.deleted_signature = edit.modified_signature.clone(); existing_node.deleted_signature = edit.modified_signature.clone();
info!( info!(
"[network_map] node marked deleted: address={} ip={} deleted_by={} timestamp={} deleted_block={}", "[network_map] node marked deleted: address={} ip={} deleted_by={} timestamp={} deleted_block={}",
edit.address, edit.address,
edit.ip, edit.ip,
edit.modified_by, edit.modified_by,
current_timestamp, current_timestamp,
existing_node.deleted_block existing_node.deleted_block
); );
} else { } else {
return RpcResponse::Binary(b"Error: Address not found".to_vec()); return RpcResponse::Binary(b"Error: Address not found".to_vec());
} }
} }
// Stop any ping task owned by the deleted node record. // Stop any ping task owned by the deleted node record.
Self::release_ping_monitor(&edit.address, &edit.modified_signature).await; Self::release_ping_monitor(&edit.address, &edit.modified_signature).await;
// Deletions propagate to peers and also tear down any live // Deletions propagate to peers and also tear down any live
// outgoing connection so bootstrap can recover a replacement. // outgoing connection so bootstrap can recover a replacement.
Self::broadcast_node( Self::broadcast_node(
map.clone(), map.clone(),
&edit, &edit,
&remote_ip, &remote_ip,
NodeEditType::Delete, NodeEditType::Delete,
&connections_key, &connections_key,
) )
.await; .await;
if let Some(port) = CONNECTIONS if let Some(port) = CONNECTIONS
.read() .read()
.await .await
.as_ref() .as_ref()
.and_then(|conn| conn.find_outgoing_port(&edit.ip)) .and_then(|conn| conn.find_outgoing_port(&edit.ip))
{ {
let mut writer = CONNECTIONS.write().await; let mut writer = CONNECTIONS.write().await;
if let Some(conn) = writer.as_mut() { if let Some(conn) = writer.as_mut() {
// Drop the live outgoing socket after marking the node deleted. // Drop the live outgoing socket after marking the node deleted.
conn.drop_connection(ConnectionType::Outgoing, edit.ip.clone(), port); conn.drop_connection(ConnectionType::Outgoing, edit.ip.clone(), port);
info!( info!(
"[connection_manager] dropped dead outgoing connection: {}:{}", "[connection_manager] dropped dead outgoing connection: {}:{}",
edit.ip, port edit.ip, port
); );
} }
drop(writer); drop(writer);
let live_connection = { let live_connection = {
let guard = CONNECTIONS.read().await; let guard = CONNECTIONS.read().await;
guard.as_ref().and_then(|conn| { guard.as_ref().and_then(|conn| {
@ -287,15 +287,14 @@ impl NodeInfo {
wallet_key: wallet_key.clone(), wallet_key: wallet_key.clone(),
db: db.clone(), db: db.clone(),
map: map.clone(), map: map.clone(),
first: false, first: false,
}; };
spawn_reconnect_bootstrap(bootstrap_params); spawn_reconnect_bootstrap(bootstrap_params);
} else { } else {
warn!("[reconnect] No live stream found to bootstrap from"); warn!("[reconnect] No live stream found to bootstrap from");
} }
} }
RpcResponse::Binary(b"Success: Node marked as deleted".to_vec()) RpcResponse::Binary(b"Success: Node marked as deleted".to_vec())
} }
}
}

View File

@ -1,50 +1,50 @@
use super::*; use super::*;
impl NodeInfo { impl NodeInfo {
pub async fn increment_mined(address: &str) { pub async fn increment_mined(address: &str) {
let mut map = ADDRESS_MAP.lock().await; let mut map = ADDRESS_MAP.lock().await;
if let Some(node_info) = map.get_mut(address) { if let Some(node_info) = map.get_mut(address) {
// Counts are capped at u8-safe policy maximum used by node rules. // Counts are capped at u8-safe policy maximum used by node rules.
if node_info.blocks_mined < 250 { if node_info.blocks_mined < 250 {
node_info.blocks_mined += 1; node_info.blocks_mined += 1;
} }
} }
} }
pub async fn decrement_mined(address: &str) { pub async fn decrement_mined(address: &str) {
let mut map = ADDRESS_MAP.lock().await; let mut map = ADDRESS_MAP.lock().await;
if let Some(node_info) = map.get_mut(address) { if let Some(node_info) = map.get_mut(address) {
// Rollback can undo mined credit, but never below zero. // Rollback can undo mined credit, but never below zero.
if node_info.blocks_mined > 0 { if node_info.blocks_mined > 0 {
node_info.blocks_mined -= 1; node_info.blocks_mined -= 1;
} }
} }
} }
pub async fn get_mined_count(address: &str) -> u8 { pub async fn get_mined_count(address: &str) -> u8 {
let map = ADDRESS_MAP.lock().await; let map = ADDRESS_MAP.lock().await;
if let Some(node_info) = map.get(address) { if let Some(node_info) = map.get(address) {
node_info.blocks_mined node_info.blocks_mined
} else { } else {
0 0
} }
} }
pub async fn set_deleted_block_from_mapping(address: &str, deleted_block: u32) { pub async fn set_deleted_block_from_mapping(address: &str, deleted_block: u32) {
let mut map = ADDRESS_MAP.lock().await; let mut map = ADDRESS_MAP.lock().await;
if let Some(node_info) = map.get_mut(address) { if let Some(node_info) = map.get_mut(address) {
// The deletion height is filled in once the chain knows the block // The deletion height is filled in once the chain knows the block
// where the delete action becomes active. // where the delete action becomes active.
node_info.deleted_block = deleted_block; node_info.deleted_block = deleted_block;
} }
} }
pub async fn rebuild_mined_counts_from_chain(db: &Db) -> Result<(), String> { pub async fn rebuild_mined_counts_from_chain(db: &Db) -> Result<(), String> {
// Recompute node mined counts directly from saved block headers // Recompute node mined counts directly from saved block headers
// so startup and recovery can rebuild memory-only state. // so startup and recovery can rebuild memory-only state.
let current_height = get_height(db); let current_height = get_height(db);
let mut mined_counts: HashMap<String, u8> = HashMap::new(); let mut mined_counts: HashMap<String, u8> = HashMap::new();
let start_height = if current_height > 0 { 1 } else { 0 }; let start_height = if current_height > 0 { 1 } else { 0 };
for block_number in start_height..=current_height { for block_number in start_height..=current_height {
let header = load_block_header(block_number).await?; let header = load_block_header(block_number).await?;
@ -52,26 +52,25 @@ impl NodeInfo {
let entry = mined_counts.entry(miner).or_insert(0); let entry = mined_counts.entry(miner).or_insert(0);
// Keep the rebuilt value under the same cap as live increments. // Keep the rebuilt value under the same cap as live increments.
if *entry < 250 { if *entry < 250 {
*entry += 1; *entry += 1;
} }
} }
{ {
let mut map = ADDRESS_MAP.lock().await; let mut map = ADDRESS_MAP.lock().await;
for node_info in map.values_mut() { for node_info in map.values_mut() {
// Clear memory-only counts before applying the rebuilt chain // Clear memory-only counts before applying the rebuilt chain
// totals so removed or inactive miners do not keep stale data. // totals so removed or inactive miners do not keep stale data.
node_info.blocks_mined = 0; node_info.blocks_mined = 0;
} }
for (address, mined_count) in mined_counts { for (address, mined_count) in mined_counts {
if let Some(node_info) = map.get_mut(&address) { if let Some(node_info) = map.get_mut(&address) {
node_info.blocks_mined = mined_count; node_info.blocks_mined = mined_count;
} }
} }
} }
Ok(()) Ok(())
} }
}
}

View File

@ -1,83 +1,83 @@
use crate::common::binary_conversions::ip_to_binary; use crate::common::binary_conversions::ip_to_binary;
use crate::common::network_startup::is_public_network_address; use crate::common::network_startup::is_public_network_address;
use crate::common::skein::skein_256_hash_data; use crate::common::skein::skein_256_hash_data;
use crate::common::types::GENESIS_IP; use crate::common::types::GENESIS_IP;
use crate::decode; use crate::decode;
use crate::lazy_static; use crate::lazy_static;
use crate::log::{error, info, warn}; use crate::log::{error, info, warn};
use crate::records::block_height::get_block_height::get_height; use crate::records::block_height::get_block_height::get_height;
use crate::records::ip_score::enums::InfractionType; use crate::records::ip_score::enums::InfractionType;
use crate::records::ip_score::score::update_ip_score; use crate::records::ip_score::score::update_ip_score;
use crate::records::memory::connections::{spawn_reconnect_bootstrap, CONNECTIONS}; use crate::records::memory::connections::{spawn_reconnect_bootstrap, CONNECTIONS};
use crate::records::memory::enums::ConnectionType; use crate::records::memory::enums::ConnectionType;
use crate::records::memory::response_channels::{reserve_entry, Command};
use crate::records::memory::network_mapping::enums::NodeEditType; use crate::records::memory::network_mapping::enums::NodeEditType;
use crate::records::memory::network_mapping::structs::{ use crate::records::memory::network_mapping::structs::{
AddAddressParams, DeleteAddressParams, PingMonitorParams, SignedNodeEdit, NODE_RECORD_BYTES, AddAddressParams, DeleteAddressParams, PingMonitorParams, SignedNodeEdit, NODE_RECORD_BYTES,
}; };
use crate::records::memory::structs::Connection; use crate::records::memory::response_channels::{reserve_entry, Command};
use crate::records::unpack_block::unpack_header::load_block_header; use crate::records::memory::structs::Connection;
use crate::rpc::client::handshake_processing::BootstrapParams; use crate::records::unpack_block::unpack_header::load_block_header;
use crate::rpc::command_maps::RPC_BLOCK_HEIGHT; use crate::rpc::client::handshake_processing::BootstrapParams;
use crate::rpc::responses::RpcResponse; use crate::rpc::command_maps::RPC_BLOCK_HEIGHT;
use crate::sled::Db; use crate::rpc::responses::RpcResponse;
use crate::sleep; use crate::sled::Db;
use crate::timeout; use crate::sleep;
use crate::wallets::structures::Wallet; use crate::timeout;
use crate::Arc; use crate::wallets::structures::Wallet;
use crate::Duration; use crate::Arc;
use crate::HashMap; use crate::Duration;
use crate::Mutex; use crate::HashMap;
use crate::TcpStream; use crate::Mutex;
use crate::Utc; use crate::TcpStream;
use crate::Utc;
lazy_static! {
static ref ADDRESS_MAP: Mutex<HashMap<String, NodeInfo>> = Mutex::new(HashMap::new()); lazy_static! {
static ref PING_MONITORS: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new()); static ref ADDRESS_MAP: Mutex<HashMap<String, NodeInfo>> = Mutex::new(HashMap::new());
} static ref PING_MONITORS: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
}
#[derive(Debug)]
pub struct NodeInfo { #[derive(Debug)]
ip: String, pub struct NodeInfo {
blocks_mined: u8, ip: String,
added_by: String, blocks_mined: u8,
added_timestamp: u64, added_by: String,
added_signature: String, added_timestamp: u64,
deleted_by: String, added_signature: String,
deleted_timestamp: u64, deleted_by: String,
deleted_block: u32, deleted_timestamp: u64,
deleted_signature: String, deleted_block: u32,
} deleted_signature: String,
}
impl NodeInfo {
async fn release_ping_monitor(address: &str, signature: &str) { impl NodeInfo {
let mut monitors = PING_MONITORS.lock().await; async fn release_ping_monitor(address: &str, signature: &str) {
if monitors let mut monitors = PING_MONITORS.lock().await;
.get(address) if monitors
.map(|current| current == signature) .get(address)
.unwrap_or(false) .map(|current| current == signature)
{ .unwrap_or(false)
monitors.remove(address); {
} monitors.remove(address);
} }
}
fn new(
ip: String, fn new(
blocks_mined: u8, ip: String,
added_by: String, blocks_mined: u8,
added_timestamp: u64, added_by: String,
added_signature: String, added_timestamp: u64,
) -> Self { added_signature: String,
NodeInfo { ) -> Self {
ip, NodeInfo {
blocks_mined, ip,
added_by, blocks_mined,
added_timestamp, added_by,
added_signature, added_timestamp,
deleted_by: "".to_string(), added_signature,
deleted_timestamp: 0_u64, deleted_by: "".to_string(),
deleted_block: 0_u32, deleted_timestamp: 0_u64,
deleted_signature: "".to_string(), deleted_block: 0_u32,
deleted_signature: "".to_string(),
} }
} }
} }
@ -88,4 +88,3 @@ pub mod enums;
mod mined_counts; mod mined_counts;
mod queries; mod queries;
pub mod structs; pub mod structs;

View File

@ -11,24 +11,23 @@ impl NodeInfo {
false false
} }
pub async fn find_address_by_ip(ip: &str) -> Option<String> { pub async fn find_address_by_ip(ip: &str) -> Option<String> {
let map = ADDRESS_MAP.lock().await; let map = ADDRESS_MAP.lock().await;
for (address, node_info) in map.iter() { for (address, node_info) in map.iter() {
// Reverse lookup is needed when a peer is identified by socket IP // Reverse lookup is needed when a peer is identified by socket IP
// but the node map is keyed by wallet short address. // but the node map is keyed by wallet short address.
if node_info.ip == ip { if node_info.ip == ip {
return Some(address.clone()); return Some(address.clone());
} }
} }
None None
} }
pub async fn find_ip_by_address(address: &str) -> Option<String> { pub async fn find_ip_by_address(address: &str) -> Option<String> {
let map = ADDRESS_MAP.lock().await; let map = ADDRESS_MAP.lock().await;
map.get(address).map(|node_info| node_info.ip.clone()) map.get(address).map(|node_info| node_info.ip.clone())
} }
pub async fn active_node_ips() -> Vec<String> { pub async fn active_node_ips() -> Vec<String> {
let map = ADDRESS_MAP.lock().await; let map = ADDRESS_MAP.lock().await;
map.values() map.values()
@ -37,8 +36,8 @@ impl NodeInfo {
.filter(|node_info| node_info.deleted_by.is_empty()) .filter(|node_info| node_info.deleted_by.is_empty())
.map(|node_info| node_info.ip.clone()) .map(|node_info| node_info.ip.clone())
.collect() .collect()
} }
pub async fn get_deleted_addresses() -> Vec<u8> { pub async fn get_deleted_addresses() -> Vec<u8> {
let map = ADDRESS_MAP.lock().await; let map = ADDRESS_MAP.lock().await;
map.iter() map.iter()
@ -46,25 +45,25 @@ impl NodeInfo {
// The RPC response is a packed list of deleted short-address // The RPC response is a packed list of deleted short-address
// bytes, so invalid address keys are skipped. // bytes, so invalid address keys are skipped.
if node_info.deleted_timestamp > 0 { if node_info.deleted_timestamp > 0 {
Wallet::short_address_to_bytes(address) Wallet::short_address_to_bytes(address)
} else { } else {
None None
} }
}) })
.flatten() .flatten()
.collect() .collect()
} }
pub async fn request_valid_nodes() -> RpcResponse { pub async fn request_valid_nodes() -> RpcResponse {
// Serialize the in-memory node map into the fixed binary layout // Serialize the in-memory node map into the fixed binary layout
// used by peer bootstrap and node-list synchronization. // used by peer bootstrap and node-list synchronization.
let map = ADDRESS_MAP.lock().await; let map = ADDRESS_MAP.lock().await;
let mut data: Vec<u8> = Vec::with_capacity(map.len() * NODE_RECORD_BYTES); let mut data: Vec<u8> = Vec::with_capacity(map.len() * NODE_RECORD_BYTES);
for (address, node_info) in map.iter() { for (address, node_info) in map.iter() {
let address_bytes = match Wallet::short_address_to_bytes(address) { let address_bytes = match Wallet::short_address_to_bytes(address) {
Some(bytes) => bytes, Some(bytes) => bytes,
None => continue, None => continue,
}; };
let ip_bytes = ip_to_binary(&node_info.ip); let ip_bytes = ip_to_binary(&node_info.ip);
let blocks_mined = node_info.blocks_mined; let blocks_mined = node_info.blocks_mined;
@ -74,58 +73,58 @@ impl NodeInfo {
// Empty deletion fields serialize as zero-filled fixed-width values // Empty deletion fields serialize as zero-filled fixed-width values
// so every node record stays the same size on the wire. // so every node record stays the same size on the wire.
let deleted_by_bytes = if node_info.deleted_by.is_empty() { let deleted_by_bytes = if node_info.deleted_by.is_empty() {
vec![0u8; Wallet::ADDRESS_BYTES_LENGTH] vec![0u8; Wallet::ADDRESS_BYTES_LENGTH]
} else { } else {
Wallet::long_address_to_bytes(node_info.deleted_by.to_string()) Wallet::long_address_to_bytes(node_info.deleted_by.to_string())
}; };
let deleted_timestamp_bytes = node_info.deleted_timestamp.to_le_bytes(); let deleted_timestamp_bytes = node_info.deleted_timestamp.to_le_bytes();
let deleted_block_bytes = node_info.deleted_block.to_le_bytes(); let deleted_block_bytes = node_info.deleted_block.to_le_bytes();
let deleted_signature_bytes = if node_info.deleted_signature.is_empty() { let deleted_signature_bytes = if node_info.deleted_signature.is_empty() {
vec![0u8; Wallet::SIGNATURE_LENGTH] vec![0u8; Wallet::SIGNATURE_LENGTH]
} else { } else {
decode(node_info.deleted_signature.clone()).unwrap() decode(node_info.deleted_signature.clone()).unwrap()
}; };
let added_signature_bytes = decode(node_info.added_signature.clone()).unwrap(); let added_signature_bytes = decode(node_info.added_signature.clone()).unwrap();
// Field order here must match the parser used by node-list // Field order here must match the parser used by node-list
// synchronization. // synchronization.
data.extend_from_slice(&address_bytes); data.extend_from_slice(&address_bytes);
data.extend_from_slice(&ip_bytes); data.extend_from_slice(&ip_bytes);
data.push(blocks_mined); data.push(blocks_mined);
data.extend_from_slice(&added_by_bytes); data.extend_from_slice(&added_by_bytes);
data.extend_from_slice(&added_timestamp_bytes); data.extend_from_slice(&added_timestamp_bytes);
data.extend_from_slice(&added_signature_bytes); data.extend_from_slice(&added_signature_bytes);
data.extend_from_slice(&deleted_by_bytes); data.extend_from_slice(&deleted_by_bytes);
data.extend_from_slice(&deleted_timestamp_bytes); data.extend_from_slice(&deleted_timestamp_bytes);
data.extend_from_slice(&deleted_block_bytes); data.extend_from_slice(&deleted_block_bytes);
data.extend_from_slice(&deleted_signature_bytes); data.extend_from_slice(&deleted_signature_bytes);
} }
RpcResponse::Binary(data) RpcResponse::Binary(data)
} }
pub async fn added_signature( pub async fn added_signature(
address: &str, address: &str,
ip: &str, ip: &str,
current_timestamp: u64, current_timestamp: u64,
wallet_key: &str, wallet_key: &str,
) -> String { ) -> String {
// Node edits are signed over address, IP, signer, and timestamp // Node edits are signed over address, IP, signer, and timestamp
// so peers can independently verify the advertised change. // so peers can independently verify the advertised change.
let wallet = match Wallet::try_obtain_wallet(wallet_key.to_string(), None).await { let wallet = match Wallet::try_obtain_wallet(wallet_key.to_string(), None).await {
Ok(wallet) => wallet, Ok(wallet) => wallet,
Err(err) => { Err(err) => {
error!("Wallet decryption failed while signing node edit: {err}"); error!("Wallet decryption failed while signing node edit: {err}");
return String::new(); return String::new();
} }
}; };
let added_by = wallet.saved.long_address; let added_by = wallet.saved.long_address;
let private_key = wallet.saved.private_key; let private_key = wallet.saved.private_key;
let data = format!("{address}{ip}{added_by}{current_timestamp}"); let data = format!("{address}{ip}{added_by}{current_timestamp}");
let hashed_data = skein_256_hash_data(&data); let hashed_data = skein_256_hash_data(&data);
Wallet::sign_transaction(&hashed_data, &private_key).await Wallet::sign_transaction(&hashed_data, &private_key).await
} }
} }

View File

@ -21,7 +21,7 @@ fn random_3_byte_number() -> [u8; 3] {
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()[1..4].try_into().unwrap()
} }

View File

@ -80,13 +80,21 @@ pub async fn process_lender(
pending_effects.append_tree_if_key_exists( pending_effects.append_tree_if_key_exists(
"nfts", "nfts",
"nft_history", "nft_history",
transaction.unsigned_loan_contract.collateral.as_bytes().to_vec(), transaction
.unsigned_loan_contract
.collateral
.as_bytes()
.to_vec(),
txhash_bytes.clone(), txhash_bytes.clone(),
); );
pending_effects.append_tree_if_key_exists( pending_effects.append_tree_if_key_exists(
"nfts", "nfts",
"nft_history", "nft_history",
transaction.unsigned_loan_contract.loan_coin.as_bytes().to_vec(), transaction
.unsigned_loan_contract
.loan_coin
.as_bytes()
.to_vec(),
txhash_bytes, txhash_bytes,
); );
Ok(binary_data) Ok(binary_data)

View File

@ -60,8 +60,16 @@ pub async fn process_nft(
BalanceOperand::Addition, BalanceOperand::Addition,
); );
pending_effects.set_tree("nfts", nft_save_name.as_bytes().to_vec(), b"1".to_vec()); pending_effects.set_tree("nfts", nft_save_name.as_bytes().to_vec(), b"1".to_vec());
pending_effects.set_tree("nft_origins", nft_save_name.as_bytes().to_vec(), txhash_bytes.clone()); pending_effects.set_tree(
pending_effects.append_tree("nft_history", nft_save_name.into_bytes(), txhash_bytes.clone()); "nft_origins",
nft_save_name.as_bytes().to_vec(),
txhash_bytes.clone(),
);
pending_effects.append_tree(
"nft_history",
nft_save_name.into_bytes(),
txhash_bytes.clone(),
);
} }
} else { } else {
let nft_save_name = transaction.unsigned_create_nft.nft_name.clone(); let nft_save_name = transaction.unsigned_create_nft.nft_name.clone();
@ -72,8 +80,16 @@ pub async fn process_nft(
BalanceOperand::Addition, BalanceOperand::Addition,
); );
pending_effects.set_tree("nfts", nft_save_name.as_bytes().to_vec(), b"1".to_vec()); pending_effects.set_tree("nfts", nft_save_name.as_bytes().to_vec(), b"1".to_vec());
pending_effects.set_tree("nft_origins", nft_save_name.as_bytes().to_vec(), txhash_bytes.clone()); pending_effects.set_tree(
pending_effects.append_tree("nft_history", nft_save_name.into_bytes(), txhash_bytes.clone()); "nft_origins",
nft_save_name.as_bytes().to_vec(),
txhash_bytes.clone(),
);
pending_effects.append_tree(
"nft_history",
nft_save_name.into_bytes(),
txhash_bytes.clone(),
);
} }
// Record the txid location so RPC lookups can resolve the saved // Record the txid location so RPC lookups can resolve the saved

View File

@ -373,12 +373,8 @@ fn apply_effect(db: &Db, effect: &PendingEffect) -> Result<AppliedEffect, String
contract_id, contract_id,
payment, payment,
} => { } => {
let previous = append_tree_value( let previous =
db, append_tree_value(db, "contract_payments", contract_id, &payment.to_le_bytes())?;
"contract_payments",
contract_id,
&payment.to_le_bytes(),
)?;
Ok(AppliedEffect::TreeMutation { Ok(AppliedEffect::TreeMutation {
tree: "contract_payments", tree: "contract_payments",
key: contract_id.clone(), key: contract_id.clone(),
@ -400,8 +396,10 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> {
amount, amount,
coin, coin,
operand, operand,
} => balance_sheet_operation_with_db(db, &address, amount, &coin, operand.inverse().as_str()) } => {
.map_err(|err| format!("Failed to roll back balance effect: {err}")), balance_sheet_operation_with_db(db, &address, amount, &coin, operand.inverse().as_str())
.map_err(|err| format!("Failed to roll back balance effect: {err}"))
}
AppliedEffect::TreeMutation { AppliedEffect::TreeMutation {
tree, tree,
key, key,
@ -428,7 +426,12 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> {
previous_rollback, previous_rollback,
} => { } => {
restore_vanity_mapping(db, &owner_address, previous_vanity)?; restore_vanity_mapping(db, &owner_address, previous_vanity)?;
restore_tree_value(db, WALLET_VANITY_ROLLBACK_TREE, &rollback_key, previous_rollback) restore_tree_value(
db,
WALLET_VANITY_ROLLBACK_TREE,
&rollback_key,
previous_rollback,
)
} }
AppliedEffect::Noop => Ok(()), AppliedEffect::Noop => Ok(()),
} }
@ -581,8 +584,8 @@ fn apply_vanity_effect(
) -> Result<AppliedEffect, String> { ) -> Result<AppliedEffect, String> {
let previous_vanity = get_registered_vanity_for_owner(db, owner_address) let previous_vanity = get_registered_vanity_for_owner(db, owner_address)
.map_err(|err| format!("Could not read existing vanity mapping: {err}"))?; .map_err(|err| format!("Could not read existing vanity mapping: {err}"))?;
let rollback_key = let rollback_key = crate::decode(txhash)
crate::decode(txhash).map_err(|_| "Could not decode vanity transaction hash".to_string())?; .map_err(|_| "Could not decode vanity transaction hash".to_string())?;
let rollback_tree = db let rollback_tree = db
.open_tree(WALLET_VANITY_ROLLBACK_TREE) .open_tree(WALLET_VANITY_ROLLBACK_TREE)
.map_err(|err| format!("Could not open vanity rollback tree: {err}"))?; .map_err(|err| format!("Could not open vanity rollback tree: {err}"))?;

View File

@ -1,6 +1,11 @@
use crate::common::check_genesis::genesis_checkup; use crate::common::check_genesis::genesis_checkup;
use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::miner::flag::{is_mining_running, is_normal_mode, is_reorganizing_mode, is_syncing_mode}; use crate::decode;
use crate::fs;
use crate::log::{error, info};
use crate::miner::flag::{
is_mining_running, is_normal_mode, is_reorganizing_mode, is_syncing_mode,
};
use crate::orphans::snapshot_check::{snapshot_height, update_snapshot}; use crate::orphans::snapshot_check::{snapshot_height, update_snapshot};
use crate::records::block_height::get_block_height::get_height; use crate::records::block_height::get_block_height::get_height;
use crate::records::block_height::increase_block_height::increase_height; use crate::records::block_height::increase_block_height::increase_height;
@ -8,8 +13,8 @@ use crate::records::memory::averages::{calculate_averages, update_block_data};
use crate::records::memory::mempool::{ use crate::records::memory::mempool::{
apply_selected_transaction_math, mark_processed_by_signatures, apply_selected_transaction_math, mark_processed_by_signatures,
mark_selected_transactions_processed, restore_processed_by_signatures, mark_selected_transactions_processed, restore_processed_by_signatures,
restore_selected_transactions_processed, select_transactions_for_block, spawn_processed_cleanup, restore_selected_transactions_processed, select_transactions_for_block,
stream_selected_transaction_originals, spawn_processed_cleanup, stream_selected_transaction_originals,
}; };
use crate::records::memory::network_mapping::NodeInfo; use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::torrent_status::prune_torrent_statuses_through_height; use crate::records::memory::torrent_status::prune_torrent_statuses_through_height;
@ -25,10 +30,7 @@ use crate::records::unpack_block::unpack_header::load_block_header;
use crate::torrent::create_metadata::{broadcast_new_torrent_to_peers, metadata_from_file}; use crate::torrent::create_metadata::{broadcast_new_torrent_to_peers, metadata_from_file};
use crate::torrent::torrenting_system::save_torrent::prune_staged_torrents; use crate::torrent::torrenting_system::save_torrent::prune_staged_torrents;
use crate::torrent::torrenting_system::torrent_cache::prune_recent_torrents; use crate::torrent::torrenting_system::torrent_cache::prune_recent_torrents;
use crate::log::{error, info};
use crate::Arc; use crate::Arc;
use crate::decode;
use crate::fs;
use crate::Mutex; use crate::Mutex;
use crate::PathBuf; use crate::PathBuf;
use crate::Utc; use crate::Utc;
@ -337,7 +339,9 @@ async fn save_binary_data_with_mempool_stream(
let _ = update_snapshot(db, next_number).await; let _ = update_snapshot(db, next_number).await;
if let Some(snapshot_height) = snapshot_height(db).await { if let Some(snapshot_height) = snapshot_height(db).await {
if let Err(err) = finalize_rewards_through_height(db, snapshot_height).await { if let Err(err) = finalize_rewards_through_height(db, snapshot_height).await {
error!("Failed to finalize rewards through snapshot height {snapshot_height}: {err}"); error!(
"Failed to finalize rewards through snapshot height {snapshot_height}: {err}"
);
} }
prune_recent_torrents(snapshot_height).await; prune_recent_torrents(snapshot_height).await;
prune_torrent_statuses_through_height(snapshot_height).await; prune_torrent_statuses_through_height(snapshot_height).await;
@ -465,7 +469,9 @@ async fn save_binary_data(params: SaveBinaryDataParams<'_>) -> Result<(), String
let _ = update_snapshot(db, next_number).await; let _ = update_snapshot(db, next_number).await;
if let Some(snapshot_height) = snapshot_height(db).await { if let Some(snapshot_height) = snapshot_height(db).await {
if let Err(err) = finalize_rewards_through_height(db, snapshot_height).await { if let Err(err) = finalize_rewards_through_height(db, snapshot_height).await {
error!("Failed to finalize rewards through snapshot height {snapshot_height}: {err}"); error!(
"Failed to finalize rewards through snapshot height {snapshot_height}: {err}"
);
} }
prune_recent_torrents(snapshot_height).await; prune_recent_torrents(snapshot_height).await;
prune_torrent_statuses_through_height(snapshot_height).await; prune_torrent_statuses_through_height(snapshot_height).await;

View File

@ -23,8 +23,7 @@ pub async fn process_token(
// Serialize the token-creation transaction and compute its txid // Serialize the token-creation transaction and compute its txid
// before applying the balance-sheet and token-registry updates. // before applying the balance-sheet and token-registry updates.
let txhash = transaction.unsigned_create_token.hash().await; let txhash = transaction.unsigned_create_token.hash().await;
let txhash_bytes = let txhash_bytes = decode(&txhash).map_err(|e| format!("Error decoding token txhash: {e}"))?;
decode(&txhash).map_err(|e| format!("Error decoding token txhash: {e}"))?;
let transaction_bytes = match transaction.to_bytes().await { let transaction_bytes = match transaction.to_bytes().await {
Ok(bytes) => bytes, Ok(bytes) => bytes,
Err(e) => return Err(e.to_string()), Err(e) => return Err(e.to_string()),
@ -63,7 +62,11 @@ pub async fn process_token(
let origin_key = transaction.unsigned_create_token.ticker.clone(); let origin_key = transaction.unsigned_create_token.ticker.clone();
let origin_value = txhash.as_bytes(); let origin_value = txhash.as_bytes();
pending_effects.set_tree("token_origins", origin_key.into_bytes(), origin_value.to_vec()); pending_effects.set_tree(
"token_origins",
origin_key.into_bytes(),
origin_value.to_vec(),
);
pending_effects.append_tree( pending_effects.append_tree(
"token_history", "token_history",
transaction.unsigned_create_token.ticker.as_bytes().to_vec(), transaction.unsigned_create_token.ticker.as_bytes().to_vec(),

View File

@ -1,26 +1,27 @@
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::sled::Db; use crate::sled::Db;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
pub(super) const WALLET_REGISTRY_TREE: &str = "wallet_registry"; pub(super) const WALLET_REGISTRY_TREE: &str = "wallet_registry";
pub(super) const WALLET_VANITY_ADDRESS_TREE: &str = "wallet_vanity_address"; pub(super) const WALLET_VANITY_ADDRESS_TREE: &str = "wallet_vanity_address";
pub(super) const WALLET_VANITY_OWNER_TREE: &str = "wallet_vanity_owner"; pub(super) const WALLET_VANITY_OWNER_TREE: &str = "wallet_vanity_owner";
pub(crate) const WALLET_VANITY_ROLLBACK_TREE: &str = "wallet_vanity_rollback"; pub(crate) const WALLET_VANITY_ROLLBACK_TREE: &str = "wallet_vanity_rollback";
mod helpers; mod helpers;
mod mappings; mod mappings;
mod storage; mod storage;
pub mod structs; pub mod structs;
pub use helpers::{get_registered_pubkey, is_registered_short_address, short_address_exists}; pub use helpers::{get_registered_pubkey, is_registered_short_address, short_address_exists};
pub use mappings::{ pub use mappings::{
get_registered_vanity_for_owner, list_registered_wallets, require_canonical_registered_short_address, get_registered_vanity_for_owner, list_registered_wallets,
resolve_canonical_registered_short_address, resolve_local_input_short_address, require_canonical_registered_short_address, resolve_canonical_registered_short_address,
resolve_owner_from_vanity_address, resolve_pubkey_from_short_address, take_previous_vanity_for_txid, resolve_local_input_short_address, resolve_owner_from_vanity_address,
}; resolve_pubkey_from_short_address, take_previous_vanity_for_txid,
pub use storage::{ };
register_or_update_vanity_address, register_short_address, remove_registered_vanity_for_owner, pub use storage::{
store_previous_vanity_for_txid, register_or_update_vanity_address, register_short_address, remove_registered_vanity_for_owner,
}; store_previous_vanity_for_txid,
pub use structs::{VanityRegistrationResult, WalletRegistrationResult}; };
pub use structs::{VanityRegistrationResult, WalletRegistrationResult};

View File

@ -1,15 +1,15 @@
use crate::common::network_startup::get_listen_ip; use crate::common::network_startup::get_listen_ip;
use crate::io;
use crate::rpc::client::handshake_message::prepare_handshake_message; use crate::rpc::client::handshake_message::prepare_handshake_message;
use crate::rpc::client::handshake_processing::process_handshake_response; use crate::rpc::client::handshake_processing::process_handshake_response;
use crate::rpc::client::structs::{Connect, Handshake}; use crate::rpc::client::structs::{Connect, Handshake};
use crate::rpc::command_maps::{MAX_RPC_REPLY_BYTES, RPC_REPLY}; use crate::rpc::command_maps::{MAX_RPC_REPLY_BYTES, RPC_REPLY};
use crate::rpc::handshake_constants::HANDSHAKE_RESPONSE_BYTES; use crate::rpc::handshake_constants::HANDSHAKE_RESPONSE_BYTES;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::{AsyncReadExt, AsyncWriteExt};
use crate::io;
use crate::IpAddr; use crate::IpAddr;
use crate::SocketAddr; use crate::SocketAddr;
use crate::TcpStream; use crate::TcpStream;
use crate::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpSocket; use tokio::net::TcpSocket;
pub async fn connect_and_handshake(params: Connect) -> Result<(), Box<dyn std::error::Error>> { pub async fn connect_and_handshake(params: Connect) -> Result<(), Box<dyn std::error::Error>> {

View File

@ -1,12 +1,12 @@
use crate::common::binary_conversions::ip_port_to_binary; use crate::common::binary_conversions::ip_port_to_binary;
use crate::common::network_startup::get_ip_and_port; use crate::common::network_startup::get_ip_and_port;
use crate::common::skein::skein_256_hash_data; use crate::common::skein::skein_256_hash_data;
use crate::decode;
use crate::io;
use crate::rpc::commands::time::request_time; use crate::rpc::commands::time::request_time;
use crate::rpc::handshake_constants::HANDSHAKE_REQUEST_BYTES; use crate::rpc::handshake_constants::HANDSHAKE_REQUEST_BYTES;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::decode;
use crate::io;
pub async fn prepare_handshake_message(wallet: &Wallet, message: &str) -> io::Result<Vec<u8>> { pub async fn prepare_handshake_message(wallet: &Wallet, message: &str) -> io::Result<Vec<u8>> {
// Client handshakes are assembled from the signed message, wallet identity, timestamp, // Client handshakes are assembled from the signed message, wallet identity, timestamp,

View File

@ -1,40 +1,46 @@
use crate::common::binary_conversions::binary_to_ip_port; use crate::common::binary_conversions::binary_to_ip_port;
use crate::common::check_genesis::genesis_checkup; use crate::common::check_genesis::genesis_checkup;
use crate::common::network_startup::get_ip_and_port;
use crate::common::skein::skein_256_hash_data; use crate::common::skein::skein_256_hash_data;
use crate::config::SETTINGS; use crate::config::SETTINGS;
use crate::miner::flag::{clear_mining_stop_request, request_mining_stop, set_mining_state, set_node_mode, MiningState, NodeMode}; use crate::encode;
use crate::io;
use crate::log::{error, info, warn};
use crate::miner::flag::{
clear_mining_stop_request, request_mining_stop, set_mining_state, set_node_mode, MiningState,
NodeMode,
};
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::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::connections::{set_reconnect_context, CONNECTIONS}; use crate::records::memory::connections::{set_reconnect_context, CONNECTIONS};
use crate::records::memory::enums::{ClientType, ConnectionType}; use crate::records::memory::enums::{ClientType, ConnectionType};
use crate::records::memory::response_channels::{reserve_entry, Command};
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::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::register_wallet::register_connected_wallet;
use crate::rpc::client::structs::{Connect, Handshake}; use crate::rpc::client::structs::{Connect, Handshake};
use crate::rpc::client::syncing::node_syncing; use crate::rpc::client::syncing::node_syncing;
use crate::rpc::client::register_wallet::register_connected_wallet;
use crate::rpc::client::wallet_registry_sync::sync_wallet_registry; use crate::rpc::client::wallet_registry_sync::sync_wallet_registry;
use crate::rpc::command_maps::RPC_RANDOM_NODE; use crate::rpc::command_maps::RPC_RANDOM_NODE;
use crate::rpc::handshake_constants::{HANDSHAKE_ADDRESS_OFFSET, HANDSHAKE_MESSAGE_BYTES, HANDSHAKE_RESPONSE_BYTES, HANDSHAKE_SIGNATURE_OFFSET,}; use crate::rpc::handshake_constants::{
HANDSHAKE_ADDRESS_OFFSET, HANDSHAKE_MESSAGE_BYTES, HANDSHAKE_RESPONSE_BYTES,
HANDSHAKE_SIGNATURE_OFFSET,
};
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::rpc::server::rpc_command_loop::start_loop; use crate::rpc::server::rpc_command_loop::start_loop;
use crate::common::network_startup::get_ip_and_port; use crate::sled::Db;
use crate::startup::network_broadcast::announce_self_to_network; use crate::startup::network_broadcast::announce_self_to_network;
use crate::startup::remote_height::request_remote_height; use crate::startup::remote_height::request_remote_height;
use crate::timeout;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::log::{error, info, warn};
use crate::sled::Db;
use crate::Arc; use crate::Arc;
use crate::Duration; use crate::Duration;
use crate::encode;
use crate::io;
use crate::Mutex; use crate::Mutex;
use crate::SocketAddr; use crate::SocketAddr;
use crate::TcpStream; use crate::TcpStream;
use crate::timeout;
#[derive(Clone)] #[derive(Clone)]
pub struct BootstrapParams { pub struct BootstrapParams {
@ -104,7 +110,10 @@ pub async fn bootstrap_peer_discovery(mut params: BootstrapParams) -> Result<(),
continue; continue;
} }
if Connection::get_stream_from_memory(&addr_string).await.is_some() { if Connection::get_stream_from_memory(&addr_string)
.await
.is_some()
{
no_progress_count += 1; no_progress_count += 1;
continue; continue;
} }

View File

@ -1,15 +1,15 @@
use crate::common::skein::skein_256_hash_bytes; use crate::common::skein::skein_256_hash_bytes;
use crate::decode;
use crate::log::warn;
use crate::records::memory::response_channels::{reserve_entry, Command}; use crate::records::memory::response_channels::{reserve_entry, Command};
use crate::rpc::command_maps::RPC_REGISTER_WALLET; use crate::rpc::command_maps::RPC_REGISTER_WALLET;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::timeout;
use crate::wallets::structures::Wallet; use crate::wallets::structures::Wallet;
use crate::log::warn;
use crate::Arc; use crate::Arc;
use crate::decode;
use crate::Duration; use crate::Duration;
use crate::Mutex; use crate::Mutex;
use crate::TcpStream; use crate::TcpStream;
use crate::timeout;
pub async fn register_connected_wallet( pub async fn register_connected_wallet(
stream: Arc<Mutex<TcpStream>>, stream: Arc<Mutex<TcpStream>>,

View File

@ -1,19 +1,21 @@
use crate::common::check_genesis::genesis_checkup; use crate::common::check_genesis::genesis_checkup;
use crate::io;
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;
use crate::records::memory::response_channels::Command; use crate::records::memory::response_channels::Command;
use crate::torrent::structs::Torrent;
use crate::torrent::torrenting_system::torrent_requests::{handle_response_and_save_torrent, send_request_torrent_message};
use crate::log::{error, info, warn};
use crate::sled::Db; use crate::sled::Db;
use crate::timeout;
use crate::torrent::structs::Torrent;
use crate::torrent::torrenting_system::torrent_requests::{
handle_response_and_save_torrent, send_request_torrent_message,
};
use crate::Arc; use crate::Arc;
use crate::Duration; use crate::Duration;
use crate::io;
use crate::Mutex; use crate::Mutex;
use crate::TcpStream; use crate::TcpStream;
use crate::timeout;
pub async fn node_syncing( pub async fn node_syncing(
stream: Arc<Mutex<TcpStream>>, stream: Arc<Mutex<TcpStream>>,

View File

@ -1,16 +1,16 @@
use crate::log::warn;
use crate::records::memory::response_channels::{reserve_entry, Command}; use crate::records::memory::response_channels::{reserve_entry, Command};
use crate::records::wallet_registry::{register_short_address, WalletRegistrationResult}; use crate::records::wallet_registry::{register_short_address, WalletRegistrationResult};
use crate::rpc::commands::wallet_registry_sync::WALLET_REGISTRY_RECORD_BYTES;
use crate::rpc::command_maps::RPC_WALLET_REGISTRY_SYNC; use crate::rpc::command_maps::RPC_WALLET_REGISTRY_SYNC;
use crate::rpc::commands::wallet_registry_sync::WALLET_REGISTRY_RECORD_BYTES;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::log::warn;
use crate::sled::Db; use crate::sled::Db;
use crate::timeout;
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;
use crate::TcpStream; use crate::TcpStream;
use crate::timeout;
pub async fn sync_wallet_registry( pub async fn sync_wallet_registry(
stream: Arc<Mutex<TcpStream>>, stream: Arc<Mutex<TcpStream>>,

View File

@ -1,10 +1,10 @@
use crate::records::memory::response_channels::Command;
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::network_mapping::structs::{AddAddressParams, SignedNodeEdit}; use crate::records::memory::network_mapping::structs::{AddAddressParams, SignedNodeEdit};
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::response_channels::Command;
use crate::rpc::read_bytes_from_stream; use crate::rpc::read_bytes_from_stream;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::wallets::structures::Wallet;
use crate::sled::Db; use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::Arc; use crate::Arc;
use crate::Mutex; use crate::Mutex;
use crate::TcpStream; use crate::TcpStream;
@ -21,9 +21,11 @@ pub async fn add_network_node(
let (uid, _) = let (uid, _) =
read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone()) read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone())
.await?; .await?;
let address_bytes = let address_bytes = read_bytes_from_stream::read_short_address_from_stream(
read_bytes_from_stream::read_short_address_from_stream(connections_key, stream_locked.clone()) connections_key,
.await?; stream_locked.clone(),
)
.await?;
let address = Wallet::bytes_to_short_address(&address_bytes) let address = Wallet::bytes_to_short_address(&address_bytes)
.ok_or_else(|| "error: Invalid short address bytes".to_string())?; .ok_or_else(|| "error: Invalid short address bytes".to_string())?;
let ip = let ip =

View File

@ -1,8 +1,8 @@
use crate::records::balance_sheet::get_wallet_balance::get_balance; use crate::records::balance_sheet::get_wallet_balance::get_balance;
use crate::records::wallet_registry::resolve_canonical_registered_short_address; use crate::records::wallet_registry::resolve_canonical_registered_short_address;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::wallets::structures::Wallet;
use crate::sled::Db; use crate::sled::Db;
use crate::wallets::structures::Wallet;
pub async fn lookup_wallet_coin(db: &Db, address: String, coin: String) -> RpcResponse { pub async fn lookup_wallet_coin(db: &Db, address: String, coin: String) -> RpcResponse {
// Return the saved confirmed balance for a specific address/asset pair. // Return the saved confirmed balance for a specific address/asset pair.

View File

@ -1,14 +1,14 @@
use crate::common::nft_assets::nft_asset_parts; use crate::common::nft_assets::nft_asset_parts;
use crate::log::error;
use crate::read_dir;
use crate::records::balance_sheet::pathing::{address_root_path, asset_name_from_relative_path}; use crate::records::balance_sheet::pathing::{address_root_path, asset_name_from_relative_path};
use crate::records::wallet_registry::resolve_canonical_registered_short_address; use crate::records::wallet_registry::resolve_canonical_registered_short_address;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::wallets::structures::Wallet;
use crate::log::error;
use crate::sled::Db; use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::AsyncReadExt; use crate::AsyncReadExt;
use crate::File; use crate::File;
use crate::Path; use crate::Path;
use crate::read_dir;
pub async fn get_token_balances(db: &Db, address: String) -> RpcResponse { pub async fn get_token_balances(db: &Db, address: String) -> RpcResponse {
// Walk the hierarchical balance-sheet tree for one address and emit // Walk the hierarchical balance-sheet tree for one address and emit

View File

@ -9,9 +9,7 @@ pub async fn request_block(db: &Db, hash: &str) -> RpcResponse {
let hash_bytes = match decode(hash) { let hash_bytes = match decode(hash) {
Ok(bytes) => bytes, Ok(bytes) => bytes,
Err(err) => { Err(err) => {
return RpcResponse::Binary( return RpcResponse::Binary(format!("error: Failed to decode hash: {err}").into_bytes())
format!("error: Failed to decode hash: {err}").into_bytes(),
)
} }
}; };
@ -57,8 +55,6 @@ pub async fn request_block(db: &Db, hash: &str) -> RpcResponse {
format!("error: Failed to convert block to bytes: {err}").into_bytes(), format!("error: Failed to convert block to bytes: {err}").into_bytes(),
), ),
}, },
Err(err) => { Err(err) => RpcResponse::Binary(format!("error: Failed to load block: {err}").into_bytes()),
RpcResponse::Binary(format!("error: Failed to load block: {err}").into_bytes())
}
} }
} }

View File

@ -1,6 +1,6 @@
use crate::decode;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::sled::Db; use crate::sled::Db;
use crate::decode;
pub async fn lookup_by_hash(db: &Db, hash: &str) -> RpcResponse { pub async fn lookup_by_hash(db: &Db, hash: &str) -> RpcResponse {
// Resolve the block hash through the block-hash index and then fetch // Resolve the block hash through the block-hash index and then fetch

View File

@ -1,6 +1,6 @@
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::wallets::structures::Wallet;
use crate::sled::Db; use crate::sled::Db;
use crate::wallets::structures::Wallet;
pub async fn block_peer(db: &Db, ip: String, signature: String, wallet_key: String) -> RpcResponse { pub async fn block_peer(db: &Db, ip: String, signature: String, wallet_key: String) -> RpcResponse {
// Peer blocking is restricted to the local node owner, proven by a // Peer blocking is restricted to the local node owner, proven by a

View File

@ -1,12 +1,12 @@
use crate::blocks::collateral::CollateralClaimTransaction; use crate::blocks::collateral::CollateralClaimTransaction;
use crate::blocks::loan_payment::ContractPaymentTransaction; use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::encode;
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::wallets::structures::Wallet;
use crate::sled::Db; use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::{DateTime, Datelike, Local, TimeZone, Utc}; use crate::{DateTime, Datelike, Local, TimeZone, Utc};
use crate::encode;
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.
@ -103,8 +103,7 @@ async fn collect_contract_activity(
let mut collateral_claim: Option<CollateralClaimTransaction> = None; let mut collateral_claim: Option<CollateralClaimTransaction> = None;
for entry in tree.iter() { for entry in tree.iter() {
let (txid_bytes, _) = let (txid_bytes, _) = entry.map_err(|e| format!("error: Failed to read txid tree: {e}"))?;
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;
@ -243,8 +242,7 @@ pub async fn contract_details(hash: Vec<u8>, db: &Db) -> RpcResponse {
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.
let Some(address) = Wallet::normalize_to_short_address(&address) let Some(address) = Wallet::normalize_to_short_address(&address) else {
else {
return RpcResponse::Binary(b"error: Invalid wallet address".to_vec()); return RpcResponse::Binary(b"error: Invalid wallet address".to_vec());
}; };

View File

@ -1,10 +1,10 @@
use crate::records::memory::response_channels::Command;
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::network_mapping::structs::{DeleteAddressParams, SignedNodeEdit}; use crate::records::memory::network_mapping::structs::{DeleteAddressParams, SignedNodeEdit};
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::response_channels::Command;
use crate::rpc::read_bytes_from_stream; use crate::rpc::read_bytes_from_stream;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::wallets::structures::Wallet;
use crate::sled::Db; use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::Arc; use crate::Arc;
use crate::Mutex; use crate::Mutex;
use crate::TcpStream; use crate::TcpStream;
@ -21,9 +21,11 @@ pub async fn delete_network_node(
let (uid, _) = let (uid, _) =
read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone()) read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone())
.await?; .await?;
let address_bytes = let address_bytes = read_bytes_from_stream::read_short_address_from_stream(
read_bytes_from_stream::read_short_address_from_stream(connections_key, stream_locked.clone()) connections_key,
.await?; stream_locked.clone(),
)
.await?;
let address = Wallet::bytes_to_short_address(&address_bytes) let address = Wallet::bytes_to_short_address(&address_bytes)
.ok_or_else(|| "error: Invalid short address bytes".to_string())?; .ok_or_else(|| "error: Invalid short address bytes".to_string())?;
let ip = let ip =

View File

@ -1,7 +1,7 @@
// The rpc commands module groups the request handlers that run after a client handshake succeeds. // The rpc commands module groups the request handlers that run after a client handshake succeeds.
pub mod add_network_node;
pub mod address_coin_lookup; pub mod address_coin_lookup;
pub mod address_complete_balance_sheet; pub mod address_complete_balance_sheet;
pub mod add_network_node;
pub mod bad_rpc_call; pub mod bad_rpc_call;
pub mod block_by_hash; pub mod block_by_hash;
pub mod block_by_height; pub mod block_by_height;
@ -11,10 +11,10 @@ pub mod block_headers;
pub mod block_height; pub mod block_height;
pub mod block_peer_ip; pub mod block_peer_ip;
pub mod contract; pub mod contract;
pub mod difficulty;
pub mod delete_network_node; pub mod delete_network_node;
pub mod latest_block; pub mod difficulty;
pub mod largest_tx_fee; pub mod largest_tx_fee;
pub mod latest_block;
pub mod memory_by_signature; pub mod memory_by_signature;
pub mod network_info; pub mod network_info;
pub mod nft_list; pub mod nft_list;
@ -31,14 +31,14 @@ pub mod torrent;
pub mod torrent_by_block; pub mod torrent_by_block;
pub mod torrent_candidates; pub mod torrent_candidates;
pub mod transaction_by_txid; pub mod transaction_by_txid;
pub mod transactions_by_address;
pub mod tx_count; pub mod tx_count;
pub mod tx_count_from_mempool; pub mod tx_count_from_mempool;
pub mod tx_submit; pub mod tx_submit;
pub mod transactions_by_address;
pub mod unblock_peer_ip; pub mod unblock_peer_ip;
pub mod validate_address; pub mod validate_address;
pub mod validate_torrent;
pub mod validate_message; pub mod validate_message;
pub mod validate_torrent;
pub mod wallet_register; pub mod wallet_register;
pub mod wallet_registry_sync; pub mod wallet_registry_sync;
pub mod wallet_vanity_lookup; pub mod wallet_vanity_lookup;

View File

@ -1,7 +1,7 @@
use crate::common::nft_assets::nft_asset_parts; use crate::common::nft_assets::nft_asset_parts;
use crate::encode;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::sled::Db; use crate::sled::Db;
use crate::encode;
pub async fn get_nfts(db: &Db) -> RpcResponse { pub async fn get_nfts(db: &Db) -> RpcResponse {
// Serialize every NFT asset as origin hash, padded asset name, and // Serialize every NFT asset as origin hash, padded asset name, and

View File

@ -6,14 +6,16 @@ use crate::blocks::swap::SwapTransaction;
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
use crate::common::binary_conversions::binary_to_string; use crate::common::binary_conversions::binary_to_string;
use crate::common::nft_assets::{nft_asset_name, nft_asset_parts}; use crate::common::nft_assets::{nft_asset_name, nft_asset_parts};
use crate::records::balance_sheet::pathing::{balance_asset_segments, balance_root_path};
use crate::records::balance_sheet::tokens_to_lower::strip_spaces_and_lowercase;
use crate::rpc::commands::transaction_by_txid::{request_transaction_by_txid, request_transaction_by_txid_with_block};
use crate::rpc::responses::RpcResponse;
use crate::wallets::structures::Wallet;
use crate::sled::Db;
use crate::decode; use crate::decode;
use crate::fs; use crate::fs;
use crate::records::balance_sheet::pathing::{balance_asset_segments, balance_root_path};
use crate::records::balance_sheet::tokens_to_lower::strip_spaces_and_lowercase;
use crate::rpc::commands::transaction_by_txid::{
request_transaction_by_txid, request_transaction_by_txid_with_block,
};
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
const ACTION_CREATE: u8 = 1; const ACTION_CREATE: u8 = 1;
const ACTION_TRANSFER: u8 = 2; const ACTION_TRANSFER: u8 = 2;
@ -102,7 +104,8 @@ async fn find_nft_origin(
let txid_tree = db.open_tree("txid").ok()?; let txid_tree = db.open_tree("txid").ok()?;
for entry in txid_tree.iter() { for entry in txid_tree.iter() {
let (txid_bytes, _) = entry.ok()?; let (txid_bytes, _) = entry.ok()?;
let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await; let RpcResponse::Binary(tx_bytes) =
request_transaction_by_txid(db, txid_bytes.to_vec()).await;
if tx_bytes.is_empty() || tx_bytes[0] != 4 { if tx_bytes.is_empty() || tx_bytes[0] != 4 {
continue; continue;
@ -173,7 +176,8 @@ async fn find_current_holder(asset_name: &str) -> String {
async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Option<Vec<u8>> { async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Option<Vec<u8>> {
// Expand a raw NFT history txid into a fixed-width history entry that // Expand a raw NFT history txid into a fixed-width history entry that
// captures the action, involved wallets, and any received asset details. // captures the action, involved wallets, and any received asset details.
let RpcResponse::Binary(response) = request_transaction_by_txid_with_block(db, txid_bytes.to_vec()).await; let RpcResponse::Binary(response) =
request_transaction_by_txid_with_block(db, txid_bytes.to_vec()).await;
if response.len() < 5 { if response.len() < 5 {
return None; return None;
@ -267,7 +271,8 @@ async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Op
.await .await
.ok()?; .ok()?;
let contract_hash = decode(&tx.unsigned_contract_payment.contract_hash).ok()?; let contract_hash = decode(&tx.unsigned_contract_payment.contract_hash).ok()?;
let RpcResponse::Binary(contract_bytes) = request_transaction_by_txid(db, contract_hash).await; let RpcResponse::Binary(contract_bytes) =
request_transaction_by_txid(db, contract_hash).await;
let contract = LoanContractTransaction::from_bytes(7, &contract_bytes) let contract = LoanContractTransaction::from_bytes(7, &contract_bytes)
.await .await
.ok()?; .ok()?;
@ -285,7 +290,8 @@ async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Op
.await .await
.ok()?; .ok()?;
let contract_hash = decode(&tx.unsigned_collateral_claim.contract_hash).ok()?; let contract_hash = decode(&tx.unsigned_collateral_claim.contract_hash).ok()?;
let RpcResponse::Binary(contract_bytes) = request_transaction_by_txid(db, contract_hash).await; let RpcResponse::Binary(contract_bytes) =
request_transaction_by_txid(db, contract_hash).await;
let contract = LoanContractTransaction::from_bytes(7, &contract_bytes) let contract = LoanContractTransaction::from_bytes(7, &contract_bytes)
.await .await
.ok()?; .ok()?;
@ -339,7 +345,8 @@ pub async fn lookup_nft_details(db: &Db, nft_name: String, item_number: u32) ->
return RpcResponse::Binary(b"error: NFT genesis not found".to_vec()); return RpcResponse::Binary(b"error: NFT genesis not found".to_vec());
}; };
let RpcResponse::Binary(genesis_response) = request_transaction_by_txid(db, genesis_bytes.clone()).await; let RpcResponse::Binary(genesis_response) =
request_transaction_by_txid(db, genesis_bytes.clone()).await;
if genesis_response.is_empty() || genesis_response[0] != 4 { if genesis_response.is_empty() || genesis_response[0] != 4 {
return RpcResponse::Binary(b"error: NFT genesis transaction not found".to_vec()); return RpcResponse::Binary(b"error: NFT genesis transaction not found".to_vec());
} }

View File

@ -1,5 +1,7 @@
use crate::common::check_genesis::genesis_checkup; use crate::common::check_genesis::genesis_checkup;
use crate::common::skein::skein_128_hash_bytes; use crate::common::skein::skein_128_hash_bytes;
use crate::lazy_static;
use crate::log::{error, warn};
use crate::miner::flag::{is_reorganizing_mode, is_syncing_mode}; use crate::miner::flag::{is_reorganizing_mode, is_syncing_mode};
use crate::orphans::structs::OrphanCheckup2; use crate::orphans::structs::OrphanCheckup2;
use crate::orphans::sync_check::sync_checkup; use crate::orphans::sync_check::sync_checkup;
@ -8,17 +10,19 @@ use crate::records::memory::response_channels::Command;
use crate::rpc::read_bytes_from_stream; use crate::rpc::read_bytes_from_stream;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::rpc::server::flood_protection::MAX_TORRENT_METADATA_BYTES; use crate::rpc::server::flood_protection::MAX_TORRENT_METADATA_BYTES;
use crate::startup::remote_height::request_remote_height;
use crate::torrent::structs::Torrent;
use crate::torrent::create_metadata::broadcast_new_torrent_to_peers;
use crate::torrent::torrenting_system::torrent_requests::{setup_download_for_torrent, stage_and_verify_torrent};
use crate::torrent::torrenting_system::torrent_cache::{has_recent_torrent, remember_recent_torrent};
use crate::log::{error, warn};
use crate::sled::Db; use crate::sled::Db;
use crate::startup::remote_height::request_remote_height;
use crate::torrent::create_metadata::broadcast_new_torrent_to_peers;
use crate::torrent::structs::Torrent;
use crate::torrent::torrenting_system::torrent_cache::{
has_recent_torrent, remember_recent_torrent,
};
use crate::torrent::torrenting_system::torrent_requests::{
setup_download_for_torrent, stage_and_verify_torrent,
};
use crate::Arc; use crate::Arc;
use crate::AtomicBool; use crate::AtomicBool;
use crate::AtomicOrdering; use crate::AtomicOrdering;
use crate::lazy_static;
use crate::Mutex; use crate::Mutex;
lazy_static! { lazy_static! {
@ -156,8 +160,7 @@ pub async fn torrent_submission(
return TorrentSubmissionOutcome::Rejected(RpcResponse::Binary(msg)); return TorrentSubmissionOutcome::Rejected(RpcResponse::Binary(msg));
} }
match stage_and_verify_torrent(height, db, torrent, wallet_key, process_now).await match stage_and_verify_torrent(height, db, torrent, wallet_key, process_now).await {
{
Ok(stage_result) => { Ok(stage_result) => {
let _ = remember_recent_torrent(&torrent_hash, height).await; let _ = remember_recent_torrent(&torrent_hash, height).await;
if let Some((torrent, staged_path)) = stage_result { if let Some((torrent, staged_path)) = stage_result {
@ -273,7 +276,8 @@ pub async fn receive_torrent(
) -> Result<(u32, RpcResponse), String> { ) -> Result<(u32, RpcResponse), String> {
let (uid, _) = let (uid, _) =
read_bytes_from_stream::read_uid_from_stream(connections_key, stream.clone()).await?; read_bytes_from_stream::read_uid_from_stream(connections_key, stream.clone()).await?;
let size = read_bytes_from_stream::read_u32_from_stream(connections_key, stream.clone()).await?; let size =
read_bytes_from_stream::read_u32_from_stream(connections_key, stream.clone()).await?;
// The size includes the block-height field, so the remaining bytes // The size includes the block-height field, so the remaining bytes
// are the torrent metadata that will be parsed and staged. // are the torrent metadata that will be parsed and staged.

View File

@ -1,9 +1,11 @@
use crate::records::memory::enums::ClientType;
use crate::records::memory::response_channels::{delete_entry, get_entry, is_retired_entry, Command};
use crate::rpc::commands::bad_rpc_call;
use crate::rpc::command_maps::MAX_RPC_REPLY_BYTES;
use crate::rpc::read_bytes_from_stream;
use crate::log::warn; use crate::log::warn;
use crate::records::memory::enums::ClientType;
use crate::records::memory::response_channels::{
delete_entry, get_entry, is_retired_entry, Command,
};
use crate::rpc::command_maps::MAX_RPC_REPLY_BYTES;
use crate::rpc::commands::bad_rpc_call;
use crate::rpc::read_bytes_from_stream;
use crate::sled::Db; use crate::sled::Db;
use crate::Arc; use crate::Arc;
use crate::Mutex; use crate::Mutex;
@ -24,8 +26,8 @@ pub async fn route_reply(
read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone()) read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone())
.await?; .await?;
let message_length = let message_length =
read_bytes_from_stream::read_u32_from_stream(connections_key, stream_locked.clone()) read_bytes_from_stream::read_u32_from_stream(connections_key, stream_locked.clone()).await?
.await? as usize; as usize;
if message_length > MAX_RPC_REPLY_BYTES { if message_length > MAX_RPC_REPLY_BYTES {
bad_rpc_call::record(ip, client_type, db, wallet_key).await; bad_rpc_call::record(ip, client_type, db, wallet_key).await;
return Err(format!( return Err(format!(
@ -44,9 +46,7 @@ pub async fn route_reply(
) )
.await?; .await?;
if tx.send(buffer).await.is_err() { if tx.send(buffer).await.is_err() {
warn!( warn!("[rpc] reply receiver dropped before payload delivery: {uid:?}");
"[rpc] reply receiver dropped before payload delivery: {uid:?}"
);
} }
delete_entry(map, uid).await; delete_entry(map, uid).await;

View File

@ -1,14 +1,14 @@
use crate::blocks::token::CreateTokenTransaction; use crate::blocks::token::CreateTokenTransaction;
use crate::common::binary_conversions::binary_to_string; use crate::common::binary_conversions::binary_to_string;
use crate::fs;
use crate::records::balance_sheet::pathing::{balance_asset_segments, balance_root_path}; use crate::records::balance_sheet::pathing::{balance_asset_segments, balance_root_path};
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::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::wallets::structures::Wallet;
use crate::sled::{Db, Tree}; use crate::sled::{Db, Tree};
use crate::{decode, encode}; use crate::wallets::structures::Wallet;
use crate::fs;
use crate::PathBuf; use crate::PathBuf;
use crate::{decode, encode};
fn parse_token_supply(value: &[u8]) -> Option<u64> { fn parse_token_supply(value: &[u8]) -> Option<u64> {
// Token supply may be stored either as raw bytes or as a decimal string. // Token supply may be stored either as raw bytes or as a decimal string.
@ -85,7 +85,8 @@ async fn find_origin_hash(
Err(_) => continue, Err(_) => continue,
}; };
let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await; let RpcResponse::Binary(tx_bytes) =
request_transaction_by_txid(db, txid_bytes.to_vec()).await;
// The fallback only cares about create-token transactions because // The fallback only cares about create-token transactions because
// those define the origin hash for a token. // those define the origin hash for a token.
@ -221,7 +222,8 @@ pub async fn lookup_token_details(db: &Db, token_name: String) -> RpcResponse {
None => return RpcResponse::Binary(b"error: Token origin not found".to_vec()), None => return RpcResponse::Binary(b"error: Token origin not found".to_vec()),
}; };
let RpcResponse::Binary(tx_bytes) = request_transaction_by_txid(db, decode(&origin_hash).unwrap_or_default()).await; let RpcResponse::Binary(tx_bytes) =
request_transaction_by_txid(db, decode(&origin_hash).unwrap_or_default()).await;
if tx_bytes.is_empty() { if tx_bytes.is_empty() {
return RpcResponse::Binary(b"error: Token contract transaction not found".to_vec()); return RpcResponse::Binary(b"error: Token contract transaction not found".to_vec());

View File

@ -1,211 +1,184 @@
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::skein::skein_128_hash_bytes; use crate::common::skein::skein_128_hash_bytes;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::torrent::structs::Torrent; use crate::sled::Db;
use crate::sled::Db; use crate::torrent::structs::Torrent;
use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom}; use crate::File;
use crate::{decode, encode}; use crate::PathBuf;
use crate::File; use crate::{decode, encode};
use crate::Path; use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom};
use crate::PathBuf;
pub async fn request_block_piece(
fn remove_block_pieces_from_db(db: &Db, block_number: u32, info_hash: &str) { db: &Db,
// When the canonical torrent exists, temporary cached pieces for that block_number: u32,
// block are no longer needed and can be dropped from the piece cache. requested_piece: u8,
let Ok(tree) = db.open_tree("block_pieces") else { requested_info_hash: u128,
return; ) -> RpcResponse {
}; // Serve a block piece either from the temporary cached-piece tree or
let prefix = format!("{block_number}-{info_hash}-"); // by slicing it directly from the canonical block file using torrent metadata.
let iter = tree.range(prefix.as_bytes()..); let tree = match db.open_tree("block_pieces") {
Ok(tree) => tree,
for (key, _value) in iter.flatten() { Err(err) => {
if !key.starts_with(prefix.as_bytes()) { let msg = format!("error: Failed to open block_pieces tree: {err}")
break; .as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
} }
let _ = tree.remove(key); };
let requested_info_hash_hex = encode(requested_info_hash.to_le_bytes());
let key = format!("{block_number}-{requested_info_hash_hex}-{requested_piece}");
let (
_network_name,
_padded_base_coin,
block_ext,
torrent_path,
_wallet_path,
block_path,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
let block_filename = PathBuf::from(&block_path)
.join(format!("{block_number}.{block_ext}"))
.to_string_lossy()
.into_owned();
let torrent_filename = PathBuf::from(&torrent_path)
.join(format!("{block_number}.torrent"))
.to_string_lossy()
.into_owned();
if let Some(piece_data) = tree.get(&key).ok().and_then(|result| result) {
// Cached pieces are used for in-progress downloads before the
// canonical torrent file is available locally. During an orphan
// fight this must be checked before the canonical file because a
// node can have cached pieces for a competing candidate at the
// same height.
RpcResponse::Binary(piece_data.to_vec())
} else if let Ok(mut torrent_file) = File::open(&torrent_filename).await {
let mut torrent_contents = Vec::new();
if let Err(err) = torrent_file.read_to_end(&mut torrent_contents).await {
let msg = format!("error: Failed to read torrent file: {err}")
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
let torrent = match Torrent::from_bytes(&torrent_contents).await {
Ok(torrent) => torrent,
Err(err) => {
let msg = format!("error: {err}").to_string().as_bytes().to_vec();
return RpcResponse::Binary(msg);
}
};
let torrent_info_hash_bytes = match decode(&torrent.info.info_hash) {
Ok(bytes) => bytes,
Err(err) => {
let msg = format!("error: Invalid torrent info hash: {err}")
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
};
let torrent_info_hash = match <[u8; 16]>::try_from(torrent_info_hash_bytes.as_slice()) {
Ok(bytes) => u128::from_le_bytes(bytes),
Err(_) => {
let msg = "error: Invalid torrent info hash length"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
};
if torrent_info_hash != requested_info_hash {
// A peer can ask for a specific candidate; reject the request
// if our canonical torrent is for a different info hash.
let msg = "error: Requested candidate not found"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
let pieces = torrent.info.pieces;
// Use the torrent piece map to locate the expected hash, read the
// matching byte range from the block file, and verify the piece hash.
if let Some(piece_object) = pieces
.iter()
.find(|piece| piece.contains_key(&requested_piece))
{
if let Some(expected_hash) = piece_object.get(&requested_piece) {
let piece_length = torrent.info.piece_length as u64;
if let Ok(mut block_file) = File::open(&block_filename).await {
let file_size = match block_file.metadata().await {
Ok(meta) => meta.len(),
Err(_) => {
let msg = "error: Error reading block file metadata"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
};
let start_byte = (requested_piece as u64 - 1) * piece_length;
if start_byte >= file_size {
let msg = "error: Requested piece is out of bounds"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
// The last piece may be shorter than the normal
// torrent piece length, so cap the read at EOF.
let piece_size = std::cmp::min(piece_length, file_size - start_byte) as usize;
let mut piece_data = vec![0u8; piece_size];
if block_file.seek(SeekFrom::Start(start_byte)).await.is_err() {
let msg = "error: Error seeking block file"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
if block_file.read_exact(&mut piece_data).await.is_err() {
let msg = "error: Error reading block file contents"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
let calculated_hash = skein_128_hash_bytes(&piece_data);
// Never serve a block slice that does not match the
// piece hash advertised in the torrent metadata.
if &calculated_hash == expected_hash {
return RpcResponse::Binary(piece_data);
} else {
let msg = "error: Hash mismatch".to_string().as_bytes().to_vec();
return RpcResponse::Binary(msg);
}
} else {
let msg = "error: Block not found".to_string().as_bytes().to_vec();
return RpcResponse::Binary(msg);
}
} else {
let msg = "error: Expected hash not found"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
} else {
let msg = "error: Requested piece is out of bounds"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
} else {
let msg = "error: piece not found".to_string().as_bytes().to_vec();
return RpcResponse::Binary(msg);
} }
} }
pub async fn request_block_piece(
db: &Db,
block_number: u32,
requested_piece: u8,
requested_info_hash: u128,
) -> RpcResponse {
// Serve a block piece either from the temporary cached-piece tree or
// by slicing it directly from the canonical block file using torrent metadata.
let tree = match db.open_tree("block_pieces") {
Ok(tree) => tree,
Err(err) => {
let msg = format!("error: Failed to open block_pieces tree: {err}")
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
};
let requested_info_hash_hex = encode(requested_info_hash.to_le_bytes());
let key = format!(
"{block_number}-{requested_info_hash_hex}-{requested_piece}"
);
let (
_network_name,
_padded_base_coin,
block_ext,
torrent_path,
_wallet_path,
block_path,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
let block_filename = PathBuf::from(&block_path)
.join(format!("{block_number}.{block_ext}"))
.to_string_lossy()
.into_owned();
let torrent_filename = PathBuf::from(&torrent_path)
.join(format!("{block_number}.torrent"))
.to_string_lossy()
.into_owned();
let file_exists = Path::new(&torrent_filename).exists();
let prefix = format!("{block_number}-{requested_info_hash_hex}-");
let pieces_exist = tree.range(prefix.as_bytes()..).peekable().peek().is_some();
// Once the canonical torrent exists, cached block pieces for the same
// height can be purged so the canonical file becomes the source of truth.
if file_exists && pieces_exist {
remove_block_pieces_from_db(db, block_number, &requested_info_hash_hex);
}
if let Some(piece_data) = tree.get(&key).ok().and_then(|result| result) {
// Cached pieces are used for in-progress downloads before the
// canonical torrent file is available locally.
RpcResponse::Binary(piece_data.to_vec())
} else if let Ok(mut torrent_file) = File::open(&torrent_filename).await {
let mut torrent_contents = Vec::new();
if let Err(err) = torrent_file.read_to_end(&mut torrent_contents).await {
let msg = format!("error: Failed to read torrent file: {err}")
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
let torrent = match Torrent::from_bytes(&torrent_contents).await {
Ok(torrent) => torrent,
Err(err) => {
let msg = format!("error: {err}").to_string().as_bytes().to_vec();
return RpcResponse::Binary(msg);
}
};
let torrent_info_hash_bytes = match decode(&torrent.info.info_hash) {
Ok(bytes) => bytes,
Err(err) => {
let msg = format!("error: Invalid torrent info hash: {err}")
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
};
let torrent_info_hash = match <[u8; 16]>::try_from(torrent_info_hash_bytes.as_slice()) {
Ok(bytes) => u128::from_le_bytes(bytes),
Err(_) => {
let msg = "error: Invalid torrent info hash length"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
};
if torrent_info_hash != requested_info_hash {
// A peer can ask for a specific candidate; reject the request
// if our canonical torrent is for a different info hash.
let msg = "error: Requested candidate not found"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
let pieces = torrent.info.pieces;
// Use the torrent piece map to locate the expected hash, read the
// matching byte range from the block file, and verify the piece hash.
if let Some(piece_object) = pieces
.iter()
.find(|piece| piece.contains_key(&requested_piece))
{
if let Some(expected_hash) = piece_object.get(&requested_piece) {
let piece_length = torrent.info.piece_length as u64;
if let Ok(mut block_file) = File::open(&block_filename).await {
let file_size = match block_file.metadata().await {
Ok(meta) => meta.len(),
Err(_) => {
let msg = "error: Error reading block file metadata"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
};
let start_byte = (requested_piece as u64 - 1) * piece_length;
if start_byte >= file_size {
let msg = "error: Requested piece is out of bounds"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
// The last piece may be shorter than the normal
// torrent piece length, so cap the read at EOF.
let piece_size = std::cmp::min(piece_length, file_size - start_byte) as usize;
let mut piece_data = vec![0u8; piece_size];
if block_file.seek(SeekFrom::Start(start_byte)).await.is_err() {
let msg = "error: Error seeking block file"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
if block_file.read_exact(&mut piece_data).await.is_err() {
let msg = "error: Error reading block file contents"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
let calculated_hash = skein_128_hash_bytes(&piece_data);
// Never serve a block slice that does not match the
// piece hash advertised in the torrent metadata.
if &calculated_hash == expected_hash {
return RpcResponse::Binary(piece_data);
} else {
let msg = "error: Hash mismatch".to_string().as_bytes().to_vec();
return RpcResponse::Binary(msg);
}
} else {
let msg = "error: Block not found".to_string().as_bytes().to_vec();
return RpcResponse::Binary(msg);
}
} else {
let msg = "error: Expected hash not found"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
} else {
let msg = "error: Requested piece is out of bounds"
.to_string()
.as_bytes()
.to_vec();
return RpcResponse::Binary(msg);
}
} else {
let msg = "error: piece not found".to_string().as_bytes().to_vec();
return RpcResponse::Binary(msg);
}
}

View File

@ -1,41 +1,41 @@
use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::rpc::responses::RpcResponse; use crate::read;
use crate::Path; use crate::rpc::responses::RpcResponse;
use crate::read; use crate::Path;
pub async fn request_block_torrent(height: &u32) -> RpcResponse { pub async fn request_block_torrent(height: &u32) -> RpcResponse {
// Torrent files live alongside blocks under a predictable // Torrent files live alongside blocks under a predictable
// `<height>.torrent` naming convention. // `<height>.torrent` naming convention.
let filename = format!("{height}.torrent"); let filename = format!("{height}.torrent");
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(&filename); let file_path = Path::new(&torrent_path).join(&filename);
if !file_path.exists() { if !file_path.exists() {
let msg = format!("error: Block {height} not found") let msg = format!("error: Block {height} not found")
.to_string() .to_string()
.as_bytes() .as_bytes()
.to_vec(); .to_vec();
return RpcResponse::Binary(msg); return RpcResponse::Binary(msg);
} }
match read(&file_path).await { match read(&file_path).await {
Ok(binary_data) => RpcResponse::Binary(binary_data), Ok(binary_data) => RpcResponse::Binary(binary_data),
Err(_) => { Err(_) => {
let msg = "error: Error reading torrent file" let msg = "error: Error reading torrent file"
.to_string() .to_string()
.as_bytes() .as_bytes()
.to_vec(); .to_vec();
RpcResponse::Binary(msg) RpcResponse::Binary(msg)
} }
} }
} }

View File

@ -1,153 +1,153 @@
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::rpc::command_maps; use crate::io;
use crate::rpc::responses::RpcResponse; use crate::rpc::command_maps;
use crate::sled::Db; use crate::rpc::responses::RpcResponse;
use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom}; use crate::sled::Db;
use crate::File; use crate::File;
use crate::io; use crate::PathBuf;
use crate::PathBuf; use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom};
const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64; const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64;
pub async fn request_transaction_by_txid(db: &Db, txid: Vec<u8>) -> RpcResponse { pub async fn request_transaction_by_txid(db: &Db, txid: Vec<u8>) -> RpcResponse {
// Resolve the saved transaction bytes directly from the txid lookup // Resolve the saved transaction bytes directly from the txid lookup
// tree and the referenced block file. // tree and the referenced block file.
match lookup_transaction_location(db, txid).await { match lookup_transaction_location(db, txid).await {
Ok((_block, _position, block_filename)) => { Ok((_block, _position, block_filename)) => {
let bytes = calculate_offset(&block_filename, _position).await; let bytes = calculate_offset(&block_filename, _position).await;
match bytes { match bytes {
Some(vec) => RpcResponse::Binary(vec), Some(vec) => RpcResponse::Binary(vec),
None => { None => {
let msg = "error: Error parsing block".to_string().as_bytes().to_vec(); let msg = "error: Error parsing block".to_string().as_bytes().to_vec();
RpcResponse::Binary(msg) RpcResponse::Binary(msg)
} }
} }
} }
Err(msg) => RpcResponse::Binary(msg.into_bytes()), Err(msg) => RpcResponse::Binary(msg.into_bytes()),
} }
} }
pub async fn request_transaction_by_txid_with_block(db: &Db, txid: Vec<u8>) -> RpcResponse { pub async fn request_transaction_by_txid_with_block(db: &Db, txid: Vec<u8>) -> RpcResponse {
// Some callers need the block number alongside the raw transaction // Some callers need the block number alongside the raw transaction
// bytes, so this variant prefixes the payload with the block height. // bytes, so this variant prefixes the payload with the block height.
match lookup_transaction_location(db, txid).await { match lookup_transaction_location(db, txid).await {
Ok((block, position, block_filename)) => { Ok((block, position, block_filename)) => {
let bytes = calculate_offset(&block_filename, position).await; let bytes = calculate_offset(&block_filename, position).await;
match bytes { match bytes {
Some(vec) => { Some(vec) => {
let mut response = Vec::with_capacity(4 + vec.len()); let mut response = Vec::with_capacity(4 + vec.len());
response.extend_from_slice(&(block as u32).to_le_bytes()); response.extend_from_slice(&(block as u32).to_le_bytes());
response.extend_from_slice(&vec); response.extend_from_slice(&vec);
RpcResponse::Binary(response) RpcResponse::Binary(response)
} }
None => { None => {
let msg = "error: Error parsing block".to_string().as_bytes().to_vec(); let msg = "error: Error parsing block".to_string().as_bytes().to_vec();
RpcResponse::Binary(msg) RpcResponse::Binary(msg)
} }
} }
} }
Err(msg) => RpcResponse::Binary(msg.into_bytes()), Err(msg) => RpcResponse::Binary(msg.into_bytes()),
} }
} }
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: Errpr retrieving value".to_string()); return Err("error: Errpr retrieving value".to_string());
} }
}; };
let value_str = binary_to_string(value.to_vec()); let value_str = binary_to_string(value.to_vec());
// 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 = match File::open(file_path).await { let mut file = match File::open(file_path).await {
Ok(file) => file, Ok(file) => file,
Err(_) => return None, Err(_) => return None,
}; };
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 bodies are fixed-size by type, so the type byte at // Transaction bodies are fixed-size by type, so the type byte at
// each offset tells us how far to jump to reach the next record. // each offset tells us how far to jump to reach the next record.
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 = match File::open(file_path).await { let mut file = match File::open(file_path).await {
Ok(file) => file, Ok(file) => file,
Err(_) => { Err(_) => {
return None; return None;
} }
}; };
file.seek(io::SeekFrom::Start(total_bytes_to_skip)) file.seek(io::SeekFrom::Start(total_bytes_to_skip))
.await .await
.ok()?; .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()?;
// Returned bytes include the transaction type byte at the front so // Returned bytes include the transaction type byte at the front so
// callers can parse the payload without extra lookup state. // callers can parse the payload without extra lookup state.
Some(transaction_bytes) Some(transaction_bytes)
} }

View File

@ -18,8 +18,8 @@ use crate::records::memory::enums::ClientType;
use crate::records::memory::response_channels::generate_uid; use crate::records::memory::response_channels::generate_uid;
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::torrent::torrenting_system::get_nodes::get_nodes_from_memory;
use crate::sled::Db; use crate::sled::Db;
use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory;
async fn broadcast_tx(tx_bytes: Vec<u8>) { async fn broadcast_tx(tx_bytes: Vec<u8>) {
// Broadcast newly accepted mempool transactions only to miner peers, // Broadcast newly accepted mempool transactions only to miner peers,

View File

@ -1,6 +1,6 @@
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::wallets::structures::Wallet;
use crate::sled::Db; use crate::sled::Db;
use crate::wallets::structures::Wallet;
pub async fn unblock_peer( pub async fn unblock_peer(
db: &Db, db: &Db,

View File

@ -1,8 +1,8 @@
use crate::rpc::read_bytes_from_stream; use crate::rpc::read_bytes_from_stream;
use crate::rpc::responses::RpcResponse; use crate::rpc::responses::RpcResponse;
use crate::rpc::server::flood_protection::MAX_TORRENT_METADATA_BYTES; use crate::rpc::server::flood_protection::MAX_TORRENT_METADATA_BYTES;
use crate::torrent::structs::Torrent;
use crate::sled::Db; use crate::sled::Db;
use crate::torrent::structs::Torrent;
use crate::Arc; use crate::Arc;
use crate::Mutex; use crate::Mutex;
use crate::TcpStream; use crate::TcpStream;

Some files were not shown because too many files have changed in this diff Show More