diff --git a/src/bin/create_burn_tx.rs b/src/bin/create_burn_tx.rs index dea59ed..2e2a3dd 100644 --- a/src/bin/create_burn_tx.rs +++ b/src/bin/create_burn_tx.rs @@ -11,10 +11,7 @@ use blockchain::{create_dir_all, AsyncWriteExt}; // Pad the asset name so it matches the 15-byte on-chain asset format. fn pad_to_width(input: &str, width: usize) -> String { let mut result = String::with_capacity(width); - let _ = std::fmt::write( - &mut result, - format_args!("{input: String { let mut result = String::with_capacity(width); - let _ = std::fmt::write( - &mut result, - format_args!("{input: String { // Asset names are fixed-width fields in the loan transaction bytes. let mut result = String::with_capacity(width); - let _ = std::fmt::write( - &mut result, - format_args!("{input: String { let mut result = String::with_capacity(width); // Pre-allocate string with capacity - let _ = std::fmt::write( - &mut result, - format_args!("{input: String { let mut result = String::with_capacity(width); // Pre-allocate string with capacity - let _ = std::fmt::write( - &mut result, - format_args!("{input: String { let mut result = String::with_capacity(width); // Pre-allocate string with capacity - let _ = std::fmt::write( - &mut result, - format_args!("{input: String { let mut result = String::with_capacity(width); // Pre-allocate string with capacity - let _ = std::fmt::write( - &mut result, - format_args!("{input: String { let mut result = String::with_capacity(width); // Pre-allocate string with capacity - let _ = std::fmt::write( - &mut result, - format_args!("{input: Option { let body = &tx_bytes[1..]; match txtype { - TRANSFER_TYPE => to_string_pretty(&TransferTransaction::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(), + TRANSFER_TYPE => { + to_string_pretty(&TransferTransaction::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(), - LENDER_TYPE => to_string_pretty(&LoanContractTransaction::from_bytes(txtype, body).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(), + LENDER_TYPE => to_string_pretty( + &LoanContractTransaction::from_bytes(txtype, body) + .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(), - ISSUE_TOKEN_TYPE => 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(), + ISSUE_TOKEN_TYPE => { + 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, } } diff --git a/src/bin/lookup_mempool_tx_by_signature.rs b/src/bin/lookup_mempool_tx_by_signature.rs index 9976aa7..72f2b6b 100644 --- a/src/bin/lookup_mempool_tx_by_signature.rs +++ b/src/bin/lookup_mempool_tx_by_signature.rs @@ -27,17 +27,50 @@ async fn decode_mempool_transaction(response: &[u8]) -> Option { let body = &response[1..]; match txtype { - TRANSFER_TYPE => to_string_pretty(&TransferTransaction::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(), + TRANSFER_TYPE => { + to_string_pretty(&TransferTransaction::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(), - LENDER_TYPE => to_string_pretty(&LoanContractTransaction::from_bytes(txtype, body).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(), + LENDER_TYPE => to_string_pretty( + &LoanContractTransaction::from_bytes(txtype, body) + .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(), - ISSUE_TOKEN_TYPE => 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(), + ISSUE_TOKEN_TYPE => { + 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, } } diff --git a/src/bin/lookup_network_info.rs b/src/bin/lookup_network_info.rs index 5297b44..10d6e80 100644 --- a/src/bin/lookup_network_info.rs +++ b/src/bin/lookup_network_info.rs @@ -45,10 +45,9 @@ fn decode_network_info(response: &[u8]) -> Option { // Mainnet uses CLC and testnet uses CLTC, so the prefix length is // inferred from the total payload size instead of hard-coded. - let wallet_prefix = - String::from_utf8_lossy(response.get(offset..offset + wallet_prefix_len)?) - .trim() - .to_string(); + let wallet_prefix = String::from_utf8_lossy(response.get(offset..offset + wallet_prefix_len)?) + .trim() + .to_string(); offset += wallet_prefix_len; let height = read_u32(response, &mut offset)?; diff --git a/src/bin/lookup_remote_balance.rs b/src/bin/lookup_remote_balance.rs index c64ac92..dc7b582 100644 --- a/src/bin/lookup_remote_balance.rs +++ b/src/bin/lookup_remote_balance.rs @@ -89,9 +89,7 @@ async fn main() { let address_file_path = if args.len() == 2 { args[1].clone() } else { - match prompt_for_path( - "Please enter the path to the file containing the wallet address: ", - ) { + match prompt_for_path("Please enter the path to the file containing the wallet address: ") { Ok(path) => path, Err(err) => { eprintln!("{err}"); diff --git a/src/bin/skein_hasher.rs b/src/bin/skein_hasher.rs index effa2f3..398f67a 100644 --- a/src/bin/skein_hasher.rs +++ b/src/bin/skein_hasher.rs @@ -1,5 +1,5 @@ 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::File; diff --git a/src/bin/validate_torrent_and_block_headers.rs b/src/bin/validate_torrent_and_block_headers.rs index d9d092f..321054e 100644 --- a/src/bin/validate_torrent_and_block_headers.rs +++ b/src/bin/validate_torrent_and_block_headers.rs @@ -1,189 +1,189 @@ -use blockchain::common::binary_conversions::hex_to_u64; -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::encode; -use blockchain::env; -use blockchain::records::unpack_block::unpack_header::load_block_header; -use blockchain::records::wallet_registry::resolve_pubkey_from_short_address; -use blockchain::torrent::structs::Torrent; -use blockchain::wallets::structures::Wallet; -use blockchain::{AsyncReadExt, File}; -use colored::*; -use std::process; - -#[tokio::main] -async fn main() { - // Validate that a local block file, block header, and torrent metadata agree. - let args: Vec = env::args().collect(); - if args.len() != 2 { - eprintln!("Usage: {} ", args[0]); - process::exit(1); - } - - let block_number: u32 = match args[1].parse() { - Ok(n) => n, - Err(_) => { - eprintln!("Block number must be an integer."); - process::exit(1); - } - }; - - 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 = format!("{block_path}/{block_number}.{block_ext}"); - let torrent_filename = format!("{torrent_path}/{block_number}.torrent"); - - // Load and decode the torrent metadata first because later checks compare against it. - let mut torrent_bytes = Vec::new(); - let mut torrent_file = File::open(&torrent_filename).await.unwrap_or_else(|_| { - eprintln!("Error: cannot open torrent file '{torrent_filename}'"); - process::exit(1); - }); - torrent_file.read_to_end(&mut 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. - let header = load_block_header(block_number).await.unwrap(); - let block_hash = header.hash().await; - let block_difficulty = hex_to_u64(&block_hash).await.unwrap(); - let miner_pubkey = resolve_pubkey_from_short_address( - &blockchain::startup::initialize_startup::open_chain_state().await, - &header.unmined_block.miner, - ) - .unwrap_or(None) - .unwrap_or_default(); - let miner_pubkey_hex = encode(&miner_pubkey); - - // Load the raw block bytes for file-size, info-hash, and piece-hash checks. - let mut block_data = Vec::new(); - let mut block_file = File::open(&block_filename).await.unwrap_or_else(|_| { - eprintln!("Error: cannot open block file '{block_filename}'"); - process::exit(1); - }); - block_file.read_to_end(&mut block_data).await.unwrap(); - - let info_hash_computed = skein_128_hash_bytes(&block_data); - - // Rebuild the unmined-block hash used by the miner proof signature. - let unmined_json = serde_json::to_string(&header.unmined_block).unwrap(); - let unmined_hash = skein_256_hash_data(&unmined_json); - let signature_ok = - Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex) - .await; - - let passed = "[PASSED]".green(); - let failed = "[FAILED]".red(); - - // Compare every header field that is duplicated inside the torrent metadata. - if header.unmined_block.timestamp == torrent.info.timestamp { - println!("timestamp match: {:>90}", format!("{passed}")); - } else { - println!("timestamp match: {:>90}", format!("{failed}")); - } - - if header.unmined_block.nonce == torrent.info.nonce { - println!("nonce match: {:>94}", format!("{passed}")); - } else { - println!("nonce match: {:>94}", format!("{failed}")); - } - - if header.unmined_block.miner == torrent.mined_by { - println!("wallet address match: {:>85}", format!("{passed}")); - } else { - println!("wallet address match: {:>85}", format!("{failed}")); - } - - if block_difficulty < torrent.info.this_block_difficulty { - println!("block difficulty check: {:>83}", format!("{passed}")); - } else { - println!("block difficulty check: {:>83}", format!("{failed}")); - } - - if header.vrf == torrent.info.vrf { - println!("VRF match: {:>96}", format!("{passed}")); - } else { - println!("VRF match: {:>96}", format!("{failed}")); - } - - if block_data.len() == torrent.info.length as usize { - println!("file size check: {:>90}", format!("{passed}")); - } else { - println!("file size check: {:>90}", format!("{failed}")); - } - - if block_hash == torrent.info.block_hash { - println!("block header hash check: {:>82}", format!("{passed}")); - } else { - println!("block header hash check: {:>82}", format!("{failed}")); - } - - let vrf_ok = Wallet::vrf_verify_with_public_key( - header.vrf, - &unmined_hash, - &miner_pubkey_hex, - &header.proof, - ) - .await; - if vrf_ok { - println!("VRF validation check: {:>85}", format!("{passed}")); - } else { - println!("VRF validation check: {:>85}", format!("{failed}")); - } - - if signature_ok { - println!("VRF Proof check: {:>90}", format!("{passed}")); - } else { - println!("VRF Proof check: {:>90}", format!("{failed}")); - } - - let mut all_pieces_passed = true; - // Piece hashes prove the torrent metadata still matches every block-file chunk. - for piece in &torrent.info.pieces { - for (index, expected_hash) in piece.iter() { - let idx = *index 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 slice = &block_data[start..end]; - let hash = skein_128_hash_bytes(slice); - - if hash == *expected_hash { - println!("piece {} hash check: {:>87}", idx, format!("{passed}")); - } else { - all_pieces_passed = false; - println!("piece {} hash check: {:>87}", idx, format!("{failed}")); - } - } - } - - if info_hash_computed == torrent.info.info_hash { - println!("block hash check: {:>89}", format!("{passed}")); - } else { - println!("block hash check: {:>89}", format!("{failed}")); - } - - // The final pass/fail summary requires every individual validation to pass. - if header.unmined_block.nonce == torrent.info.nonce - && header.unmined_block.miner == torrent.mined_by - && header.unmined_block.timestamp == torrent.info.timestamp - && block_difficulty < torrent.info.this_block_difficulty - && header.vrf == torrent.info.vrf - && block_data.len() == torrent.info.length as usize - && all_pieces_passed - && block_hash == torrent.info.block_hash - && signature_ok - && vrf_ok - { - println!("\nBlock {block_number} fully validated."); - } else { - println!("\nBlock {block_number} FAILED validation."); - } -} +use blockchain::common::binary_conversions::hex_to_u64; +use blockchain::common::network_paths_and_settings::block_extension_and_paths; +use blockchain::common::skein::{skein_128_hash_bytes, skein_256_hash_data}; +use blockchain::encode; +use blockchain::env; +use blockchain::records::unpack_block::unpack_header::load_block_header; +use blockchain::records::wallet_registry::resolve_pubkey_from_short_address; +use blockchain::torrent::structs::Torrent; +use blockchain::wallets::structures::Wallet; +use blockchain::{AsyncReadExt, File}; +use colored::*; +use std::process; + +#[tokio::main] +async fn main() { + // Validate that a local block file, block header, and torrent metadata agree. + let args: Vec = env::args().collect(); + if args.len() != 2 { + eprintln!("Usage: {} ", args[0]); + process::exit(1); + } + + let block_number: u32 = match args[1].parse() { + Ok(n) => n, + Err(_) => { + eprintln!("Block number must be an integer."); + process::exit(1); + } + }; + + 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 = format!("{block_path}/{block_number}.{block_ext}"); + let torrent_filename = format!("{torrent_path}/{block_number}.torrent"); + + // Load and decode the torrent metadata first because later checks compare against it. + let mut torrent_bytes = Vec::new(); + let mut torrent_file = File::open(&torrent_filename).await.unwrap_or_else(|_| { + eprintln!("Error: cannot open torrent file '{torrent_filename}'"); + process::exit(1); + }); + torrent_file.read_to_end(&mut 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. + let header = load_block_header(block_number).await.unwrap(); + let block_hash = header.hash().await; + let block_difficulty = hex_to_u64(&block_hash).await.unwrap(); + let miner_pubkey = resolve_pubkey_from_short_address( + &blockchain::startup::initialize_startup::open_chain_state().await, + &header.unmined_block.miner, + ) + .unwrap_or(None) + .unwrap_or_default(); + let miner_pubkey_hex = encode(&miner_pubkey); + + // Load the raw block bytes for file-size, info-hash, and piece-hash checks. + let mut block_data = Vec::new(); + let mut block_file = File::open(&block_filename).await.unwrap_or_else(|_| { + eprintln!("Error: cannot open block file '{block_filename}'"); + process::exit(1); + }); + block_file.read_to_end(&mut block_data).await.unwrap(); + + let info_hash_computed = skein_128_hash_bytes(&block_data); + + // Rebuild the unmined-block hash used by the miner proof signature. + let unmined_json = serde_json::to_string(&header.unmined_block).unwrap(); + let unmined_hash = skein_256_hash_data(&unmined_json); + let signature_ok = + Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex) + .await; + + let passed = "[PASSED]".green(); + let failed = "[FAILED]".red(); + + // Compare every header field that is duplicated inside the torrent metadata. + if header.unmined_block.timestamp == torrent.info.timestamp { + println!("timestamp match: {:>90}", format!("{passed}")); + } else { + println!("timestamp match: {:>90}", format!("{failed}")); + } + + if header.unmined_block.nonce == torrent.info.nonce { + println!("nonce match: {:>94}", format!("{passed}")); + } else { + println!("nonce match: {:>94}", format!("{failed}")); + } + + if header.unmined_block.miner == torrent.mined_by { + println!("wallet address match: {:>85}", format!("{passed}")); + } else { + println!("wallet address match: {:>85}", format!("{failed}")); + } + + if block_difficulty < torrent.info.this_block_difficulty { + println!("block difficulty check: {:>83}", format!("{passed}")); + } else { + println!("block difficulty check: {:>83}", format!("{failed}")); + } + + if header.vrf == torrent.info.vrf { + println!("VRF match: {:>96}", format!("{passed}")); + } else { + println!("VRF match: {:>96}", format!("{failed}")); + } + + if block_data.len() == torrent.info.length as usize { + println!("file size check: {:>90}", format!("{passed}")); + } else { + println!("file size check: {:>90}", format!("{failed}")); + } + + if block_hash == torrent.info.block_hash { + println!("block header hash check: {:>82}", format!("{passed}")); + } else { + println!("block header hash check: {:>82}", format!("{failed}")); + } + + let vrf_ok = Wallet::vrf_verify_with_public_key( + header.vrf, + &unmined_hash, + &miner_pubkey_hex, + &header.proof, + ) + .await; + if vrf_ok { + println!("VRF validation check: {:>85}", format!("{passed}")); + } else { + println!("VRF validation check: {:>85}", format!("{failed}")); + } + + if signature_ok { + println!("VRF Proof check: {:>90}", format!("{passed}")); + } else { + println!("VRF Proof check: {:>90}", format!("{failed}")); + } + + let mut all_pieces_passed = true; + // Piece hashes prove the torrent metadata still matches every block-file chunk. + for piece in &torrent.info.pieces { + for (index, expected_hash) in piece.iter() { + let idx = *index 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 slice = &block_data[start..end]; + let hash = skein_128_hash_bytes(slice); + + if hash == *expected_hash { + println!("piece {} hash check: {:>87}", idx, format!("{passed}")); + } else { + all_pieces_passed = false; + println!("piece {} hash check: {:>87}", idx, format!("{failed}")); + } + } + } + + if info_hash_computed == torrent.info.info_hash { + println!("block hash check: {:>89}", format!("{passed}")); + } else { + println!("block hash check: {:>89}", format!("{failed}")); + } + + // The final pass/fail summary requires every individual validation to pass. + if header.unmined_block.nonce == torrent.info.nonce + && header.unmined_block.miner == torrent.mined_by + && header.unmined_block.timestamp == torrent.info.timestamp + && block_difficulty < torrent.info.this_block_difficulty + && header.vrf == torrent.info.vrf + && block_data.len() == torrent.info.length as usize + && all_pieces_passed + && block_hash == torrent.info.block_hash + && signature_ok + && vrf_ok + { + println!("\nBlock {block_number} fully validated."); + } else { + println!("\nBlock {block_number} FAILED validation."); + } +} diff --git a/src/bin/verify_address.rs b/src/bin/verify_address.rs index 5e04145..6a854c3 100644 --- a/src/bin/verify_address.rs +++ b/src/bin/verify_address.rs @@ -83,9 +83,7 @@ async fn main() { let address_path = if args.len() == 2 { args[1].clone() } else { - match prompt_for_path( - "Please enter the path to the file containing the wallet address: ", - ) { + match prompt_for_path("Please enter the path to the file containing the wallet address: ") { Ok(path) => path, Err(err) => { eprintln!("{err}"); diff --git a/src/bin/verify_sign_loan_tx.rs b/src/bin/verify_sign_loan_tx.rs index 1409a84..739ffe1 100644 --- a/src/bin/verify_sign_loan_tx.rs +++ b/src/bin/verify_sign_loan_tx.rs @@ -118,9 +118,8 @@ async fn main() { "Do you agree that payments should be made {}?", display_payment_period(&payment_period) ); - let question4 = format!( - "Do you agree that this loan requires {payment_number} total payments?" - ); + let question4 = + format!("Do you agree that this loan requires {payment_number} total payments?"); let question5 = format!( "Do you agree that each payment should be {} {}?", display_amount(payment_amount), diff --git a/src/bin/verify_sign_swap_tx.rs b/src/bin/verify_sign_swap_tx.rs index b17cdf7..7a78245 100644 --- a/src/bin/verify_sign_swap_tx.rs +++ b/src/bin/verify_sign_swap_tx.rs @@ -1,249 +1,242 @@ -use blockchain::blocks::swap::UnsignedSwapTransaction; -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::env; -use blockchain::fs; -use blockchain::json; -use blockchain::read_to_string; -use blockchain::records::wallet_registry::resolve_local_input_short_address; -use blockchain::wallets::structures::Wallet; -use blockchain::Value; - -// padd the coin to ensure 15 characters -fn pad_to_width(input: &str, width: usize) -> String { - let mut result = String::with_capacity(width); // Pre-allocate string with capacity - let _ = std::fmt::write( - &mut result, - format_args!("{input: Result { - resolve_local_input_short_address(address.trim()) -} - -#[tokio::main] -async fn main() { - // Get the filename from the command line arguments - let args: Vec = env::args().collect(); - if args.len() != 2 { - println!("Usage:./sign_swap "); - return; - } - let filename = &args[1]; - let decryption_key = prompt_hidden_nonempty( - "What is your wallet decryption key? ", - "Wallet key cannot be empty. Please try again.", - ) - .await; - - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { - Ok(wallet) => wallet, - Err(err) => { - eprintln!("Wallet decryption failed: {err}"); - return; - } - }; - - let private_key = &wallet.saved.private_key; - let address = &wallet.saved.short_address; - - // Read the contents of the file - let contents = match read_to_string(filename).await { - Ok(contents) => contents, - Err(_) => { - println!("Error reading file: {filename}"); - return; - } - }; - - // Parse the JSON - let json: Result = serde_json::from_str(&contents); - let json = match json { - Ok(json) => json, - Err(_) => { - println!("Error parsing JSON in file: {filename}"); - return; - } - }; - - let txtype = 6; - - let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32; - let offer_expiration = json["offer_expiration"].as_u64().unwrap_or_default() as u32; - - // get values from transaction json - let token_name1 = json["ticker1"] - .as_str() - .unwrap_or_default() - .trim() - .to_lowercase(); - 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 receive_amount = value1 as f64 / 100000000.0; - - let token_name2 = json["ticker2"] - .as_str() - .unwrap_or_default() - .trim() - .to_lowercase(); - 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 send_amount = value2 as f64 / 100000000.0; - - let txfee1_value: u64 = json["txfee1"].as_u64().unwrap_or_default(); - let tip1_value: u64 = json["tip1"].as_u64().unwrap_or_default(); - - let sender1 = match normalize_short_address_input(json["sender1"].as_str().unwrap_or_default()) - { - Ok(address) => address, - 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 tip2 = tip2_value as f64 / 100000000.0; - - let sender2 = match normalize_short_address_input(json["sender2"].as_str().unwrap_or_default()) - { - Ok(address) => address, - Err(_) => { - println!("sender2 wallet invalid"); - return; - } - }; - - // ensure wallet and sender2 match - if sender2 != address.trim() { - println!( - "Transaction is not valid for your wallet address. Expected {sender2} found {address}" - ); - return; - } - - let ( - _network_name, - network_coin, - _suffix, - _torrent_path, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - // setup validation questions - let question1 = format!( - "Are you expecting to receive {receive_amount} {token_name1}?" - ); - let question2 = format!("Are you expecting to send {send_amount} {token_name2}?"); - let question3 = format!( - "Are you willing to spend {txfee2} {network_coin} in fees?" - ); - let question4 = format!("Are you willing to tip {tip2} {token_name2}?"); - - // ask validation questions - if !ask_yes_no_question(&question1).await { - println!("Transaction is not valid"); - return; - } - if !ask_yes_no_question(&question2).await { - println!("Transaction is not valid"); - return; - } - if !ask_yes_no_question(&question3).await { - println!("Transaction is not valid"); - return; - } - if !ask_yes_no_question(&question4).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); - - let unsigned_swap = UnsignedSwapTransaction::new( - txtype, - timestamp, - offer_expiration, - &padded_token_name1, - nft_series1, - value1, - &padded_token_name2, - nft_series2, - value2, - &sender1, - address.trim(), - tip1_value, - tip2_value, - txfee1_value, - txfee2_value, - ) - .await; - - let hashed_data = unsigned_swap.hash().await; - - let original_hash = &json["hash"].as_str().unwrap_or_default().trim(); - let signature1 = &json["signature1"].as_str().unwrap_or_default().trim(); - - let signature2 = if hashed_data == *original_hash.to_string() { - match unsigned_swap.hash_and_sign(&private_key.to_string()).await { - Ok(signature) => signature, - Err(err) => { - println!("Signing transaction failed: {err}"); - return; - } - } - } else { - println!("Signing transaction failed. The included hash was incorrect."); - return; - }; - - let output = json!({ - "txtype": txtype, - "timestamp": timestamp, - "offer_expiration": offer_expiration, - "ticker1": padded_token_name1, - "nft_series1": nft_series1, - "value1": value1, - "ticker2": padded_token_name2, - "nft_series2": nft_series2, - "value2": value2, - "sender1": sender1, - "sender2": address.trim(), - "tip1": tip1_value, - "tip2": tip2_value, - "txfee1": txfee1_value, - "txfee2": txfee2_value, - "hash": hashed_data, - "signature1": signature1, - "signature2": signature2 - }); - let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON"); - - // Define the directory path - let dir_path = "./transactions"; - - // Create the directory if it doesn't exist - if let Err(e) = fs::create_dir_all(dir_path) { - eprintln!("Failed to create directory: {e}"); - return; - } - - // Define the file path - let file_path = format!("{dir_path}/{hashed_data}.json"); - - // 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}"); -} +use blockchain::blocks::swap::UnsignedSwapTransaction; +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::env; +use blockchain::fs; +use blockchain::json; +use blockchain::read_to_string; +use blockchain::records::wallet_registry::resolve_local_input_short_address; +use blockchain::wallets::structures::Wallet; +use blockchain::Value; + +// padd the coin to ensure 15 characters +fn pad_to_width(input: &str, width: usize) -> String { + let mut result = String::with_capacity(width); // Pre-allocate string with capacity + let _ = std::fmt::write(&mut result, format_args!("{input: Result { + resolve_local_input_short_address(address.trim()) +} + +#[tokio::main] +async fn main() { + // Get the filename from the command line arguments + let args: Vec = env::args().collect(); + if args.len() != 2 { + println!("Usage:./sign_swap "); + return; + } + let filename = &args[1]; + let decryption_key = prompt_hidden_nonempty( + "What is your wallet decryption key? ", + "Wallet key cannot be empty. Please try again.", + ) + .await; + + let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + Ok(wallet) => wallet, + Err(err) => { + eprintln!("Wallet decryption failed: {err}"); + return; + } + }; + + let private_key = &wallet.saved.private_key; + let address = &wallet.saved.short_address; + + // Read the contents of the file + let contents = match read_to_string(filename).await { + Ok(contents) => contents, + Err(_) => { + println!("Error reading file: {filename}"); + return; + } + }; + + // Parse the JSON + let json: Result = serde_json::from_str(&contents); + let json = match json { + Ok(json) => json, + Err(_) => { + println!("Error parsing JSON in file: {filename}"); + return; + } + }; + + let txtype = 6; + + let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32; + let offer_expiration = json["offer_expiration"].as_u64().unwrap_or_default() as u32; + + // get values from transaction json + let token_name1 = json["ticker1"] + .as_str() + .unwrap_or_default() + .trim() + .to_lowercase(); + 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 receive_amount = value1 as f64 / 100000000.0; + + let token_name2 = json["ticker2"] + .as_str() + .unwrap_or_default() + .trim() + .to_lowercase(); + 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 send_amount = value2 as f64 / 100000000.0; + + let txfee1_value: u64 = json["txfee1"].as_u64().unwrap_or_default(); + let tip1_value: u64 = json["tip1"].as_u64().unwrap_or_default(); + + let sender1 = match normalize_short_address_input(json["sender1"].as_str().unwrap_or_default()) + { + Ok(address) => address, + 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 tip2 = tip2_value as f64 / 100000000.0; + + let sender2 = match normalize_short_address_input(json["sender2"].as_str().unwrap_or_default()) + { + Ok(address) => address, + Err(_) => { + println!("sender2 wallet invalid"); + return; + } + }; + + // ensure wallet and sender2 match + if sender2 != address.trim() { + println!( + "Transaction is not valid for your wallet address. Expected {sender2} found {address}" + ); + return; + } + + let ( + _network_name, + network_coin, + _suffix, + _torrent_path, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + // setup validation questions + let question1 = format!("Are you expecting to receive {receive_amount} {token_name1}?"); + let question2 = format!("Are you expecting to send {send_amount} {token_name2}?"); + let question3 = format!("Are you willing to spend {txfee2} {network_coin} in fees?"); + let question4 = format!("Are you willing to tip {tip2} {token_name2}?"); + + // ask validation questions + if !ask_yes_no_question(&question1).await { + println!("Transaction is not valid"); + return; + } + if !ask_yes_no_question(&question2).await { + println!("Transaction is not valid"); + return; + } + if !ask_yes_no_question(&question3).await { + println!("Transaction is not valid"); + return; + } + if !ask_yes_no_question(&question4).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); + + let unsigned_swap = UnsignedSwapTransaction::new( + txtype, + timestamp, + offer_expiration, + &padded_token_name1, + nft_series1, + value1, + &padded_token_name2, + nft_series2, + value2, + &sender1, + address.trim(), + tip1_value, + tip2_value, + txfee1_value, + txfee2_value, + ) + .await; + + let hashed_data = unsigned_swap.hash().await; + + let original_hash = &json["hash"].as_str().unwrap_or_default().trim(); + let signature1 = &json["signature1"].as_str().unwrap_or_default().trim(); + + let signature2 = if hashed_data == *original_hash.to_string() { + match unsigned_swap.hash_and_sign(&private_key.to_string()).await { + Ok(signature) => signature, + Err(err) => { + println!("Signing transaction failed: {err}"); + return; + } + } + } else { + println!("Signing transaction failed. The included hash was incorrect."); + return; + }; + + let output = json!({ + "txtype": txtype, + "timestamp": timestamp, + "offer_expiration": offer_expiration, + "ticker1": padded_token_name1, + "nft_series1": nft_series1, + "value1": value1, + "ticker2": padded_token_name2, + "nft_series2": nft_series2, + "value2": value2, + "sender1": sender1, + "sender2": address.trim(), + "tip1": tip1_value, + "tip2": tip2_value, + "txfee1": txfee1_value, + "txfee2": txfee2_value, + "hash": hashed_data, + "signature1": signature1, + "signature2": signature2 + }); + let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON"); + + // Define the directory path + let dir_path = "./transactions"; + + // Create the directory if it doesn't exist + if let Err(e) = fs::create_dir_all(dir_path) { + eprintln!("Failed to create directory: {e}"); + return; + } + + // Define the file path + let file_path = format!("{dir_path}/{hashed_data}.json"); + + // 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}"); +} diff --git a/src/blocks/block.rs b/src/blocks/block.rs index ed4573c..63c474e 100644 --- a/src/blocks/block.rs +++ b/src/blocks/block.rs @@ -185,10 +185,8 @@ impl VrfBlock { cursor .write_all(&self.unmined_block.timestamp.to_le_bytes()) .await?; - let miner_bytes = - Wallet::short_address_to_bytes(&self.unmined_block.miner).ok_or_else(|| { - tokio::io::Error::other("Invalid short miner address") - })?; + let miner_bytes = Wallet::short_address_to_bytes(&self.unmined_block.miner) + .ok_or_else(|| tokio::io::Error::other("Invalid short miner address"))?; cursor.write_all(&miner_bytes).await?; cursor .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 { // A VRF header must be exactly the fixed header byte length. 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. @@ -219,9 +216,8 @@ impl VrfBlock { let mut miner_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut miner_bytes).await?; - let miner = Wallet::bytes_to_short_address(&miner_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid short miner address") - })?; + let miner = Wallet::bytes_to_short_address(&miner_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid short miner address"))?; // Decode parent hash, difficulty, nonce, VRF number, and proof. let mut prev_hash_bytes = vec![0; 32]; diff --git a/src/blocks/burn.rs b/src/blocks/burn.rs index deed94e..347789e 100644 --- a/src/blocks/burn.rs +++ b/src/blocks/burn.rs @@ -105,9 +105,7 @@ impl BurnTransaction { .write_all(&self.unsigned_burn.time.to_le_bytes()) .await?; let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_burn.address) - .ok_or_else(|| { - tokio::io::Error::other("Invalid burn short address") - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid burn short address"))?; cursor.write_all(&address_bytes).await?; cursor.write_all(self.unsigned_burn.coin.as_bytes()).await?; cursor @@ -126,8 +124,7 @@ impl BurnTransaction { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result { 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); @@ -136,10 +133,8 @@ impl BurnTransaction { let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut address_bytes).await?; - let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid burn short address bytes", - ) - })?; + let address = Wallet::bytes_to_short_address(&address_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid burn short address bytes"))?; let mut coin_bytes = vec![0; 15]; cursor.read_exact(&mut coin_bytes).await?; diff --git a/src/blocks/collateral.rs b/src/blocks/collateral.rs index 1e7e1b8..94d2d41 100644 --- a/src/blocks/collateral.rs +++ b/src/blocks/collateral.rs @@ -110,10 +110,7 @@ impl CollateralClaimTransaction { .write_all(&decode(&self.unsigned_collateral_claim.contract_hash).unwrap()) .await?; let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_collateral_claim.address) - .ok_or_else(|| { - tokio::io::Error::other("Invalid collateral claimant short address", - ) - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid collateral claimant short address"))?; cursor.write_all(&address_bytes).await?; cursor .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 { // The block parser already consumed the transaction type byte. 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. @@ -144,8 +140,7 @@ impl CollateralClaimTransaction { let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut address_bytes).await?; 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?; diff --git a/src/blocks/genesis.rs b/src/blocks/genesis.rs index 741f21a..f103633 100644 --- a/src/blocks/genesis.rs +++ b/src/blocks/genesis.rs @@ -65,8 +65,7 @@ impl GenesisTransaction { // The transaction type is read by the block parser, so this // function receives only the remaining genesis bytes. 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. diff --git a/src/blocks/issue_token.rs b/src/blocks/issue_token.rs index ffdc844..28beeb5 100644 --- a/src/blocks/issue_token.rs +++ b/src/blocks/issue_token.rs @@ -104,10 +104,7 @@ impl IssueTokenTransaction { .write_all(&self.unsigned_issue_token.time.to_le_bytes()) .await?; let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_issue_token.creator) - .ok_or_else(|| { - tokio::io::Error::other("Invalid issue-token creator short address", - ) - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid issue-token creator short address"))?; cursor.write_all(&creator_bytes).await?; cursor .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 { // The block parser already consumed the transaction type byte. 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); @@ -138,8 +134,7 @@ impl IssueTokenTransaction { let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut creator_bytes).await?; 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. diff --git a/src/blocks/loan_payment.rs b/src/blocks/loan_payment.rs index 5406689..d9dc3d2 100644 --- a/src/blocks/loan_payment.rs +++ b/src/blocks/loan_payment.rs @@ -123,9 +123,7 @@ impl ContractPaymentTransaction { .write_all(&decode(&self.unsigned_contract_payment.contract_hash).unwrap()) .await?; let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_contract_payment.address) - .ok_or_else(|| { - tokio::io::Error::other("Invalid payer short address") - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid payer short address"))?; cursor.write_all(&address_bytes).await?; cursor .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 { // The block parser already consumed the transaction type byte. 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. @@ -160,10 +157,8 @@ impl ContractPaymentTransaction { // Decode payer short address, miner tip, fee, txid hash, and signature. let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut address_bytes).await?; - let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid payer short address bytes", - ) - })?; + let address = Wallet::bytes_to_short_address(&address_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid payer short address bytes"))?; let tip = cursor.read_u64_le().await?; let txfee = cursor.read_u64_le().await?; diff --git a/src/blocks/loans.rs b/src/blocks/loans.rs index da7ec43..bbd145c 100644 --- a/src/blocks/loans.rs +++ b/src/blocks/loans.rs @@ -21,9 +21,9 @@ pub struct UnsignedLoanContractTransaction { 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_amount: u64, // 8 bytes amount due for each payment - 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 txfee: u64, // 8 bytes transaction fee paid by the lender + 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 txfee: u64, // 8 bytes transaction fee paid by the lender } #[derive(Debug, Serialize, Clone)] // 1486 bytes @@ -35,9 +35,23 @@ pub struct LoanContractTransaction { } impl LoanContractTransaction { - pub const BYTE_LENGTH: usize = - 1 + 4 + 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; + pub const BYTE_LENGTH: usize = 1 + + 4 + + 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 { @@ -163,9 +177,7 @@ impl LoanContractTransaction { .write_all(&self.unsigned_loan_contract.loan_amount.to_le_bytes()) .await?; let lender_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.lender) - .ok_or_else(|| { - tokio::io::Error::other("Invalid lender short address") - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid lender short address"))?; cursor.write_all(&lender_bytes).await?; cursor .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()) .await?; let borrower_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.borrower) - .ok_or_else(|| { - tokio::io::Error::other("Invalid borrower short address", - ) - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid borrower short address"))?; cursor.write_all(&borrower_bytes).await?; cursor .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 { // The block parser already consumed the transaction type byte. 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. @@ -226,10 +234,8 @@ impl LoanContractTransaction { // Decode lender short address. let mut lender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut lender_bytes).await?; - let lender = Wallet::bytes_to_short_address(&lender_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid lender short address bytes", - ) - })?; + let lender = Wallet::bytes_to_short_address(&lender_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid lender short address bytes"))?; // Decode collateral asset and amount. let mut collateral_bytes = vec![0; 15]; @@ -241,10 +247,8 @@ impl LoanContractTransaction { // Decode borrower short address. let mut borrower_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut borrower_bytes).await?; - let borrower = Wallet::bytes_to_short_address(&borrower_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid borrower short address bytes", - ) - })?; + let borrower = Wallet::bytes_to_short_address(&borrower_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid borrower short address bytes"))?; // Decode payment schedule and late-payment rules. let mut payment_period_bytes = vec![0; 1]; diff --git a/src/blocks/marketing.rs b/src/blocks/marketing.rs index 1a51025..8bb6f91 100644 --- a/src/blocks/marketing.rs +++ b/src/blocks/marketing.rs @@ -31,8 +31,19 @@ pub struct MarketingTransaction { } impl MarketingTransaction { - pub const BYTE_LENGTH: usize = - 1 + 4 + 8 + 6 + 40 + 100 + 1 + 1 + 2 + 2 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH; + pub const BYTE_LENGTH: usize = 1 + + 4 + + 8 + + 6 + + 40 + + 100 + + 1 + + 1 + + 2 + + 2 + + Wallet::SHORT_ADDRESS_BYTES_LENGTH + + 8 + + Wallet::SIGNATURE_LENGTH; } impl UnsignedMarketingTransaction { @@ -152,10 +163,7 @@ impl MarketingTransaction { .write_all(&self.unsigned_marketing.click_value.to_le_bytes()) .await?; let advertiser_bytes = Wallet::short_address_to_bytes(&self.unsigned_marketing.advertiser) - .ok_or_else(|| { - tokio::io::Error::other("Invalid advertiser short address", - ) - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid advertiser short address"))?; cursor.write_all(&advertiser_bytes).await?; cursor .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 { // The block parser already consumed the transaction type byte. 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. @@ -201,10 +208,8 @@ impl MarketingTransaction { // Decode advertiser short address, fee, and signature. let mut advertiser_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut advertiser_bytes).await?; - let advertiser = Wallet::bytes_to_short_address(&advertiser_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid advertiser short address bytes", - ) - })?; + let advertiser = Wallet::bytes_to_short_address(&advertiser_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid advertiser short address bytes"))?; let txfee = cursor.read_u64_le().await?; let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH]; diff --git a/src/blocks/nft.rs b/src/blocks/nft.rs index e01bef7..a91b7f7 100644 --- a/src/blocks/nft.rs +++ b/src/blocks/nft.rs @@ -10,15 +10,15 @@ use crate::{AsyncReadExt, AsyncWriteExt}; #[derive(Debug, Serialize, Clone)] // 255 bytes pub struct UnsignedCreateNftTransaction { - pub txtype: u8, // 1 byte transaction type, should be 4 - pub time: u32, // 4 bytes transaction timestamp - pub creator: String, // 22 bytes creator short address - 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 txtype: u8, // 1 byte transaction type, should be 4 + pub time: u32, // 4 bytes transaction timestamp + pub creator: String, // 22 bytes creator short address + 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 item_ipfs: String, // 100 bytes padded CID string - pub count: u32, // 4 bytes 1 for single NFT, otherwise series item count - pub desc: String, // 100 bytes description padded with spaces - pub txfee: u64, // 8 bytes transaction fee + pub count: u32, // 4 bytes 1 for single NFT, otherwise series item count + pub desc: String, // 100 bytes description padded with spaces + pub txfee: u64, // 8 bytes transaction fee } #[derive(Debug, Serialize, Clone)] // 921 bytes @@ -28,8 +28,16 @@ pub struct CreateNftTransaction { } impl CreateNftTransaction { - pub const BYTE_LENGTH: usize = - 1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 1 + 15 + 100 + 4 + 100 + 8 + Wallet::SIGNATURE_LENGTH; + pub const BYTE_LENGTH: usize = 1 + + 4 + + Wallet::SHORT_ADDRESS_BYTES_LENGTH + + 1 + + 15 + + 100 + + 4 + + 100 + + 8 + + Wallet::SIGNATURE_LENGTH; } impl UnsignedCreateNftTransaction { @@ -119,9 +127,7 @@ impl CreateNftTransaction { .write_all(&self.unsigned_create_nft.time.to_le_bytes()) .await?; let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_nft.creator) - .ok_or_else(|| { - tokio::io::Error::other("Invalid creator short address") - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid creator short address"))?; cursor.write_all(&creator_bytes).await?; cursor .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 { // The block parser already consumed the transaction type byte. 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. @@ -161,10 +166,8 @@ impl CreateNftTransaction { let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut creator_bytes).await?; - let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid creator short address bytes", - ) - })?; + let creator = Wallet::bytes_to_short_address(&creator_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid creator short address bytes"))?; // Decode series flag, name, CID, count, description, fee, and signature. let series = cursor.read_u8().await?; diff --git a/src/blocks/rewards.rs b/src/blocks/rewards.rs index 9b8175b..c111104 100644 --- a/src/blocks/rewards.rs +++ b/src/blocks/rewards.rs @@ -65,8 +65,7 @@ impl RewardsTransaction { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result { // The block parser already consumed the transaction type byte. 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. diff --git a/src/blocks/swap.rs b/src/blocks/swap.rs index d800c0c..a45feb4 100644 --- a/src/blocks/swap.rs +++ b/src/blocks/swap.rs @@ -14,17 +14,17 @@ pub struct UnsignedSwapTransaction { pub timestamp: u32, // 4 bytes offer creation timestamp pub offer_expiration: u32, // 4 bytes offer expiration timestamp pub ticker1: String, // 15 bytes asset offered by sender1 - pub nft_series1: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item - pub value1: u64, // 8 bytes amount offered by sender1 - pub ticker2: String, // 15 bytes asset offered by sender2 - pub nft_series2: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item - pub value2: u64, // 8 bytes amount offered by sender2 - pub sender1: String, // 22 bytes sender1 short address - pub sender2: String, // 22 bytes sender2 short address - pub tip1: u64, // 8 bytes miner tip paid in ticker1 - pub tip2: u64, // 8 bytes miner tip paid in ticker2 - pub txfee1: u64, // 8 bytes sender1 fee - pub txfee2: u64, // 8 bytes sender2 fee + pub nft_series1: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item + pub value1: u64, // 8 bytes amount offered by sender1 + pub ticker2: String, // 15 bytes asset offered by sender2 + pub nft_series2: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item + pub value2: u64, // 8 bytes amount offered by sender2 + pub sender1: String, // 22 bytes sender1 short address + pub sender2: String, // 22 bytes sender2 short address + pub tip1: u64, // 8 bytes miner tip paid in ticker1 + pub tip2: u64, // 8 bytes miner tip paid in ticker2 + pub txfee1: u64, // 8 bytes sender1 fee + pub txfee2: u64, // 8 bytes sender2 fee } #[derive(Debug, Serialize, Clone)] // 1471 bytes @@ -35,9 +35,23 @@ pub struct SwapTransaction { } impl SwapTransaction { - pub const BYTE_LENGTH: usize = - 1 + 4 + 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; + pub const BYTE_LENGTH: usize = 1 + + 4 + + 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 { @@ -178,13 +192,9 @@ impl SwapTransaction { .write_all(&self.unsigned_swap.value2.to_le_bytes()) .await?; let sender1_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender1) - .ok_or_else(|| { - tokio::io::Error::other("Invalid sender1 short address") - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid sender1 short address"))?; let sender2_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender2) - .ok_or_else(|| { - tokio::io::Error::other("Invalid sender2 short address") - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid sender2 short address"))?; cursor.write_all(&sender1_bytes).await?; cursor.write_all(&sender2_bytes).await?; cursor @@ -208,8 +218,7 @@ impl SwapTransaction { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result { // The block parser already consumed the transaction type byte. 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. @@ -238,17 +247,13 @@ impl SwapTransaction { // Decode both sender short addresses. let mut sender1_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut sender1_bytes).await?; - let sender1 = Wallet::bytes_to_short_address(&sender1_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid sender1 short address bytes", - ) - })?; + let sender1 = Wallet::bytes_to_short_address(&sender1_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid sender1 short address bytes"))?; let mut sender2_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut sender2_bytes).await?; - let sender2 = Wallet::bytes_to_short_address(&sender2_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid sender2 short address bytes", - ) - })?; + let sender2 = Wallet::bytes_to_short_address(&sender2_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid sender2 short address bytes"))?; // Decode miner tips, fees, and both signatures. let tip1 = cursor.read_u64_le().await?; diff --git a/src/blocks/token.rs b/src/blocks/token.rs index 2fc1d3d..3a98f0d 100644 --- a/src/blocks/token.rs +++ b/src/blocks/token.rs @@ -115,9 +115,7 @@ impl CreateTokenTransaction { .write_all(&self.unsigned_create_token.time.to_le_bytes()) .await?; let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_token.creator) - .ok_or_else(|| { - tokio::io::Error::other("Invalid creator short address") - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid creator short address"))?; cursor.write_all(&creator_bytes).await?; cursor .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 { // The block parser already consumed the transaction type byte. 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. @@ -152,10 +149,8 @@ impl CreateTokenTransaction { // Decode the creator short address. let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut creator_bytes).await?; - let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid creator short address bytes", - ) - })?; + let creator = Wallet::bytes_to_short_address(&creator_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid creator short address bytes"))?; // Decode the fixed 15-byte token ticker. let mut ticker_bytes = vec![0; 15]; diff --git a/src/blocks/transfer.rs b/src/blocks/transfer.rs index e94cb67..5c38244 100644 --- a/src/blocks/transfer.rs +++ b/src/blocks/transfer.rs @@ -14,11 +14,11 @@ pub struct UnsignedTransferTransaction { pub txtype: u8, // 1 byte transaction type, should be 2 pub time: u32, // 4 bytes transaction timestamp 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 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 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 sender: String, // 22 bytes sender 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 @@ -28,8 +28,15 @@ pub struct TransferTransaction { } impl TransferTransaction { - pub const BYTE_LENGTH: usize = - 1 + 4 + 8 + 15 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH; + pub const BYTE_LENGTH: usize = 1 + + 4 + + 8 + + 15 + + 4 + + Wallet::SHORT_ADDRESS_BYTES_LENGTH + + Wallet::SHORT_ADDRESS_BYTES_LENGTH + + 8 + + Wallet::SIGNATURE_LENGTH; } impl UnsignedTransferTransaction { @@ -126,14 +133,9 @@ impl TransferTransaction { .write_all(&self.unsigned_transfer.nft_series.to_le_bytes()) .await?; let sender_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.sender) - .ok_or_else(|| { - tokio::io::Error::other("Invalid sender short address") - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid sender short address"))?; let receiver_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.receiver) - .ok_or_else(|| { - tokio::io::Error::other("Invalid receiver short address", - ) - })?; + .ok_or_else(|| tokio::io::Error::other("Invalid receiver short address"))?; cursor.write_all(&sender_bytes).await?; cursor.write_all(&receiver_bytes).await?; cursor @@ -147,8 +149,7 @@ impl TransferTransaction { pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result { // The block parser already consumed the transaction type byte. 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. @@ -168,18 +169,14 @@ impl TransferTransaction { // Decode the sender short address. let mut sender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut sender_bytes).await?; - let sender = Wallet::bytes_to_short_address(&sender_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid sender short address bytes", - ) - })?; + let sender = Wallet::bytes_to_short_address(&sender_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid sender short address bytes"))?; // Decode the receiver short address. let mut receiver_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut receiver_bytes).await?; - let receiver = Wallet::bytes_to_short_address(&receiver_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid receiver short address bytes", - ) - })?; + let receiver = Wallet::bytes_to_short_address(&receiver_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid receiver short address bytes"))?; // Decode fee and signature. let txfee = cursor.read_u64_le().await?; diff --git a/src/blocks/vanity.rs b/src/blocks/vanity.rs index 73a67e0..36dfa71 100644 --- a/src/blocks/vanity.rs +++ b/src/blocks/vanity.rs @@ -9,11 +9,11 @@ use crate::{AsyncReadExt, AsyncWriteExt}; #[derive(Debug, Serialize, Clone)] // 57 bytes pub struct UnsignedVanityAddressTransaction { - pub txtype: u8, // 1 byte transaction type, should be 12 - pub timestamp: u32, // 4 bytes transaction timestamp - pub address: String, // 22 bytes real short address receiving the vanity mapping - pub vanity_address: String, // 22 bytes vanity short address being registered - pub txfee: u64, // 8 bytes fee paid for vanity registration + pub txtype: u8, // 1 byte transaction type, should be 12 + pub timestamp: u32, // 4 bytes transaction timestamp + pub address: String, // 22 bytes real short address receiving the vanity mapping + pub vanity_address: String, // 22 bytes vanity short address being registered + pub txfee: u64, // 8 bytes fee paid for vanity registration } #[derive(Debug, Serialize, Clone)] // 723 bytes @@ -23,8 +23,12 @@ pub struct VanityAddressTransaction { } impl VanityAddressTransaction { - pub const BYTE_LENGTH: usize = - 1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH; + pub const BYTE_LENGTH: usize = 1 + + 4 + + Wallet::SHORT_ADDRESS_BYTES_LENGTH + + Wallet::SHORT_ADDRESS_BYTES_LENGTH + + 8 + + Wallet::SIGNATURE_LENGTH; } impl UnsignedVanityAddressTransaction { @@ -99,10 +103,9 @@ impl VanityAddressTransaction { .write_all(&self.unsigned_vanity_address.timestamp.to_le_bytes()) .await?; - let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_vanity_address.address) - .ok_or_else(|| { - tokio::io::Error::other("Invalid sender short address") - })?; + let address_bytes = + Wallet::short_address_to_bytes(&self.unsigned_vanity_address.address) + .ok_or_else(|| tokio::io::Error::other("Invalid sender short address"))?; // Vanity addresses use the same 22-byte width as short addresses // but are encoded through the vanity-specific byte conversion. let vanity_bytes = @@ -135,15 +138,13 @@ impl VanityAddressTransaction { let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH]; cursor.read_exact(&mut address_bytes).await?; - let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid sender short address bytes") - })?; + let address = Wallet::bytes_to_short_address(&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]; cursor.read_exact(&mut vanity_bytes).await?; - let vanity_address = Wallet::bytes_to_vanity_address(&vanity_bytes).ok_or_else(|| { - tokio::io::Error::other("Invalid vanity short address bytes") - })?; + let vanity_address = Wallet::bytes_to_vanity_address(&vanity_bytes) + .ok_or_else(|| tokio::io::Error::other("Invalid vanity short address bytes"))?; let txfee = cursor.read_u64_le().await?; diff --git a/src/common/binary_conversions.rs b/src/common/binary_conversions.rs index ab1baa8..53ab7d4 100644 --- a/src/common/binary_conversions.rs +++ b/src/common/binary_conversions.rs @@ -28,9 +28,7 @@ pub async fn hex_to_u64(hex_string: &str) -> Result { pub fn binary_to_string(binary_data: Vec) -> String { match String::from_utf8(binary_data.clone()) { Ok(s) => s, - Err(_) => { - "Invalid UTF-8 Data".to_string() - } + Err(_) => "Invalid UTF-8 Data".to_string(), } } diff --git a/src/common/skein.rs b/src/common/skein.rs index 5404d90..3e60c94 100644 --- a/src/common/skein.rs +++ b/src/common/skein.rs @@ -1,10 +1,10 @@ use crate::encode; +use crate::ripemd::Digest as RipemdDigest; use crate::Digest; use crate::Output; use crate::Ripemd160; use crate::Skein256; use crate::Skein512; -use crate::ripemd::Digest as RipemdDigest; pub fn skein_128_hash_bytes(data: &[u8]) -> String { // Contractless 128-bit hashes are Skein256 hashes reduced to 16 bytes. diff --git a/src/config.rs b/src/config.rs index fde233a..5b89dd8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -159,8 +159,7 @@ impl Settings { config_dir, ), log_path: Self::expand( - conf.get_from(Some("Paths"), "LOG_PATH") - .unwrap_or("./logs"), + conf.get_from(Some("Paths"), "LOG_PATH").unwrap_or("./logs"), config_dir, ), log_level: conf diff --git a/src/miner/genesis.rs b/src/miner/genesis.rs index 8dda40f..a50b413 100644 --- a/src/miner/genesis.rs +++ b/src/miner/genesis.rs @@ -2,20 +2,22 @@ use crate::blocks::block::{Block, UnminedBlock}; use crate::blocks::genesis::{GenesisTransaction, UnsignedGenesisTransaction}; use crate::common::check_genesis::genesis_checkup; 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::response_channels::Command; use crate::records::record_chain::save::save_block; use crate::records::record_chain::structs::{SaveBlockParams, SaveType}; +use crate::sled::Db; +use crate::sleep; use crate::verifications::verification_service::VerificationService; use crate::wallets::structures::Wallet; -use crate::log::{error, info}; -use crate::sled::Db; use crate::Arc; use crate::Duration; use crate::Error; use crate::Mutex; -use crate::sleep; use crate::Utc; pub async fn create_genesis_transaction( diff --git a/src/orphans/add_genesis.rs b/src/orphans/add_genesis.rs index 3651dce..3bf1cfd 100644 --- a/src/orphans/add_genesis.rs +++ b/src/orphans/add_genesis.rs @@ -1,15 +1,15 @@ use crate::common::check_genesis::genesis_checkup; 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::{ handle_response_and_save_torrent, send_request_torrent_message, }; -use crate::sled::Db; -use crate::torrent::structs::Torrent; use crate::Arc; use crate::Duration; use crate::Mutex; use crate::TcpStream; -use crate::timeout; pub async fn create_genesis_block( local_height: u32, diff --git a/src/orphans/orphan_checkup.rs b/src/orphans/orphan_checkup.rs index 7237885..95acb38 100644 --- a/src/orphans/orphan_checkup.rs +++ b/src/orphans/orphan_checkup.rs @@ -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::orphans::replay_errors::should_retry_staged_candidate; use crate::orphans::structs::{OrphanCheckup, UndoTransactions}; use crate::orphans::undo_block_transactions::undo_transactions; use crate::records::memory::torrent_status::{ 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::{ 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::verifications::verification_service::global_verification_service; async fn staged_candidates_for_height(height: u32) -> Vec { let mut candidates = Vec::new(); @@ -42,12 +51,12 @@ fn torrent_beats(left: &Torrent, right: &Torrent) -> bool { .is_lt() } -async fn best_competing_candidate( +async fn ordered_competing_candidates( height: u32, local_torrent: &Torrent, candidates: &[Torrent], -) -> Option { - let mut preferred: Option = None; +) -> Vec { + let mut ordered = Vec::new(); for torrent in candidates { // Identical info hashes are the same block candidate, not a competing fork. @@ -64,19 +73,83 @@ async fn best_competing_candidate( continue; } - match &preferred { - Some(current_torrent) => { - // Among candidates that have not been ruled out, choose the - // same deterministic winner the block fight uses. - if torrent_beats(torrent, current_torrent) { - preferred = Some(torrent.clone()); - } - } - None => preferred = Some(torrent.clone()), + ordered.push(torrent.clone()); + } + + // Among candidates that have not been ruled out, choose the same + // deterministic order the block fight uses. + ordered.sort_by(|a, b| { + a.info + .timestamp + .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, ¶ms.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(¶ms.db, height, &torrent.info.info_hash).await?; + + if result.len() != torrent.info.length as usize { + cleanup_candidate_pieces(¶ms.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(¶ms.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(¶ms.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(¶ms.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> { @@ -89,41 +162,69 @@ pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), Stri let local_torrent = load_torrent(¶ms.db, height).await?; let staged_candidates = staged_candidates_for_height(height).await; - if let Some(competing_torrent) = - best_competing_candidate(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(), - }; + let ordered_candidates = + ordered_competing_candidates(height, &local_torrent, &staged_candidates).await; - if torrent_beats(&competing_torrent, &local_torrent) { - set_torrent_status(height, &competing_info_hash, TorrentStatus::Valid).await; - if !params.node_syncing { - begin_reorg_lock().await; + for competing_torrent in ordered_candidates { + let competing_info_hash = competing_torrent.info.info_hash.clone(); + + 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}"); - undo_transactions(undo_transactions_params, wallet_key).await?; - return Ok(()); + break; } - // The local block remains the winner at this height, so every - // staged competitor for the same height has now been checked. - 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; + match candidate_attaches_before_rollback( + ¶ms, + height, + &competing_torrent, + wallet_key, + ) + .await + { + Ok(()) => { + 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; + } } } } diff --git a/src/orphans/replay_errors.rs b/src/orphans/replay_errors.rs index 8902e7f..9110411 100644 --- a/src/orphans/replay_errors.rs +++ b/src/orphans/replay_errors.rs @@ -5,8 +5,10 @@ pub fn should_retry_staged_candidate(error: &str) -> bool { || error.contains("piece not found") || error.contains("Requested candidate 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 replacement torrent") || error.contains("No replacement torrent received") || error.contains("Piece reply channel closed") + || error.contains("Replay waiting for block pieces") } diff --git a/src/orphans/save_blocks.rs b/src/orphans/save_blocks.rs index 686499c..0cd1fd9 100644 --- a/src/orphans/save_blocks.rs +++ b/src/orphans/save_blocks.rs @@ -1,5 +1,6 @@ -use crate::orphans::structs::UndoTransactions; 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::memory::response_channels::reserve_entry; 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, }; use crate::{timeout, Duration}; +use std::collections::HashSet; pub async fn save_new_blocks( params: &UndoTransactions, @@ -22,6 +24,7 @@ pub async fn save_new_blocks( ) -> Result<(), String> { // after rollback, request and save each remote block from the // divergence point up to the height we need to restore + let mut hydrated_heights = HashSet::new(); loop { let mut resolved_from_staging = false; let staged_candidates = list_staged_torrents_for_height(true_start_height).await?; @@ -101,12 +104,7 @@ pub async fn save_new_blocks( } else { TorrentStatus::Invalid }; - set_torrent_status( - true_start_height, - &torrent_info_hash, - status, - ) - .await; + set_torrent_status(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 // directly from the connected peer. let (hashmap_key, _save_tx, save_rx) = reserve_entry(params.map.clone()).await; diff --git a/src/orphans/snapshot_check.rs b/src/orphans/snapshot_check.rs index 12cb792..2917ecb 100644 --- a/src/orphans/snapshot_check.rs +++ b/src/orphans/snapshot_check.rs @@ -47,11 +47,8 @@ pub async fn update_snapshot(db: &Db, current_height: u32) -> Result<(), String> let hash = header.hash().await; let value = format!("{snapshot_height}:{hash}"); let key = b"snapshot"; - db.insert(key, value.as_bytes()).map_err(|e| { - format!( - "Failed to store snapshot at height {snapshot_height}: {e}" - ) - })?; + db.insert(key, value.as_bytes()) + .map_err(|e| format!("Failed to store snapshot at height {snapshot_height}: {e}"))?; 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; } } diff --git a/src/orphans/sync_check.rs b/src/orphans/sync_check.rs index f717be5..209f640 100644 --- a/src/orphans/sync_check.rs +++ b/src/orphans/sync_check.rs @@ -104,6 +104,7 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re } let mut advanced_height = false; + let mut retryable_pending = false; for torrent in ordered_candidates { 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) => { if should_retry_staged_candidate(&err) { + retryable_pending = true; // Piece availability is not proof that the candidate // lost the block fight; leave it pending so a later // 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 { // Every staged candidate for the current expected height was // 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(()); } } @@ -196,9 +203,16 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<() if !snapshot_verified(undo_transactions_params, wallet_key).await { // A snapshot rollback already happened, so replay staged torrents and // exit instead of running the near-tip rules against stale heights. + let mut replay_waiting = false; match replay_staged_torrents(¶ms, wallet_key).await { 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 { 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; + let mut replay_waiting = false; + let height_before_window_check = get_height(¶ms.db); match orphan_window_check(checkup_params, wallet_key).await { Ok(()) => {} - Err(err) => error!("[orphan] orphan window check error: {err}"), + Err(err) => { + if should_retry_staged_candidate(&err) + && get_height(¶ms.db) < height_before_window_check + { + replay_waiting = true; + } + error!("[orphan] orphan window check error: {err}"); + } } + let height_before_replay = get_height(¶ms.db); match replay_staged_torrents(¶ms, wallet_key).await { 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(¶ms.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 { end_reorg_lock(); diff --git a/src/orphans/undo_block_transactions.rs b/src/orphans/undo_block_transactions.rs index dce8a2e..0878b5a 100644 --- a/src/orphans/undo_block_transactions.rs +++ b/src/orphans/undo_block_transactions.rs @@ -39,10 +39,17 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re for transaction in transactions.into_iter().rev() { match transaction { Transaction::Rewards(rewards_tx) => { - undo_rewards_transaction(rewards_tx, &mining_receiver, ¶ms.db, current_height).await + undo_rewards_transaction( + rewards_tx, + &mining_receiver, + ¶ms.db, + current_height, + ) + .await } Transaction::Transfer(transfer_tx) => { - undo_transfer_transaction(transfer_tx.clone(), &mining_receiver, ¶ms.db).await; + undo_transfer_transaction(transfer_tx.clone(), &mining_receiver, ¶ms.db) + .await; rolled_back_transactions.push(Transaction::Transfer(transfer_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)); } Transaction::Token(create_token_tx) => { - undo_create_token_transaction(create_token_tx.clone(), &mining_receiver, ¶ms.db) - .await; + undo_create_token_transaction( + create_token_tx.clone(), + &mining_receiver, + ¶ms.db, + ) + .await; rolled_back_transactions.push(Transaction::Token(create_token_tx)); } Transaction::IssueToken(issue_token_tx) => { - undo_issue_token_transaction(issue_token_tx.clone(), &mining_receiver, ¶ms.db).await; + undo_issue_token_transaction( + issue_token_tx.clone(), + &mining_receiver, + ¶ms.db, + ) + .await; rolled_back_transactions.push(Transaction::IssueToken(issue_token_tx)); } Transaction::Nft(create_nft_tx) => { - undo_create_nft_transaction(create_nft_tx.clone(), &mining_receiver, ¶ms.db).await; + undo_create_nft_transaction( + create_nft_tx.clone(), + &mining_receiver, + ¶ms.db, + ) + .await; rolled_back_transactions.push(Transaction::Nft(create_nft_tx)); } Transaction::Marketing(marketing_tx) => { - undo_marketing_transaction(marketing_tx.clone(), &mining_receiver, ¶ms.db).await; + undo_marketing_transaction(marketing_tx.clone(), &mining_receiver, ¶ms.db) + .await; rolled_back_transactions.push(Transaction::Marketing(marketing_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)); } Transaction::Lender(loan_tx) => { - undo_loan_creation_transaction(loan_tx.clone(), &mining_receiver, ¶ms.db).await; + undo_loan_creation_transaction(loan_tx.clone(), &mining_receiver, ¶ms.db) + .await; rolled_back_transactions.push(Transaction::Lender(loan_tx)); } Transaction::Borrower(borrower_tx) => { - undo_borrower_transaction(borrower_tx.clone(), &mining_receiver, ¶ms.db).await?; + undo_borrower_transaction(borrower_tx.clone(), &mining_receiver, ¶ms.db) + .await?; rolled_back_transactions.push(Transaction::Borrower(borrower_tx)); } Transaction::Collateral(collateral_tx) => { - undo_collateral_transaction(collateral_tx.clone(), &mining_receiver, ¶ms.db).await?; + undo_collateral_transaction( + collateral_tx.clone(), + &mining_receiver, + ¶ms.db, + ) + .await?; rolled_back_transactions.push(Transaction::Collateral(collateral_tx)); } Transaction::Genesis(_) => { @@ -87,10 +116,11 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re // the requested rollback boundary is invalid. return Err( "Genesis transaction cannot be undone by orphan rollback".to_string() - ) + ); } Transaction::Vanity(vanity_tx) => { - undo_vanity_transaction(vanity_tx.clone(), &mining_receiver, ¶ms.db).await?; + undo_vanity_transaction(vanity_tx.clone(), &mining_receiver, ¶ms.db) + .await?; rolled_back_transactions.push(Transaction::Vanity(vanity_tx)); } } diff --git a/src/orphans/undo_transactions/restore_mempool.rs b/src/orphans/undo_transactions/restore_mempool.rs index 3964429..f9156bd 100644 --- a/src/orphans/undo_transactions/restore_mempool.rs +++ b/src/orphans/undo_transactions/restore_mempool.rs @@ -11,12 +11,12 @@ use crate::blocks::transfer::TransferTransaction; use crate::blocks::vanity::VanityAddressTransaction; use crate::common::nft_assets::nft_asset_name; use crate::common::types::Transaction; +use crate::decode; use crate::records::memory::mempool::{restore_processed_by_signatures, BASECOIN}; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::responses::RpcResponse; use crate::sled::Db; use crate::verifications::async_funcs::checks::balance_check::balance_checkup; -use crate::decode; async fn restore_if_spendable(signatures: &[String], spendable: bool, insert: F) where @@ -107,8 +107,14 @@ pub async fn restore_create_nft(transaction: &CreateNftTransaction, db: &Db) { pub async fn restore_marketing(transaction: &MarketingTransaction, db: &Db) { let marketing = &transaction.unsigned_marketing; - let spendable = - balance_checkup(db, 0, marketing.txfee, BASECOIN.clone(), &marketing.advertiser).await; + let spendable = balance_checkup( + db, + 0, + marketing.txfee, + BASECOIN.clone(), + &marketing.advertiser, + ) + .await; let signature = transaction.signature.clone(); 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 value1 = swap.value1.saturating_add(swap.tip1); let value2 = swap.value2.saturating_add(swap.tip2); - let sender1_spendable = - balance_checkup(db, value1, swap.txfee1, asset1, &swap.sender1).await; - let sender2_spendable = - balance_checkup(db, value2, swap.txfee2, asset2, &swap.sender2).await; - let signatures = vec![transaction.signature1.clone(), transaction.signature2.clone()]; + let sender1_spendable = balance_checkup(db, value1, swap.txfee1, asset1, &swap.sender1).await; + let sender2_spendable = balance_checkup(db, value2, swap.txfee2, asset2, &swap.sender2).await; + let signatures = vec![ + transaction.signature1.clone(), + transaction.signature2.clone(), + ]; - restore_if_spendable(&signatures, sender1_spendable && sender2_spendable, || async { - let _ = transaction.add_to_memory().await; - }) + restore_if_spendable( + &signatures, + sender1_spendable && sender2_spendable, + || async { + let _ = transaction.add_to_memory().await; + }, + ) .await; } pub async fn restore_loan_creation(transaction: &LoanContractTransaction, db: &Db) { let loan = &transaction.unsigned_loan_contract; - let lender_spendable = - balance_checkup(db, loan.loan_amount, loan.txfee, loan.loan_coin.clone(), &loan.lender) - .await; - let borrower_spendable = - balance_checkup(db, loan.collateral_amount, 0, loan.collateral.clone(), &loan.borrower) - .await; - let signatures = vec![transaction.signature1.clone(), transaction.signature2.clone()]; + let lender_spendable = balance_checkup( + db, + loan.loan_amount, + loan.txfee, + loan.loan_coin.clone(), + &loan.lender, + ) + .await; + let borrower_spendable = balance_checkup( + db, + loan.collateral_amount, + 0, + loan.collateral.clone(), + &loan.borrower, + ) + .await; + let signatures = vec![ + transaction.signature1.clone(), + transaction.signature2.clone(), + ]; - restore_if_spendable(&signatures, lender_spendable && borrower_spendable, || async { - let _ = transaction.add_to_memory().await; - }) + restore_if_spendable( + &signatures, + lender_spendable && borrower_spendable, + || async { + let _ = transaction.add_to_memory().await; + }, + ) .await; } @@ -168,8 +196,14 @@ pub async fn restore_borrower(transaction: &ContractPaymentTransaction, db: &Db) pub async fn restore_collateral(transaction: &CollateralClaimTransaction, db: &Db) { let collateral = &transaction.unsigned_collateral_claim; - let spendable = - balance_checkup(db, 0, collateral.txfee, BASECOIN.clone(), &collateral.address).await; + let spendable = balance_checkup( + db, + 0, + collateral.txfee, + BASECOIN.clone(), + &collateral.address, + ) + .await; let signature = transaction.signature.clone(); restore_if_spendable(&[signature], spendable, || async { diff --git a/src/orphans/undo_transactions/undo_burn.rs b/src/orphans/undo_transactions/undo_burn.rs index fdb0f04..9e547bc 100644 --- a/src/orphans/undo_transactions/undo_burn.rs +++ b/src/orphans/undo_transactions/undo_burn.rs @@ -1,93 +1,92 @@ -use crate::blocks::burn::BurnTransaction; -use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::blocks::burn::BurnTransaction; +use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::nft_assets::nft_asset_name; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::records::record_chain::nft_provenance::remove_nft_history_entry; -use crate::records::record_chain::token_provenance::remove_token_history_entry; -use crate::sled::Db; - -pub async fn undo_burn_transaction(transaction: BurnTransaction, mining_receiver: &str, db: &Db) { - // Reverse the burn fee and burned-asset balance movement before - // restoring the live token or NFT state back into the active chain. - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let burned_asset = nft_asset_name( - &transaction.unsigned_burn.coin, - transaction.unsigned_burn.nft_series, - ); - - // Remove the miner fee, refund the burner fee, and return the burned asset - // to the burner balance. - let _ = balance_sheet_operation_with_db( - db, - mining_receiver, - transaction.unsigned_burn.txfee, - &type_str, - operand_subtraction, - ); - let _ = balance_sheet_operation_with_db( - db, - &transaction.unsigned_burn.address, - transaction.unsigned_burn.txfee, - &type_str, - operand_addition, - ); - let _ = balance_sheet_operation_with_db( - db, - &transaction.unsigned_burn.address, - transaction.unsigned_burn.value, - &burned_asset, - operand_addition, - ); - - let hash_binary = decode(&transaction.unsigned_burn.hash().await).unwrap(); - - // Delete the txid lookup inserted when the burn was saved. - let txid_tree = db.open_tree("txid").unwrap(); - txid_tree.remove(hash_binary.clone()).unwrap(); - - // Restore NFT rows directly, or add the burned amount back into the - // fungible token supply if this burn targeted a token asset. - let nft_tree = db.open_tree("nfts").unwrap(); - let nft_origin_tree = db.open_tree("nft_origins").unwrap(); - if nft_origin_tree - .contains_key(burned_asset.as_bytes()) - .unwrap_or(false) - { - // NFT burns remove the live NFT row; rollback restores the row and - // removes the burn from NFT history. - let _ = remove_nft_history_entry(db, &burned_asset, &hash_binary); - let _ = nft_tree.insert(burned_asset.as_bytes(), b"1"); - } else { - // Token burns reduce supply; rollback adds the burned amount back. - let _ = remove_token_history_entry(db, &transaction.unsigned_burn.coin, &hash_binary); - let token_tree = db.open_tree("tokens").unwrap(); - let current_supply = token_tree - .get(transaction.unsigned_burn.coin.as_bytes()) - .unwrap() - .map(|bytes| { - let mut supply_bytes = [0u8; 8]; - supply_bytes.copy_from_slice(bytes.as_ref()); - u64::from_le_bytes(supply_bytes) - }) - .unwrap_or(0); - let restored_supply = current_supply.saturating_add(transaction.unsigned_burn.value); - let _ = token_tree.insert( - transaction.unsigned_burn.coin.as_bytes(), - &restored_supply.to_le_bytes(), - ); - } - +use crate::records::record_chain::nft_provenance::remove_nft_history_entry; +use crate::records::record_chain::token_provenance::remove_token_history_entry; +use crate::sled::Db; + +pub async fn undo_burn_transaction(transaction: BurnTransaction, mining_receiver: &str, db: &Db) { + // Reverse the burn fee and burned-asset balance movement before + // restoring the live token or NFT state back into the active chain. + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let burned_asset = nft_asset_name( + &transaction.unsigned_burn.coin, + transaction.unsigned_burn.nft_series, + ); + + // Remove the miner fee, refund the burner fee, and return the burned asset + // to the burner balance. + let _ = balance_sheet_operation_with_db( + db, + mining_receiver, + transaction.unsigned_burn.txfee, + &type_str, + operand_subtraction, + ); + let _ = balance_sheet_operation_with_db( + db, + &transaction.unsigned_burn.address, + transaction.unsigned_burn.txfee, + &type_str, + operand_addition, + ); + let _ = balance_sheet_operation_with_db( + db, + &transaction.unsigned_burn.address, + transaction.unsigned_burn.value, + &burned_asset, + operand_addition, + ); + + let hash_binary = decode(&transaction.unsigned_burn.hash().await).unwrap(); + + // Delete the txid lookup inserted when the burn was saved. + let txid_tree = db.open_tree("txid").unwrap(); + txid_tree.remove(hash_binary.clone()).unwrap(); + + // Restore NFT rows directly, or add the burned amount back into the + // fungible token supply if this burn targeted a token asset. + let nft_tree = db.open_tree("nfts").unwrap(); + let nft_origin_tree = db.open_tree("nft_origins").unwrap(); + if nft_origin_tree + .contains_key(burned_asset.as_bytes()) + .unwrap_or(false) + { + // NFT burns remove the live NFT row; rollback restores the row and + // removes the burn from NFT history. + let _ = remove_nft_history_entry(db, &burned_asset, &hash_binary); + let _ = nft_tree.insert(burned_asset.as_bytes(), b"1"); + } else { + // Token burns reduce supply; rollback adds the burned amount back. + let _ = remove_token_history_entry(db, &transaction.unsigned_burn.coin, &hash_binary); + let token_tree = db.open_tree("tokens").unwrap(); + let current_supply = token_tree + .get(transaction.unsigned_burn.coin.as_bytes()) + .unwrap() + .map(|bytes| { + let mut supply_bytes = [0u8; 8]; + supply_bytes.copy_from_slice(bytes.as_ref()); + u64::from_le_bytes(supply_bytes) + }) + .unwrap_or(0); + let restored_supply = current_supply.saturating_add(transaction.unsigned_burn.value); + let _ = token_tree.insert( + transaction.unsigned_burn.coin.as_bytes(), + &restored_supply.to_le_bytes(), + ); + } } diff --git a/src/orphans/undo_transactions/undo_create_nft.rs b/src/orphans/undo_transactions/undo_create_nft.rs index 29e5a6a..0c45f92 100644 --- a/src/orphans/undo_transactions/undo_create_nft.rs +++ b/src/orphans/undo_transactions/undo_create_nft.rs @@ -1,81 +1,80 @@ -use crate::blocks::nft::CreateNftTransaction; -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::common::nft_assets::nft_asset_name; +use crate::blocks::nft::CreateNftTransaction; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::common::nft_assets::nft_asset_name; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::records::record_chain::nft_provenance::{remove_nft_history_entry, remove_nft_origin}; -use crate::sled::Db; - -const NFT_UNIT: u64 = 100_000_000; - -pub async fn undo_create_nft_transaction( - transaction: CreateNftTransaction, - mining_receiver: &str, - db: &Db, -) { - // remove the created nft state and restore the creator balances - // when a create-nft transaction is rolled back - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let (txfee, creator) = ( - &transaction.unsigned_create_nft.txfee, - &transaction.unsigned_create_nft.creator, - ); - - // Remove the miner fee and refund the creator's base-coin fee. - let _ = balance_sheet_operation_with_db( - db, - mining_receiver, - *txfee, - &type_str, - operand_subtraction, - ); - let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition); - let hash_binary = decode(&transaction.unsigned_create_nft.hash().await).unwrap(); - - // Remove the create-NFT transaction lookup from the txid tree. - let tree = db.open_tree("txid").unwrap(); - let key = hash_binary.clone(); - tree.remove(key).unwrap(); - - // remove each created nft item and clear the - // associated provenance entries - let tree = db.open_tree("nfts").unwrap(); - if transaction.unsigned_create_nft.series == 1 { - // Series creation mints numbered items, so each item is removed - // individually from balances, provenance, origins, and the NFT tree. - 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 _ = 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_origin(db, &nft_name); - tree.remove(nft_name.as_bytes()).unwrap(); - } - } else { - // Single NFT creation only writes the base NFT name. - let nft_name = &transaction.unsigned_create_nft.nft_name; - let _ = - 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_origin(db, nft_name); - tree.remove(nft_name.as_bytes()).unwrap(); - } - +use crate::records::record_chain::nft_provenance::{remove_nft_history_entry, remove_nft_origin}; +use crate::sled::Db; + +const NFT_UNIT: u64 = 100_000_000; + +pub async fn undo_create_nft_transaction( + transaction: CreateNftTransaction, + mining_receiver: &str, + db: &Db, +) { + // remove the created nft state and restore the creator balances + // when a create-nft transaction is rolled back + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let (txfee, creator) = ( + &transaction.unsigned_create_nft.txfee, + &transaction.unsigned_create_nft.creator, + ); + + // Remove the miner fee and refund the creator's base-coin fee. + let _ = balance_sheet_operation_with_db( + db, + mining_receiver, + *txfee, + &type_str, + operand_subtraction, + ); + let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition); + let hash_binary = decode(&transaction.unsigned_create_nft.hash().await).unwrap(); + + // Remove the create-NFT transaction lookup from the txid tree. + let tree = db.open_tree("txid").unwrap(); + let key = hash_binary.clone(); + tree.remove(key).unwrap(); + + // remove each created nft item and clear the + // associated provenance entries + let tree = db.open_tree("nfts").unwrap(); + if transaction.unsigned_create_nft.series == 1 { + // Series creation mints numbered items, so each item is removed + // individually from balances, provenance, origins, and the NFT tree. + 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 _ = 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_origin(db, &nft_name); + tree.remove(nft_name.as_bytes()).unwrap(); + } + } else { + // Single NFT creation only writes the base NFT name. + let nft_name = &transaction.unsigned_create_nft.nft_name; + let _ = + 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_origin(db, nft_name); + tree.remove(nft_name.as_bytes()).unwrap(); + } } diff --git a/src/orphans/undo_transactions/undo_create_token.rs b/src/orphans/undo_transactions/undo_create_token.rs index 2f98764..6928ed4 100644 --- a/src/orphans/undo_transactions/undo_create_token.rs +++ b/src/orphans/undo_transactions/undo_create_token.rs @@ -1,69 +1,68 @@ -use crate::blocks::token::CreateTokenTransaction; -use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::blocks::token::CreateTokenTransaction; +use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::records::record_chain::token_provenance::clear_token_history; -use crate::sled::Db; - -pub async fn undo_create_token_transaction( - transaction: CreateTokenTransaction, - mining_receiver: &str, - db: &Db, -) { - // remove the created token state and restore the creator balances - // when a create-token transaction is rolled back - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let (txfee, creator, ticker, number) = ( - &transaction.unsigned_create_token.txfee, - &transaction.unsigned_create_token.creator, - &transaction.unsigned_create_token.ticker, - &transaction.unsigned_create_token.number, - ); - - // Remove the miner fee, refund the creator fee, and remove the created - // supply from the creator balance. - let _ = balance_sheet_operation_with_db( - db, - mining_receiver, - *txfee, - &type_str, - operand_subtraction, - ); - 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 ticker_binary = &transaction.unsigned_create_token.ticker.as_bytes(); - let hash_binary = decode(&transaction.unsigned_create_token.hash().await).unwrap(); - - // Remove the create-token transaction lookup from the txid tree. - let tree = db.open_tree("txid").unwrap(); - let key = hash_binary.clone(); - tree.remove(key).unwrap(); - - // remove the token definition and origin entry - // created when the token was first saved - let tree = db.open_tree("tokens").unwrap(); - let key = ticker_binary; - tree.remove(key).unwrap(); - - let origin_tree = db.open_tree("token_origins").unwrap(); - origin_tree.remove(key).unwrap(); - - let limit_tree = db.open_tree("token_limits").unwrap(); - limit_tree.remove(key).unwrap(); - // Token history is cleared because the token itself no longer exists. - let _ = clear_token_history(db, ticker); - +use crate::records::record_chain::token_provenance::clear_token_history; +use crate::sled::Db; + +pub async fn undo_create_token_transaction( + transaction: CreateTokenTransaction, + mining_receiver: &str, + db: &Db, +) { + // remove the created token state and restore the creator balances + // when a create-token transaction is rolled back + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let (txfee, creator, ticker, number) = ( + &transaction.unsigned_create_token.txfee, + &transaction.unsigned_create_token.creator, + &transaction.unsigned_create_token.ticker, + &transaction.unsigned_create_token.number, + ); + + // Remove the miner fee, refund the creator fee, and remove the created + // supply from the creator balance. + let _ = balance_sheet_operation_with_db( + db, + mining_receiver, + *txfee, + &type_str, + operand_subtraction, + ); + 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 ticker_binary = &transaction.unsigned_create_token.ticker.as_bytes(); + let hash_binary = decode(&transaction.unsigned_create_token.hash().await).unwrap(); + + // Remove the create-token transaction lookup from the txid tree. + let tree = db.open_tree("txid").unwrap(); + let key = hash_binary.clone(); + tree.remove(key).unwrap(); + + // remove the token definition and origin entry + // created when the token was first saved + let tree = db.open_tree("tokens").unwrap(); + let key = ticker_binary; + tree.remove(key).unwrap(); + + let origin_tree = db.open_tree("token_origins").unwrap(); + origin_tree.remove(key).unwrap(); + + let limit_tree = db.open_tree("token_limits").unwrap(); + limit_tree.remove(key).unwrap(); + // Token history is cleared because the token itself no longer exists. + let _ = clear_token_history(db, ticker); } diff --git a/src/orphans/undo_transactions/undo_issue_token.rs b/src/orphans/undo_transactions/undo_issue_token.rs index 6d1669b..5b7080a 100644 --- a/src/orphans/undo_transactions/undo_issue_token.rs +++ b/src/orphans/undo_transactions/undo_issue_token.rs @@ -1,62 +1,61 @@ -use crate::blocks::issue_token::IssueTokenTransaction; -use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::blocks::issue_token::IssueTokenTransaction; +use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::records::record_chain::token_provenance::remove_token_history_entry; -use crate::sled::Db; - -pub async fn undo_issue_token_transaction( - transaction: IssueTokenTransaction, - mining_receiver: &str, - db: &Db, -) { - // Reverse the issued supply and fee movements so rollback restores - // both the creator balance and the stored token supply. - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let txfee = &transaction.unsigned_issue_token.txfee; - let creator = &transaction.unsigned_issue_token.creator; - let ticker = &transaction.unsigned_issue_token.ticker; - let number = &transaction.unsigned_issue_token.number; - - // Remove the miner fee, refund the issuer fee, and take the issued amount - // back out of the issuer balance. - let _ = balance_sheet_operation_with_db( - db, - mining_receiver, - *txfee, - &type_str, - operand_subtraction, - ); - 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 hash_binary = decode(&transaction.unsigned_issue_token.hash().await).unwrap(); - - // Delete the issued-token transaction lookup and provenance record. - let txid_tree = db.open_tree("txid").unwrap(); - txid_tree.remove(hash_binary.clone()).unwrap(); - let _ = remove_token_history_entry(db, ticker, &hash_binary); - - // Restore the previous live token supply by subtracting the issued amount. - let token_tree = db.open_tree("tokens").unwrap(); - if let Ok(Some(existing_supply)) = token_tree.get(ticker.as_bytes()) { - let mut supply_bytes = [0u8; 8]; - supply_bytes.copy_from_slice(existing_supply.as_ref()); - let current_supply = u64::from_le_bytes(supply_bytes); - let restored_supply = current_supply.saturating_sub(*number); - let _ = token_tree.insert(ticker.as_bytes(), &restored_supply.to_le_bytes()); - } - +use crate::records::record_chain::token_provenance::remove_token_history_entry; +use crate::sled::Db; + +pub async fn undo_issue_token_transaction( + transaction: IssueTokenTransaction, + mining_receiver: &str, + db: &Db, +) { + // Reverse the issued supply and fee movements so rollback restores + // both the creator balance and the stored token supply. + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let txfee = &transaction.unsigned_issue_token.txfee; + let creator = &transaction.unsigned_issue_token.creator; + let ticker = &transaction.unsigned_issue_token.ticker; + let number = &transaction.unsigned_issue_token.number; + + // Remove the miner fee, refund the issuer fee, and take the issued amount + // back out of the issuer balance. + let _ = balance_sheet_operation_with_db( + db, + mining_receiver, + *txfee, + &type_str, + operand_subtraction, + ); + 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 hash_binary = decode(&transaction.unsigned_issue_token.hash().await).unwrap(); + + // Delete the issued-token transaction lookup and provenance record. + let txid_tree = db.open_tree("txid").unwrap(); + txid_tree.remove(hash_binary.clone()).unwrap(); + let _ = remove_token_history_entry(db, ticker, &hash_binary); + + // Restore the previous live token supply by subtracting the issued amount. + let token_tree = db.open_tree("tokens").unwrap(); + if let Ok(Some(existing_supply)) = token_tree.get(ticker.as_bytes()) { + let mut supply_bytes = [0u8; 8]; + supply_bytes.copy_from_slice(existing_supply.as_ref()); + let current_supply = u64::from_le_bytes(supply_bytes); + let restored_supply = current_supply.saturating_sub(*number); + let _ = token_tree.insert(ticker.as_bytes(), &restored_supply.to_le_bytes()); + } } diff --git a/src/orphans/undo_transactions/undo_loan_creation.rs b/src/orphans/undo_transactions/undo_loan_creation.rs index 949fa61..9dc1a23 100644 --- a/src/orphans/undo_transactions/undo_loan_creation.rs +++ b/src/orphans/undo_transactions/undo_loan_creation.rs @@ -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::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::records::record_chain::nft_provenance::remove_nft_history_entry; -use crate::sled::Db; - -pub async fn undo_loan_creation_transaction( - transaction: LoanContractTransaction, - mining_receiver: &str, - db: &Db, -) { - // remove a loan contract and restore both sides of the - // contract balances when the block is rolled back - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let (txfee, loan_coin, loan_amount, lender, collateral, collateral_amount, borrower, hash) = ( - &transaction.unsigned_loan_contract.txfee, - &transaction.unsigned_loan_contract.loan_coin.clone(), - &transaction.unsigned_loan_contract.loan_amount, - &transaction.unsigned_loan_contract.lender.clone(), - &transaction.unsigned_loan_contract.collateral.clone(), - &transaction.unsigned_loan_contract.collateral_amount, - &transaction.unsigned_loan_contract.borrower.clone(), - &transaction.hash.clone(), - ); - let collateral_wallet = format!("collateral_{hash}"); - - // undo the fee, loan distribution, and collateral - // movement that were applied when the contract was saved - let _ = balance_sheet_operation_with_db( - db, - mining_receiver, - *txfee, - &type_str, - operand_subtraction, - ); - let _ = balance_sheet_operation_with_db(db, lender, *txfee, &type_str, operand_addition); - let _ = - 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, - &collateral_wallet, - *collateral_amount, - collateral, - operand_subtraction, - ); - let _ = balance_sheet_operation_with_db( - db, - borrower, - *collateral_amount, - collateral, - operand_addition, - ); - - let hash_binary = decode(hash).unwrap(); - - // delete the txid and remove the active loan record - // so the contract no longer exists on-chain - let tree = db.open_tree("txid").unwrap(); - let key = hash_binary.clone(); - tree.remove(key).unwrap(); - - let tree = db.open_tree("loan").unwrap(); - let loan_key = decode(&transaction.unsigned_loan_contract.hash().await).unwrap(); - tree.remove(loan_key).unwrap(); - - let nft_tree = db.open_tree("nfts").unwrap(); - // Loan creation can move NFT collateral or NFT loan assets, so clear - // provenance entries for whichever side is NFT-backed. - if nft_tree - .contains_key(collateral.as_bytes()) - .unwrap_or(false) - { - let _ = remove_nft_history_entry(db, collateral, &hash_binary); - } - if nft_tree.contains_key(loan_coin.as_bytes()).unwrap_or(false) { - let _ = remove_nft_history_entry(db, loan_coin, &hash_binary); - } - +use crate::records::record_chain::nft_provenance::remove_nft_history_entry; +use crate::sled::Db; + +pub async fn undo_loan_creation_transaction( + transaction: LoanContractTransaction, + mining_receiver: &str, + db: &Db, +) { + // remove a loan contract and restore both sides of the + // contract balances when the block is rolled back + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let (txfee, loan_coin, loan_amount, lender, collateral, collateral_amount, borrower, hash) = ( + &transaction.unsigned_loan_contract.txfee, + &transaction.unsigned_loan_contract.loan_coin.clone(), + &transaction.unsigned_loan_contract.loan_amount, + &transaction.unsigned_loan_contract.lender.clone(), + &transaction.unsigned_loan_contract.collateral.clone(), + &transaction.unsigned_loan_contract.collateral_amount, + &transaction.unsigned_loan_contract.borrower.clone(), + &transaction.hash.clone(), + ); + let collateral_wallet = format!("collateral_{hash}"); + + // undo the fee, loan distribution, and collateral + // movement that were applied when the contract was saved + let _ = balance_sheet_operation_with_db( + db, + mining_receiver, + *txfee, + &type_str, + operand_subtraction, + ); + let _ = balance_sheet_operation_with_db(db, lender, *txfee, &type_str, operand_addition); + let _ = + 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, + &collateral_wallet, + *collateral_amount, + collateral, + operand_subtraction, + ); + let _ = balance_sheet_operation_with_db( + db, + borrower, + *collateral_amount, + collateral, + operand_addition, + ); + + let hash_binary = decode(hash).unwrap(); + + // delete the txid and remove the active loan record + // so the contract no longer exists on-chain + let tree = db.open_tree("txid").unwrap(); + let key = hash_binary.clone(); + tree.remove(key).unwrap(); + + let tree = db.open_tree("loan").unwrap(); + let loan_key = decode(&transaction.unsigned_loan_contract.hash().await).unwrap(); + tree.remove(loan_key).unwrap(); + + let nft_tree = db.open_tree("nfts").unwrap(); + // Loan creation can move NFT collateral or NFT loan assets, so clear + // provenance entries for whichever side is NFT-backed. + if nft_tree + .contains_key(collateral.as_bytes()) + .unwrap_or(false) + { + let _ = remove_nft_history_entry(db, collateral, &hash_binary); + } + if nft_tree.contains_key(loan_coin.as_bytes()).unwrap_or(false) { + let _ = remove_nft_history_entry(db, loan_coin, &hash_binary); + } } diff --git a/src/orphans/undo_transactions/undo_marketing.rs b/src/orphans/undo_transactions/undo_marketing.rs index 3f2cbde..c077dd0 100644 --- a/src/orphans/undo_transactions/undo_marketing.rs +++ b/src/orphans/undo_transactions/undo_marketing.rs @@ -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::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::sled::Db; - -pub async fn undo_marketing_transaction( - transaction: MarketingTransaction, - mining_receiver: &str, - db: &Db, -) { - // reverse the fee payment and remove the marketing txid - // when a marketing transaction is rolled back - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let (txfee, advertiser) = ( - &transaction.unsigned_marketing.txfee, - &transaction.unsigned_marketing.advertiser, - ); - - // Remove the miner fee and refund the advertiser fee. - let _ = balance_sheet_operation_with_db( - db, - mining_receiver, - *txfee, - &type_str, - operand_subtraction, - ); - let _ = balance_sheet_operation_with_db(db, advertiser, *txfee, &type_str, operand_addition); - - let hash = decode(&transaction.unsigned_marketing.hash().await).unwrap(); - - // Remove the marketing transaction lookup from the txid tree. - let tree = db.open_tree("txid").unwrap(); - let key = hash; - tree.remove(key).unwrap(); - +use crate::sled::Db; + +pub async fn undo_marketing_transaction( + transaction: MarketingTransaction, + mining_receiver: &str, + db: &Db, +) { + // reverse the fee payment and remove the marketing txid + // when a marketing transaction is rolled back + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let (txfee, advertiser) = ( + &transaction.unsigned_marketing.txfee, + &transaction.unsigned_marketing.advertiser, + ); + + // Remove the miner fee and refund the advertiser fee. + let _ = balance_sheet_operation_with_db( + db, + mining_receiver, + *txfee, + &type_str, + operand_subtraction, + ); + let _ = balance_sheet_operation_with_db(db, advertiser, *txfee, &type_str, operand_addition); + + let hash = decode(&transaction.unsigned_marketing.hash().await).unwrap(); + + // Remove the marketing transaction lookup from the txid tree. + let tree = db.open_tree("txid").unwrap(); + let key = hash; + tree.remove(key).unwrap(); } diff --git a/src/orphans/undo_transactions/undo_rewards.rs b/src/orphans/undo_transactions/undo_rewards.rs index c2a221e..38a5b24 100644 --- a/src/orphans/undo_transactions/undo_rewards.rs +++ b/src/orphans/undo_transactions/undo_rewards.rs @@ -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::decode; 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; pub async fn undo_rewards_transaction( @@ -11,34 +13,34 @@ pub async fn undo_rewards_transaction( db: &Db, block_height: u32, ) { - // remove the miner reward and delete the reward txid - // when the block that minted it is rolled back - let operand = "subtraction"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - - let value = transaction.unsigned.value; - + // remove the miner reward and delete the reward txid + // when the block that minted it is rolled back + let operand = "subtraction"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + + let value = transaction.unsigned.value; + // Rewards are only spendable after finalization, so rollback subtracts // them only when that delayed credit has actually been applied. if reward_credit_applied(db, block_height) { let _ = balance_sheet_operation_with_db(db, mining_receiver, value, &type_str, operand); remove_reward_credit_marker(db, block_height); } - - let hash = decode(transaction.unsigned.hash().await).unwrap(); - - // Remove the reward transaction lookup from the txid tree. - let tree = db.open_tree("txid").unwrap(); - let key = hash; - tree.remove(key).unwrap(); -} + + let hash = decode(transaction.unsigned.hash().await).unwrap(); + + // Remove the reward transaction lookup from the txid tree. + let tree = db.open_tree("txid").unwrap(); + let key = hash; + tree.remove(key).unwrap(); +} diff --git a/src/orphans/undo_transactions/undo_swap.rs b/src/orphans/undo_transactions/undo_swap.rs index ae112c8..b411766 100644 --- a/src/orphans/undo_transactions/undo_swap.rs +++ b/src/orphans/undo_transactions/undo_swap.rs @@ -1,103 +1,102 @@ -use crate::blocks::swap::SwapTransaction; -use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::blocks::swap::SwapTransaction; +use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::nft_assets::nft_asset_name; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::records::record_chain::nft_provenance::remove_nft_history_entry; -use crate::sled::Db; - -pub async fn undo_swap_transaction(transaction: SwapTransaction, mining_receiver: &str, db: &Db) { - // reverse both sides of the asset exchange and remove the - // swap transaction from chain state during rollback - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let ( - txfee1, - txfee2, - value1, - value2, - ticker1, - ticker2, - nft_series1, - nft_series2, - sender1, - sender2, - tip1, - tip2, - ) = ( - &transaction.unsigned_swap.txfee1, - &transaction.unsigned_swap.txfee2, - &transaction.unsigned_swap.value1, - &transaction.unsigned_swap.value2, - &transaction.unsigned_swap.ticker1, - &transaction.unsigned_swap.ticker2, - &transaction.unsigned_swap.nft_series1, - &transaction.unsigned_swap.nft_series2, - &transaction.unsigned_swap.sender1, - &transaction.unsigned_swap.sender2, - &transaction.unsigned_swap.tip1, - &transaction.unsigned_swap.tip2, - ); - let asset1 = nft_asset_name(ticker1, *nft_series1); - let asset2 = nft_asset_name(ticker2, *nft_series2); - - // Refund both base-coin fees and remove those fees from the miner balance. - let _ = balance_sheet_operation_with_db( - db, - mining_receiver, - *txfee1, - &type_str, - operand_subtraction, - ); - let _ = balance_sheet_operation_with_db(db, sender1, *txfee1, &type_str, operand_addition); - let _ = balance_sheet_operation_with_db( - db, - mining_receiver, - *txfee2, - &type_str, - operand_subtraction, - ); - 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. - let _ = - 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, mining_receiver, *tip2, &asset2, operand_subtraction); - let _ = balance_sheet_operation_with_db(db, sender2, *tip2, &asset2, operand_addition); - // 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, sender2, *value2, &asset2, operand_addition); - let _ = balance_sheet_operation_with_db(db, sender2, *value1, &asset1, operand_subtraction); - let _ = balance_sheet_operation_with_db(db, sender1, *value1, &asset1, operand_addition); - - // Convert the txid hash back to bytes for tree lookup/removal. - let hash = decode(&transaction.unsigned_swap.hash().await).unwrap(); - - // Remove the txid lookup for the rolled-back swap. - let tree = db.open_tree("txid").unwrap(); - let key = hash.clone(); - tree.remove(key).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 - // provenance history as well. - if nft_tree.contains_key(asset1.as_bytes()).unwrap_or(false) { - let _ = remove_nft_history_entry(db, &asset1, &hash); - } - if nft_tree.contains_key(asset2.as_bytes()).unwrap_or(false) { - let _ = remove_nft_history_entry(db, &asset2, &hash); - } - +use crate::records::record_chain::nft_provenance::remove_nft_history_entry; +use crate::sled::Db; + +pub async fn undo_swap_transaction(transaction: SwapTransaction, mining_receiver: &str, db: &Db) { + // reverse both sides of the asset exchange and remove the + // swap transaction from chain state during rollback + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let ( + txfee1, + txfee2, + value1, + value2, + ticker1, + ticker2, + nft_series1, + nft_series2, + sender1, + sender2, + tip1, + tip2, + ) = ( + &transaction.unsigned_swap.txfee1, + &transaction.unsigned_swap.txfee2, + &transaction.unsigned_swap.value1, + &transaction.unsigned_swap.value2, + &transaction.unsigned_swap.ticker1, + &transaction.unsigned_swap.ticker2, + &transaction.unsigned_swap.nft_series1, + &transaction.unsigned_swap.nft_series2, + &transaction.unsigned_swap.sender1, + &transaction.unsigned_swap.sender2, + &transaction.unsigned_swap.tip1, + &transaction.unsigned_swap.tip2, + ); + let asset1 = nft_asset_name(ticker1, *nft_series1); + let asset2 = nft_asset_name(ticker2, *nft_series2); + + // Refund both base-coin fees and remove those fees from the miner balance. + let _ = balance_sheet_operation_with_db( + db, + mining_receiver, + *txfee1, + &type_str, + operand_subtraction, + ); + let _ = balance_sheet_operation_with_db(db, sender1, *txfee1, &type_str, operand_addition); + let _ = balance_sheet_operation_with_db( + db, + mining_receiver, + *txfee2, + &type_str, + operand_subtraction, + ); + 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. + let _ = + 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, mining_receiver, *tip2, &asset2, operand_subtraction); + let _ = balance_sheet_operation_with_db(db, sender2, *tip2, &asset2, operand_addition); + // 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, sender2, *value2, &asset2, operand_addition); + let _ = balance_sheet_operation_with_db(db, sender2, *value1, &asset1, operand_subtraction); + let _ = balance_sheet_operation_with_db(db, sender1, *value1, &asset1, operand_addition); + + // Convert the txid hash back to bytes for tree lookup/removal. + let hash = decode(&transaction.unsigned_swap.hash().await).unwrap(); + + // Remove the txid lookup for the rolled-back swap. + let tree = db.open_tree("txid").unwrap(); + let key = hash.clone(); + tree.remove(key).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 + // provenance history as well. + if nft_tree.contains_key(asset1.as_bytes()).unwrap_or(false) { + let _ = remove_nft_history_entry(db, &asset1, &hash); + } + if nft_tree.contains_key(asset2.as_bytes()).unwrap_or(false) { + let _ = remove_nft_history_entry(db, &asset2, &hash); + } } diff --git a/src/orphans/undo_transactions/undo_transfer.rs b/src/orphans/undo_transactions/undo_transfer.rs index 0548681..a39cf1c 100644 --- a/src/orphans/undo_transactions/undo_transfer.rs +++ b/src/orphans/undo_transactions/undo_transfer.rs @@ -1,71 +1,70 @@ -use crate::blocks::transfer::TransferTransaction; -use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::blocks::transfer::TransferTransaction; +use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::nft_assets::nft_asset_name; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; -use crate::records::record_chain::nft_provenance::remove_nft_history_entry; -use crate::sled::Db; - -pub async fn undo_transfer_transaction( - transaction: TransferTransaction, - mining_receiver: &str, - db: &Db, -) { - // reverse the transfer and fee movements, then remove the - // transfer transaction from chain state during rollback - let operand_subtraction = "subtraction"; - let operand_addition = "addition"; - let ( - _network_name, - _padded_base_coin, - type_str, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let (coin, nft_series, receiver, sender, value, txfee) = ( - &transaction.unsigned_transfer.coin, - &transaction.unsigned_transfer.nft_series, - &transaction.unsigned_transfer.receiver, - &transaction.unsigned_transfer.sender, - &transaction.unsigned_transfer.value, - &transaction.unsigned_transfer.txfee, - ); - let transfer_asset = nft_asset_name(coin, *nft_series); - - // Remove the miner fee, return the transferred asset to the sender, and - // refund the sender's base-coin fee. - let _ = balance_sheet_operation_with_db( - db, - mining_receiver, - *txfee, - &type_str, - operand_subtraction, - ); - let _ = - 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, *txfee, &type_str, operand_addition); - - let hash = decode(&transaction.unsigned_transfer.hash().await).unwrap(); - - // Remove the txid lookup so the rolled-back transfer no longer resolves as - // an on-chain transaction. - let tree = db.open_tree("txid").unwrap(); - let key = hash.clone(); - tree.remove(key).unwrap(); - - // NFT transfers also write provenance, so remove this transfer from the - // asset history if the transferred asset is an NFT. - let nft_tree = db.open_tree("nfts").unwrap(); - if nft_tree - .contains_key(transfer_asset.as_bytes()) - .unwrap_or(false) - { - let _ = remove_nft_history_entry(db, &transfer_asset, &hash); - } - +use crate::records::record_chain::nft_provenance::remove_nft_history_entry; +use crate::sled::Db; + +pub async fn undo_transfer_transaction( + transaction: TransferTransaction, + mining_receiver: &str, + db: &Db, +) { + // reverse the transfer and fee movements, then remove the + // transfer transaction from chain state during rollback + let operand_subtraction = "subtraction"; + let operand_addition = "addition"; + let ( + _network_name, + _padded_base_coin, + type_str, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let (coin, nft_series, receiver, sender, value, txfee) = ( + &transaction.unsigned_transfer.coin, + &transaction.unsigned_transfer.nft_series, + &transaction.unsigned_transfer.receiver, + &transaction.unsigned_transfer.sender, + &transaction.unsigned_transfer.value, + &transaction.unsigned_transfer.txfee, + ); + let transfer_asset = nft_asset_name(coin, *nft_series); + + // Remove the miner fee, return the transferred asset to the sender, and + // refund the sender's base-coin fee. + let _ = balance_sheet_operation_with_db( + db, + mining_receiver, + *txfee, + &type_str, + operand_subtraction, + ); + let _ = + 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, *txfee, &type_str, operand_addition); + + let hash = decode(&transaction.unsigned_transfer.hash().await).unwrap(); + + // Remove the txid lookup so the rolled-back transfer no longer resolves as + // an on-chain transaction. + let tree = db.open_tree("txid").unwrap(); + let key = hash.clone(); + tree.remove(key).unwrap(); + + // NFT transfers also write provenance, so remove this transfer from the + // asset history if the transferred asset is an NFT. + let nft_tree = db.open_tree("nfts").unwrap(); + if nft_tree + .contains_key(transfer_asset.as_bytes()) + .unwrap_or(false) + { + let _ = remove_nft_history_entry(db, &transfer_asset, &hash); + } } diff --git a/src/records/balance_sheet/get_wallet_balance.rs b/src/records/balance_sheet/get_wallet_balance.rs index ea16558..f7e7efe 100644 --- a/src/records/balance_sheet/get_wallet_balance.rs +++ b/src/records/balance_sheet/get_wallet_balance.rs @@ -46,11 +46,7 @@ pub async fn get_balance_with_db( // Balance queries accept vanity/registered aliases, but storage always // resolves back to the canonical short address. let canonical_address = resolve_canonical_registered_short_address(db, address) - .map_err(|err| { - io::Error::other( - format!("Wallet registry lookup failed: {err}"), - ) - })? + .map_err(|err| io::Error::other(format!("Wallet registry lookup failed: {err}")))? .unwrap_or_else(|| address.to_string()); get_balance(&canonical_address, coin_type).await diff --git a/src/records/balance_sheet/operations.rs b/src/records/balance_sheet/operations.rs index 3ffa227..77a4e17 100644 --- a/src/records/balance_sheet/operations.rs +++ b/src/records/balance_sheet/operations.rs @@ -69,9 +69,7 @@ pub fn balance_sheet_operation( let mut file_balance = if file_exists { // Existing balances are stored as a single little-endian u64. file.read_exact(&mut buffer).map_err(|e| { - eprintln!( - "Error reading file balance_sheet address {address}: {e}" - ); + eprintln!("Error reading file balance_sheet address {address}: {e}"); e })?; 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 // address before the filesystem balance is updated. let canonical_address = resolve_canonical_registered_short_address(db, address) - .map_err(|err| { - io::Error::other( - format!("Wallet registry lookup failed: {err}"), - ) - })? + .map_err(|err| io::Error::other(format!("Wallet registry lookup failed: {err}")))? .unwrap_or_else(|| address.to_string()); balance_sheet_operation(&canonical_address, balance, coin_type, operand) diff --git a/src/records/memory/connections.rs b/src/records/memory/connections.rs index 621c1c8..bcc28cc 100644 --- a/src/records/memory/connections.rs +++ b/src/records/memory/connections.rs @@ -1,101 +1,97 @@ use crate::common::binary_conversions::{binary_to_ip, ip_to_binary}; -use crate::lazy_static; -use crate::log::{info, warn}; -use crate::records::memory::enums::{ClientType, ConnectionType}; -use crate::records::memory::response_channels::{delete_entry, reserve_entry, Command}; +use crate::lazy_static; +use crate::log::{info, warn}; +use crate::records::memory::enums::{ClientType, ConnectionType}; +use crate::records::memory::response_channels::{delete_entry, reserve_entry, Command}; use crate::records::memory::structs::{Connection, StoreConnectionParams}; use crate::rpc::client::handshake_processing::{bootstrap_peer_discovery, BootstrapParams}; -use crate::rpc::command_maps::RPC_BLOCK_HEIGHT; -use crate::rpc::responses::RpcResponse; -use crate::sled::Db; -use crate::sleep; -use crate::thread_rng; -use crate::timeout; -use crate::wallets::structures::Wallet; -use crate::Arc; -use crate::AsyncWriteExt; -use crate::AtomicBool; -use crate::AtomicOrdering; -use crate::Duration; -use crate::IteratorRandom; -use crate::Mutex; -use crate::RwLock; +use crate::rpc::command_maps::RPC_BLOCK_HEIGHT; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::sleep; +use crate::thread_rng; +use crate::timeout; +use crate::wallets::structures::Wallet; +use crate::Arc; +use crate::AsyncWriteExt; +use crate::AtomicBool; +use crate::AtomicOrdering; +use crate::Duration; +use crate::IteratorRandom; +use crate::Mutex; +use crate::RwLock; use crate::TcpStream; - -fn split_ip_port_key(value: &str) -> Option<(String, u16)> { - // Connection keys are stored as ip:port strings; IPv6 addresses may arrive - // bracketed, so strip brackets before parsing the port. - let (ip_part, port_part) = value.rsplit_once(':')?; - let ip = ip_part - .strip_prefix('[') - .and_then(|inner| inner.strip_suffix(']')) - .unwrap_or(ip_part) - .to_string(); - let port = port_part.parse::().ok()?; - Some((ip, port)) -} - -use crate::records::memory::structs::{ConnectionInfo, ConnectionKey}; - -#[derive(Clone)] -struct ReconnectContext { - db: Db, - wallet_key: String, - map: Arc>, -} - -lazy_static! { - static ref RECONNECT_CONTEXT: Mutex> = Mutex::new(None); - static ref RECONNECT_IN_PROGRESS: AtomicBool = AtomicBool::new(false); -} - -fn try_start_reconnect() -> bool { - // Only one reconnect path should run at a time, whether it came from - // liveness failure or bootstrap recovery. - RECONNECT_IN_PROGRESS - .compare_exchange(false, true, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst) - .is_ok() -} - -fn finish_reconnect() { - // Release the reconnect gate after the async reconnect attempt finishes. - RECONNECT_IN_PROGRESS.store(false, AtomicOrdering::SeqCst); -} - -pub async fn set_reconnect_context( - db: Db, - wallet_key: String, - map: Arc>, -) { - let mut context = RECONNECT_CONTEXT.lock().await; - // Store enough state for later liveness checks to reconnect without - // needing the original startup stack. - *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 { - // 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 = { - let guard = RECONNECT_CONTEXT.lock().await; - guard.clone() - }; - - let Some(context) = context else { - warn!("[reconnect] no reconnect context configured"); - return; - }; - + +fn split_ip_port_key(value: &str) -> Option<(String, u16)> { + // Connection keys are stored as ip:port strings; IPv6 addresses may arrive + // bracketed, so strip brackets before parsing the port. + let (ip_part, port_part) = value.rsplit_once(':')?; + let ip = ip_part + .strip_prefix('[') + .and_then(|inner| inner.strip_suffix(']')) + .unwrap_or(ip_part) + .to_string(); + let port = port_part.parse::().ok()?; + Some((ip, port)) +} + +use crate::records::memory::structs::{ConnectionInfo, ConnectionKey}; + +#[derive(Clone)] +struct ReconnectContext { + db: Db, + wallet_key: String, + map: Arc>, +} + +lazy_static! { + static ref RECONNECT_CONTEXT: Mutex> = Mutex::new(None); + static ref RECONNECT_IN_PROGRESS: AtomicBool = AtomicBool::new(false); +} + +fn try_start_reconnect() -> bool { + // Only one reconnect path should run at a time, whether it came from + // liveness failure or bootstrap recovery. + RECONNECT_IN_PROGRESS + .compare_exchange(false, true, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst) + .is_ok() +} + +fn finish_reconnect() { + // Release the reconnect gate after the async reconnect attempt finishes. + RECONNECT_IN_PROGRESS.store(false, AtomicOrdering::SeqCst); +} + +pub async fn set_reconnect_context(db: Db, wallet_key: String, map: Arc>) { + let mut context = RECONNECT_CONTEXT.lock().await; + // Store enough state for later liveness checks to reconnect without + // needing the original startup stack. + *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 { + // 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 = { + let guard = RECONNECT_CONTEXT.lock().await; + guard.clone() + }; + + let Some(context) = context else { + warn!("[reconnect] no reconnect context configured"); + return; + }; + let excluded_ip_bytes = ip_to_binary(excluded_ip); let live_connection = { 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 { warn!("[reconnect] bootstrap recovery failed: {err}"); } - } - .await; - - finish_reconnect(); -} - -pub fn spawn_reconnect_bootstrap(params: BootstrapParams) { - if !try_start_reconnect() { - warn!("[reconnect] bootstrap recovery already in progress, skipping duplicate request"); - return; - } - - // Bootstrap discovery can perform network requests, so it runs detached - // from the caller that noticed the connection problem. - tokio::spawn(async move { - if let Err(err) = bootstrap_peer_discovery(params).await { - warn!("[reconnect] bootstrap recovery failed: {err}"); - } - finish_reconnect(); - }); -} - -impl Connection { - // Initialize the in-memory connection manager state. - pub fn new() -> Self { - Self::default() - } - - // Store a live socket in memory along with its role, peer identity, - // and session metadata used by the RPC and peer-management paths. + } + .await; + + finish_reconnect(); +} + +pub fn spawn_reconnect_bootstrap(params: BootstrapParams) { + if !try_start_reconnect() { + warn!("[reconnect] bootstrap recovery already in progress, skipping duplicate request"); + return; + } + + // Bootstrap discovery can perform network requests, so it runs detached + // from the caller that noticed the connection problem. + tokio::spawn(async move { + if let Err(err) = bootstrap_peer_discovery(params).await { + warn!("[reconnect] bootstrap recovery failed: {err}"); + } + finish_reconnect(); + }); +} + +impl Connection { + // Initialize the in-memory connection manager state. + pub fn new() -> Self { + Self::default() + } + + // Store a live socket in memory along with its role, peer identity, + // and session metadata used by the RPC and peer-management paths. pub fn store_connection(&mut self, params: StoreConnectionParams) -> bool { let StoreConnectionParams { connection_type, @@ -172,101 +168,101 @@ impl Connection { command_map, } = params; - let ip_bytes = ip_to_binary(&ip); - let connection_key = ConnectionKey { - connection_type: connection_type.as_bytes(), - ip: ip_bytes.clone(), - port, - }; - - let connection_key2 = ConnectionKey { - connection_type: connection_type.opposite().as_bytes(), - ip: ip_bytes.clone(), - port, - }; - - // Miner nodes are identified by IP, not by port. A second node - // announcing the same IP is rejected even if it uses another - // socket port. - if client_type == ClientType::Miner - && self.connection_map.iter().any(|(key, info)| { - key.ip == ip_bytes - && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) - }) - { - return false; - } - - // Non-miner RPC clients still use the full socket key so short - // request/response connections do not collide unnecessarily. - if self.connection_map.contains_key(&connection_key) - || self.connection_map.contains_key(&connection_key2) - { - return false; - } - - let address = Wallet::long_address_to_bytes(wallet_address); - if address.len() != Wallet::ADDRESS_BYTES_LENGTH { - return false; - } - let connection_info = ConnectionInfo::new( - connection_type.as_bytes(), - ip_bytes, - port, - stream.clone(), - client_type.as_bytes(), - address, - ); - self.connection_map.insert(connection_key, connection_info); - - if client_type == ClientType::Miner { - Connection::client_checkup(stream, connection_type, ip, port, command_map); - } - true - } - - // Remove a specific connection entry by direction, IP, and port. - pub fn drop_connection( - &mut self, - connection_type: ConnectionType, - ip: String, - port: u16, - ) -> Option { - let ip_bytes = ip_to_binary(&ip); - let connection_key = ConnectionKey { - connection_type: connection_type.as_bytes(), - ip: ip_bytes, - port, - }; - let removed = self.connection_map.remove(&connection_key); - if let Some(connection_info) = removed.as_ref() { - let stream = Arc::clone(&connection_info.stream); - tokio::spawn(async move { - let mut stream_guard = stream.lock().await; - let _ = stream_guard.shutdown().await; - }); - } - if let Some(connection_info) = removed.as_ref() { - let client_role = ClientType::from_bytes(&connection_info.client_type) - .map(|client_type| client_type.as_str()) - .unwrap_or("unknown"); - info!( - "[connection_manager] connection dropped: role={} direction={} peer={}:{}", - client_role, - connection_type.as_str(), - ip, - port - ); - } - removed - } - - pub fn client_checkup( - stream: Arc>, - connection_type: ConnectionType, - ip: String, - port: u16, - command_map: Arc>, + let ip_bytes = ip_to_binary(&ip); + let connection_key = ConnectionKey { + connection_type: connection_type.as_bytes(), + ip: ip_bytes.clone(), + port, + }; + + let connection_key2 = ConnectionKey { + connection_type: connection_type.opposite().as_bytes(), + ip: ip_bytes.clone(), + port, + }; + + // Miner nodes are identified by IP, not by port. A second node + // announcing the same IP is rejected even if it uses another + // socket port. + if client_type == ClientType::Miner + && self.connection_map.iter().any(|(key, info)| { + key.ip == ip_bytes + && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) + }) + { + return false; + } + + // Non-miner RPC clients still use the full socket key so short + // request/response connections do not collide unnecessarily. + if self.connection_map.contains_key(&connection_key) + || self.connection_map.contains_key(&connection_key2) + { + return false; + } + + let address = Wallet::long_address_to_bytes(wallet_address); + if address.len() != Wallet::ADDRESS_BYTES_LENGTH { + return false; + } + let connection_info = ConnectionInfo::new( + connection_type.as_bytes(), + ip_bytes, + port, + stream.clone(), + client_type.as_bytes(), + address, + ); + self.connection_map.insert(connection_key, connection_info); + + if client_type == ClientType::Miner { + Connection::client_checkup(stream, connection_type, ip, port, command_map); + } + true + } + + // Remove a specific connection entry by direction, IP, and port. + pub fn drop_connection( + &mut self, + connection_type: ConnectionType, + ip: String, + port: u16, + ) -> Option { + let ip_bytes = ip_to_binary(&ip); + let connection_key = ConnectionKey { + connection_type: connection_type.as_bytes(), + ip: ip_bytes, + port, + }; + let removed = self.connection_map.remove(&connection_key); + if let Some(connection_info) = removed.as_ref() { + let stream = Arc::clone(&connection_info.stream); + tokio::spawn(async move { + let mut stream_guard = stream.lock().await; + let _ = stream_guard.shutdown().await; + }); + } + if let Some(connection_info) = removed.as_ref() { + let client_role = ClientType::from_bytes(&connection_info.client_type) + .map(|client_type| client_type.as_str()) + .unwrap_or("unknown"); + info!( + "[connection_manager] connection dropped: role={} direction={} peer={}:{}", + client_role, + connection_type.as_str(), + ip, + port + ); + } + removed + } + + pub fn client_checkup( + stream: Arc>, + connection_type: ConnectionType, + ip: String, + port: u16, + command_map: Arc>, ) { tokio::spawn(async move { loop { @@ -274,39 +270,39 @@ impl Connection { let still_registered = { let guard = CONNECTIONS.read().await; guard - .as_ref() - .map(|conn| { - let connection_key = ConnectionKey { - connection_type: connection_type.as_bytes(), - ip: ip_to_binary(&ip), - port, - }; - conn.connection_map.contains_key(&connection_key) - }) - .unwrap_or(false) - }; - - if !still_registered { - break; - } - - let message_type = RPC_BLOCK_HEIGHT; // Block-height request used as a lightweight checkup ping. - let (checkup_key, _checkup_tx, checkup_rx_mutex) = - reserve_entry(command_map.clone()).await; - - // Send a lightweight ping message and wait for the reply - // routed back through the shared response hashmap. - let mut message: Vec = Vec::with_capacity(4); - message.push(message_type); - message.extend_from_slice(&checkup_key); - - RpcResponse::send_raw(&stream, None, &message).await; - - let response_result = { - let mut checkup_rx = checkup_rx_mutex.lock().await; - timeout(Duration::from_secs(30), checkup_rx.recv()).await - }; - + .as_ref() + .map(|conn| { + let connection_key = ConnectionKey { + connection_type: connection_type.as_bytes(), + ip: ip_to_binary(&ip), + port, + }; + conn.connection_map.contains_key(&connection_key) + }) + .unwrap_or(false) + }; + + if !still_registered { + break; + } + + let message_type = RPC_BLOCK_HEIGHT; // Block-height request used as a lightweight checkup ping. + let (checkup_key, _checkup_tx, checkup_rx_mutex) = + reserve_entry(command_map.clone()).await; + + // Send a lightweight ping message and wait for the reply + // routed back through the shared response hashmap. + let mut message: Vec = Vec::with_capacity(4); + message.push(message_type); + message.extend_from_slice(&checkup_key); + + RpcResponse::send_raw(&stream, None, &message).await; + + let response_result = { + let mut checkup_rx = checkup_rx_mutex.lock().await; + timeout(Duration::from_secs(30), checkup_rx.recv()).await + }; + match response_result { Ok(Some(_reply)) => { info!( @@ -314,26 +310,26 @@ impl Connection { connection_type.as_str(), ip, port - ); - } - _ => { - let still_registered = { - let guard = CONNECTIONS.read().await; - guard - .as_ref() - .map(|conn| { - let connection_key = ConnectionKey { - connection_type: connection_type.as_bytes(), - ip: ip_to_binary(&ip), - port, - }; - conn.connection_map.contains_key(&connection_key) - }) - .unwrap_or(false) - }; - - if !still_registered { - delete_entry(command_map.clone(), checkup_key).await; + ); + } + _ => { + let still_registered = { + let guard = CONNECTIONS.read().await; + guard + .as_ref() + .map(|conn| { + let connection_key = ConnectionKey { + connection_type: connection_type.as_bytes(), + ip: ip_to_binary(&ip), + port, + }; + conn.connection_map.contains_key(&connection_key) + }) + .unwrap_or(false) + }; + + if !still_registered { + delete_entry(command_map.clone(), checkup_key).await; break; } @@ -350,77 +346,75 @@ impl Connection { if let Some(conn) = guard.as_mut() { conn.drop_connection(connection_type, ip.clone(), port); } - drop(guard); - if connection_type == ConnectionType::Outgoing { - reconnect_dropped_outgoing(&ip).await; - } - break; - } - } - } - }); - } - - // Count active incoming peer connections. - pub fn count_incoming_connections(&self) -> usize { - self.connection_map - .values() - .filter(|info| { - ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Incoming) - }) - .count() - } - - // Count active outgoing peer connections. - pub fn count_outgoing_connections(&self) -> usize { - self.connection_map - .values() - .filter(|info| { - ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Outgoing) - }) - .count() - } - - // Return all live peer streams so broadcast-style paths can fan out - // messages without caring whether a peer was incoming or outgoing. - pub fn get_all_streams(&self) -> Vec>> { - self.connection_map - .values() - .filter(|connection_info| { - ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner) - }) - .map(|connection_info| Arc::clone(&connection_info.stream)) - .collect() - } - - // Return all non-client peer streams so network-wide broadcasts can - // reach every reachable chain peer. - pub fn get_all_peer_streams(&self) -> Vec>> { - self.connection_map - .values() - .filter(|connection_info| { - ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner) - }) - .map(|connection_info| Arc::clone(&connection_info.stream)) - .collect() - } - - // Resolve a stored outgoing node connection back to its live stream. - pub fn get_stream_for_outgoing(&self, ip: &str, port: u16) -> Option>> { - let ip_bytes = ip_to_binary(ip); - let connection_key = ConnectionKey { - connection_type: ConnectionType::Outgoing.as_bytes(), - ip: ip_bytes, - port, - }; - self.connection_map - .get(&connection_key) - .filter(|info| { - ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) - }) - .map(|info| Arc::clone(&info.stream)) - } - + drop(guard); + if connection_type == ConnectionType::Outgoing { + reconnect_dropped_outgoing(&ip).await; + } + break; + } + } + } + }); + } + + // Count active incoming peer connections. + pub fn count_incoming_connections(&self) -> usize { + self.connection_map + .values() + .filter(|info| { + ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Incoming) + }) + .count() + } + + // Count active outgoing peer connections. + pub fn count_outgoing_connections(&self) -> usize { + self.connection_map + .values() + .filter(|info| { + ConnectionType::from_bytes(&info.connection_type) == Some(ConnectionType::Outgoing) + }) + .count() + } + + // Return all live peer streams so broadcast-style paths can fan out + // messages without caring whether a peer was incoming or outgoing. + pub fn get_all_streams(&self) -> Vec>> { + self.connection_map + .values() + .filter(|connection_info| { + ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner) + }) + .map(|connection_info| Arc::clone(&connection_info.stream)) + .collect() + } + + // Return all non-client peer streams so network-wide broadcasts can + // reach every reachable chain peer. + pub fn get_all_peer_streams(&self) -> Vec>> { + self.connection_map + .values() + .filter(|connection_info| { + ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner) + }) + .map(|connection_info| Arc::clone(&connection_info.stream)) + .collect() + } + + // Resolve a stored outgoing node connection back to its live stream. + pub fn get_stream_for_outgoing(&self, ip: &str, port: u16) -> Option>> { + let ip_bytes = ip_to_binary(ip); + let connection_key = ConnectionKey { + connection_type: ConnectionType::Outgoing.as_bytes(), + ip: ip_bytes, + port, + }; + self.connection_map + .get(&connection_key) + .filter(|info| ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)) + .map(|info| Arc::clone(&info.stream)) + } + // 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 // to select an arbitrary live socket. @@ -430,16 +424,18 @@ impl Connection { let conn = lock.as_ref()?; let ip_bytes = ip_to_binary(&ip); - conn.connection_map.iter().find_map(|(connection_key, info)| { - if connection_key.ip == ip_bytes - && connection_key.port == port - && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) - { - Some(Arc::clone(&info.stream)) - } else { - None - } - }) + conn.connection_map + .iter() + .find_map(|(connection_key, info)| { + if connection_key.ip == ip_bytes + && connection_key.port == port + && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) + { + Some(Arc::clone(&info.stream)) + } else { + None + } + }) } // 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. - pub fn find_connection_info(&self, ip: &str) -> Option<(ConnectionType, u16)> { - let ip_bytes = ip_to_binary(ip); - - for (key, _info) in self.connection_map.iter() { - if key.ip == ip_bytes { - let connection_type = ConnectionType::from_bytes(&key.connection_type)?; - return Some((connection_type, key.port)); - } - } - None - } - - // Find a stored connection by IP, constrained to a specific client role. - pub fn find_connection_info_by_client_type( - &self, - ip: &str, - client_type: ClientType, - ) -> Option<(ConnectionType, u16)> { - let ip_bytes = ip_to_binary(ip); - let client_type_bytes = client_type.as_bytes(); - - for (key, info) in self.connection_map.iter() { - if key.ip == ip_bytes && info.client_type == client_type_bytes { - let connection_type = ConnectionType::from_bytes(&key.connection_type)?; - return Some((connection_type, key.port)); - } - } - None - } - - // Find the stored outgoing port for a peer IP so reconnect and - // cleanup logic can target the correct connection entry. - pub fn find_outgoing_port(&self, ip: &str) -> Option { - let ip_bytes = ip_to_binary(ip); - self.connection_map - .iter() - .find(|(key, _)| { - key.connection_type == ConnectionType::Outgoing.as_bytes() && key.ip == ip_bytes - }) - .map(|(key, _)| key.port) - } - - // Prefer a random incoming node connection, falling back to an - // outgoing node connection when no incoming peer is available. - pub fn get_random_connection(&self, excluded_key: Option<&str>) -> Option<(Vec, u16)> { - let mut rng = thread_rng(); - let excluded = excluded_key.and_then(split_ip_port_key); - - if let Some((key, _info)) = self - .connection_map - .iter() - .filter(|(key, info)| { - ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Incoming) - && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) - && excluded - .as_ref() - .map(|(ip, _)| key.ip != ip_to_binary(ip)) - .unwrap_or(true) - }) - .choose(&mut rng) - { - return Some((key.ip.clone(), key.port)); - } - - if let Some((key, _info)) = self - .connection_map - .iter() - .filter(|(key, info)| { - ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Outgoing) - && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) - && excluded - .as_ref() - .map(|(ip, _)| key.ip != ip_to_binary(ip)) - .unwrap_or(true) - }) - .choose(&mut rng) - { - return Some((key.ip.clone(), key.port)); - } - None - } -} - -lazy_static! { - pub static ref CONNECTIONS: Arc>> = Arc::new(RwLock::new(None)); -} - + + // Find the first stored connection record for the requested IP. + pub fn find_connection_info(&self, ip: &str) -> Option<(ConnectionType, u16)> { + let ip_bytes = ip_to_binary(ip); + + for (key, _info) in self.connection_map.iter() { + if key.ip == ip_bytes { + let connection_type = ConnectionType::from_bytes(&key.connection_type)?; + return Some((connection_type, key.port)); + } + } + None + } + + // Find a stored connection by IP, constrained to a specific client role. + pub fn find_connection_info_by_client_type( + &self, + ip: &str, + client_type: ClientType, + ) -> Option<(ConnectionType, u16)> { + let ip_bytes = ip_to_binary(ip); + let client_type_bytes = client_type.as_bytes(); + + for (key, info) in self.connection_map.iter() { + if key.ip == ip_bytes && info.client_type == client_type_bytes { + let connection_type = ConnectionType::from_bytes(&key.connection_type)?; + return Some((connection_type, key.port)); + } + } + None + } + + // Find the stored outgoing port for a peer IP so reconnect and + // cleanup logic can target the correct connection entry. + pub fn find_outgoing_port(&self, ip: &str) -> Option { + let ip_bytes = ip_to_binary(ip); + self.connection_map + .iter() + .find(|(key, _)| { + key.connection_type == ConnectionType::Outgoing.as_bytes() && key.ip == ip_bytes + }) + .map(|(key, _)| key.port) + } + + // Prefer a random incoming node connection, falling back to an + // outgoing node connection when no incoming peer is available. + pub fn get_random_connection(&self, excluded_key: Option<&str>) -> Option<(Vec, u16)> { + let mut rng = thread_rng(); + let excluded = excluded_key.and_then(split_ip_port_key); + + if let Some((key, _info)) = self + .connection_map + .iter() + .filter(|(key, info)| { + ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Incoming) + && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) + && excluded + .as_ref() + .map(|(ip, _)| key.ip != ip_to_binary(ip)) + .unwrap_or(true) + }) + .choose(&mut rng) + { + return Some((key.ip.clone(), key.port)); + } + + if let Some((key, _info)) = self + .connection_map + .iter() + .filter(|(key, info)| { + ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Outgoing) + && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) + && excluded + .as_ref() + .map(|(ip, _)| key.ip != ip_to_binary(ip)) + .unwrap_or(true) + }) + .choose(&mut rng) + { + return Some((key.ip.clone(), key.port)); + } + None + } +} + +lazy_static! { + pub static ref CONNECTIONS: Arc>> = Arc::new(RwLock::new(None)); +} + pub async fn initialize_connection() { - // Lazily create the singleton connection manager the first time the - // node starts accepting or opening peer connections. - let mut connection_instance = CONNECTIONS.write().await; - if connection_instance.is_none() { - *connection_instance = Some(Connection::new()); + // Lazily create the singleton connection manager the first time the + // node starts accepting or opening peer connections. + let mut connection_instance = CONNECTIONS.write().await; + if connection_instance.is_none() { + *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 { - // Recover the stored client role from the serialized connection key - // used throughout the RPC layer. - let (ip, port) = split_ip_port_key(key)?; - let ip_bytes = ip_to_binary(&ip); - - let guard = CONNECTIONS.read().await; - let conn = guard.as_ref()?; - - for (connection_key, info) in conn.connection_map.iter() { - if connection_key.ip == ip_bytes && connection_key.port == port { - return ClientType::from_bytes(&info.client_type); - } - } - - None -} + // Recover the stored client role from the serialized connection key + // used throughout the RPC layer. + let (ip, port) = split_ip_port_key(key)?; + let ip_bytes = ip_to_binary(&ip); + + let guard = CONNECTIONS.read().await; + let conn = guard.as_ref()?; + + for (connection_key, info) in conn.connection_map.iter() { + if connection_key.ip == ip_bytes && connection_key.port == port { + return ClientType::from_bytes(&info.client_type); + } + } + + None +} diff --git a/src/records/memory/mempool/lookups.rs b/src/records/memory/mempool/lookups.rs index 2e8308a..f23070a 100644 --- a/src/records/memory/mempool/lookups.rs +++ b/src/records/memory/mempool/lookups.rs @@ -1,89 +1,89 @@ -use super::*; - +use super::*; + pub async fn signature_exists(signature: &str, hash: &str) -> Result { let client = DB.get().expect("DB not initialized"); // Check every mempool table because the signature column names differ by // transaction type, especially for two-party swaps and loans. let row = client - .query_one( - r#" - SELECT - CASE - 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 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 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 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 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 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 collateral_claim l WHERE l.signature = $1 AND l.hash = $2 AND l.processed = false) - THEN 1 - ELSE 0 - END AS signature_found; - "#, - &[&signature, &hash], - ) - .await?; - - let found: i32 = row.get(0); - Ok(found == 1) -} - + .query_one( + r#" + SELECT + CASE + 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 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 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 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 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 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 collateral_claim l WHERE l.signature = $1 AND l.hash = $2 AND l.processed = false) + THEN 1 + ELSE 0 + END AS signature_found; + "#, + &[&signature, &hash], + ) + .await?; + + let found: i32 = row.get(0); + Ok(found == 1) +} + pub async fn transaction_by_signature(signature: &str) -> RpcResponse { let client = DB.get().expect("DB not initialized"); // Return the original serialized transaction bytes, not a reconstructed // row, so RPC callers receive the same payload that would enter a block. let result = client - .query_opt( - r#" - SELECT original FROM ( - SELECT original FROM transfer WHERE signature = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM token WHERE signature = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM issue_token WHERE signature = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM burn WHERE signature = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM nft WHERE signature = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM marketing WHERE signature = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM vanity_address WHERE signature = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM swap WHERE signature1 = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM swap WHERE signature2 = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM loan_contract WHERE signature1 = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM loan_contract WHERE signature2 = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM loan_payment WHERE signature = $1 AND processed = false LIMIT 1 - UNION ALL - SELECT original FROM collateral_claim WHERE signature = $1 AND processed = false LIMIT 1 - ) AS subquery LIMIT 1 - "#, - &[&signature], - ) - .await; - - match result { - Ok(Some(row)) => { - let bytes: Vec = row.get(0); - RpcResponse::Binary(bytes) - } - _ => RpcResponse::Binary(Vec::new()), - } -} - + .query_opt( + r#" + SELECT original FROM ( + SELECT original FROM transfer WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM token WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM issue_token WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM burn WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM nft WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM marketing WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM vanity_address WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM swap WHERE signature1 = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM swap WHERE signature2 = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM loan_contract WHERE signature1 = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM loan_contract WHERE signature2 = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM loan_payment WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM collateral_claim WHERE signature = $1 AND processed = false LIMIT 1 + ) AS subquery LIMIT 1 + "#, + &[&signature], + ) + .await; + + match result { + Ok(Some(row)) => { + let bytes: Vec = row.get(0); + RpcResponse::Binary(bytes) + } + _ => RpcResponse::Binary(Vec::new()), + } +} + pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse { let client = DB.get().expect("DB not initialized"); // 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 // stream by transaction type and fixed byte length. let rows = match client - .query( - r#" - SELECT original FROM ( - SELECT original FROM transfer WHERE receiver = ANY($1) AND processed = false - UNION ALL - SELECT original FROM token WHERE creator = ANY($1) AND processed = false - UNION ALL - SELECT original FROM issue_token WHERE creator = ANY($1) AND processed = false - UNION ALL - SELECT original FROM burn WHERE address = ANY($1) AND processed = false - UNION ALL - SELECT original FROM nft WHERE creator = ANY($1) AND processed = false - UNION ALL - SELECT original FROM marketing WHERE advertiser = ANY($1) AND processed = false - UNION ALL - SELECT original FROM vanity_address WHERE address = ANY($1) AND processed = false - UNION ALL - SELECT original FROM swap WHERE sender1 = ANY($1) AND processed = false - UNION ALL - SELECT original FROM swap WHERE sender2 = ANY($1) AND processed = false - UNION ALL - SELECT original FROM loan_contract WHERE lender = ANY($1) AND processed = false - UNION ALL - SELECT original FROM loan_contract WHERE borrower = ANY($1) AND processed = false - UNION ALL - SELECT original FROM loan_payment WHERE address = ANY($1) AND processed = false - UNION ALL - SELECT original FROM collateral_claim WHERE address = ANY($1) AND processed = false - ) AS subquery; - "#, - &[&addresses], - ) - .await - { - Ok(r) => r, - Err(_) => return RpcResponse::Binary(Vec::new()), - }; - - let mut bytes = Vec::new(); - for row in rows { - let chunk: Vec = row.get(0); - bytes.extend(chunk); - } - - RpcResponse::Binary(bytes) -} - + .query( + r#" + SELECT original FROM ( + SELECT original FROM transfer WHERE receiver = ANY($1) AND processed = false + UNION ALL + SELECT original FROM token WHERE creator = ANY($1) AND processed = false + UNION ALL + SELECT original FROM issue_token WHERE creator = ANY($1) AND processed = false + UNION ALL + SELECT original FROM burn WHERE address = ANY($1) AND processed = false + UNION ALL + SELECT original FROM nft WHERE creator = ANY($1) AND processed = false + UNION ALL + SELECT original FROM marketing WHERE advertiser = ANY($1) AND processed = false + UNION ALL + SELECT original FROM vanity_address WHERE address = ANY($1) AND processed = false + UNION ALL + SELECT original FROM swap WHERE sender1 = ANY($1) AND processed = false + UNION ALL + SELECT original FROM swap WHERE sender2 = ANY($1) AND processed = false + UNION ALL + SELECT original FROM loan_contract WHERE lender = ANY($1) AND processed = false + UNION ALL + SELECT original FROM loan_contract WHERE borrower = ANY($1) AND processed = false + UNION ALL + SELECT original FROM loan_payment WHERE address = ANY($1) AND processed = false + UNION ALL + SELECT original FROM collateral_claim WHERE address = ANY($1) AND processed = false + ) AS subquery; + "#, + &[&addresses], + ) + .await + { + Ok(r) => r, + Err(_) => return RpcResponse::Binary(Vec::new()), + }; + + let mut bytes = Vec::new(); + for row in rows { + let chunk: Vec = row.get(0); + bytes.extend(chunk); + } + + RpcResponse::Binary(bytes) +} + pub async fn largest_fee() -> RpcResponse { let client = DB.get().expect("DB not initialized"); // Swaps have two possible fees, so both sides are included in the max. let row = match client - .query_one( - r#" - SELECT MAX(fee) AS largest_txid FROM ( - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM transfer - UNION ALL - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM token - UNION ALL - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM issue_token - UNION ALL - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM burn - UNION ALL - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM nft - UNION ALL - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM marketing - UNION ALL - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM vanity_address - UNION ALL - SELECT CAST(MAX(fee1) AS BIGINT) AS fee FROM swap - UNION ALL - SELECT CAST(MAX(fee2) AS BIGINT) AS fee FROM swap - UNION ALL - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_contract - UNION ALL - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_payment - UNION ALL - SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM collateral_claim - ) AS combined_max_txids; - "#, - &[], - ) - .await - { - Ok(r) => r, - Err(_) => return RpcResponse::Binary(0u32.to_le_bytes().to_vec()), - }; - - let max_fee: Option = row.get(0); - let fee = (max_fee.unwrap_or(0) as u64).to_le_bytes().to_vec(); - + .query_one( + r#" + SELECT MAX(fee) AS largest_txid FROM ( + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM transfer + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM token + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM issue_token + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM burn + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM nft + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM marketing + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM vanity_address + UNION ALL + SELECT CAST(MAX(fee1) AS BIGINT) AS fee FROM swap + UNION ALL + SELECT CAST(MAX(fee2) AS BIGINT) AS fee FROM swap + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_contract + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_payment + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM collateral_claim + ) AS combined_max_txids; + "#, + &[], + ) + .await + { + Ok(r) => r, + Err(_) => return RpcResponse::Binary(0u32.to_le_bytes().to_vec()), + }; + + let max_fee: Option = row.get(0); + let fee = (max_fee.unwrap_or(0) as u64).to_le_bytes().to_vec(); + RpcResponse::Binary(fee) } @@ -242,49 +242,49 @@ async fn pending_saved_loan_payment_balance( pub async fn get_coin_balance( db: &Db, address: &str, - coin: &str, + coin: &str, ) -> Result> { let client = DB.get().expect("DB not initialized"); // Pending-balance checks use canonical addresses so vanity and short // address inputs see the same outgoing obligations. let addresses = canonical_mempool_addresses(db, address); 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 .query_one( r#" - SELECT CAST(( - COALESCE((SELECT SUM(t.value) - FROM transfer t - WHERE t.sender = ANY($1) AND t.coin = $2 AND t.nft_series = $3 AND t.processed = false), 0) - - + COALESCE((SELECT SUM(tok.number) - FROM token tok - WHERE tok.creator = ANY($1) AND tok.ticker = $2 AND tok.processed = false), 0) - - + COALESCE((SELECT SUM(it.number) - FROM issue_token it - WHERE it.creator = ANY($1) AND it.ticker = $2 AND it.processed = false), 0) - + SELECT CAST(( + COALESCE((SELECT SUM(t.value) + FROM transfer t + WHERE t.sender = ANY($1) AND t.coin = $2 AND t.nft_series = $3 AND t.processed = false), 0) + + + COALESCE((SELECT SUM(tok.number) + FROM token tok + WHERE tok.creator = ANY($1) AND tok.ticker = $2 AND tok.processed = false), 0) + + + COALESCE((SELECT SUM(it.number) + FROM issue_token it + WHERE it.creator = ANY($1) AND it.ticker = $2 AND it.processed = false), 0) + + COALESCE((SELECT SUM(b.value) FROM burn b WHERE b.address = ANY($1) AND b.coin = $2 AND b.nft_series = $3 AND b.processed = false), 0) - - + COALESCE((SELECT SUM(s.value1) - FROM swap s - WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0) - + COALESCE((SELECT SUM(s.tip1) - FROM swap s - WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0) - - + COALESCE((SELECT SUM(s.value2) - FROM swap s - WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0) - + COALESCE((SELECT SUM(s.tip2) - FROM swap s - WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0) - + + + COALESCE((SELECT SUM(s.value1) + FROM swap s + WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0) + + COALESCE((SELECT SUM(s.tip1) + FROM swap s + WHERE s.sender1 = ANY($1) AND s.ticker1 = $2 AND s.nft_series1 = $3 AND s.processed = false), 0) + + + COALESCE((SELECT SUM(s.value2) + FROM swap s + WHERE s.sender2 = ANY($1) AND s.ticker2 = $2 AND s.nft_series2 = $3 AND s.processed = false), 0) + + COALESCE((SELECT SUM(s.tip2) + FROM swap s + 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) FROM loan_contract lc 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) FROM loan_contract lc WHERE lc.borrower = ANY($1) AND lc.collateral = $2 AND lc.processed = false), 0) - - + COALESCE(( - SELECT SUM(lp.payback_amount) - FROM loan_payment lp - 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 - ), 0) - - + COALESCE(( - SELECT SUM(lp.tip) - FROM loan_payment lp - 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 - ), 0) - - ) AS BIGINT) AS total - "#, - &[&addresses, &asset_name, &nft_series], - ) + + + COALESCE(( + SELECT SUM(lp.payback_amount) + FROM loan_payment lp + 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 + ), 0) + + + COALESCE(( + SELECT SUM(lp.tip) + FROM loan_payment lp + 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 + ), 0) + + ) AS BIGINT) AS total + "#, + &[&addresses, &asset_name, &nft_series], + ) .await?; // 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?; Ok((total.max(0) as u64).saturating_add(chain_loan_payments)) } - -pub async fn get_basecoin_balance( - db: &Db, - address: &str, + +pub async fn get_basecoin_balance( + db: &Db, + address: &str, ) -> Result> { let client = DB.get().expect("DB not initialized"); 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 // any pending loan/swap movements denominated in the base coin. let row = client - .query_one( - r#" - SELECT CAST(( + .query_one( + r#" + SELECT CAST(( COALESCE((SELECT SUM(t.value) FROM transfer t 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 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(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(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(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(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(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(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(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(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(lc.loan_amount) FROM loan_contract lc WHERE lc.lender = ANY($1) AND lc.loan_coin = $2 AND lc.processed = false), 0) + COALESCE((SELECT SUM(lc.collateral_amount) FROM loan_contract lc WHERE lc.borrower = ANY($1) AND lc.collateral = $2 AND lc.processed = false), 0) - + COALESCE(( - SELECT SUM(lp.payback_amount) - FROM loan_payment lp - 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 - ), 0) - + COALESCE(( - SELECT SUM(lp.tip) - FROM loan_payment lp - 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 - ), 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.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.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) - ) AS BIGINT) AS total - "#, - &[&addresses, &*BASECOIN], - ) - .await?; - + + COALESCE(( + SELECT SUM(lp.payback_amount) + FROM loan_payment lp + 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 + ), 0) + + COALESCE(( + SELECT SUM(lp.tip) + FROM loan_payment lp + 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 + ), 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.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.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) + ) AS BIGINT) AS total + "#, + &[&addresses, &*BASECOIN], + ) + .await?; + let total: i64 = row.get(0); - let chain_loan_payments = - pending_saved_loan_payment_balance(db, &addresses, &BASECOIN).await?; + let chain_loan_payments = pending_saved_loan_payment_balance(db, &addresses, &BASECOIN).await?; Ok((total.max(0) as u64).saturating_add(chain_loan_payments)) } - + pub async fn get_pending_payments_for_contract( contract_hash: &str, ) -> Result> { @@ -393,63 +392,62 @@ pub async fn get_pending_payments_for_contract( // Loan verification uses this to prevent pending payments from exceeding // what the contract still owes. let row = client - .query_one( - r#" - SELECT CAST(COALESCE(SUM(lp.payback_amount), 0) AS BIGINT) AS total - FROM loan_payment lp - WHERE lp.contract_hash = $1 - AND lp.processed = false - "#, - &[&contract_hash], - ) - .await?; - - let total: i64 = row.get(0); - Ok(total.max(0) as u64) -} - + .query_one( + r#" + SELECT CAST(COALESCE(SUM(lp.payback_amount), 0) AS BIGINT) AS total + FROM loan_payment lp + WHERE lp.contract_hash = $1 + AND lp.processed = false + "#, + &[&contract_hash], + ) + .await?; + + let total: i64 = row.get(0); + Ok(total.max(0) as u64) +} + pub async fn total_transactions() -> RpcResponse { let client = DB.get().expect("DB not initialized"); // Count rows across all mempool tables, including processed rows that may // still be retained briefly for orphan rollback. let row = match client - .query_one( - r#" - SELECT CAST(SUM(row_count) AS BIGINT) AS total_rows FROM ( - SELECT COUNT(*) AS row_count FROM transfer - UNION ALL - SELECT COUNT(*) AS row_count FROM token - UNION ALL - SELECT COUNT(*) AS row_count FROM issue_token - UNION ALL - SELECT COUNT(*) AS row_count FROM burn - UNION ALL - SELECT COUNT(*) AS row_count FROM nft - UNION ALL - SELECT COUNT(*) AS row_count FROM marketing - UNION ALL - SELECT COUNT(*) AS row_count FROM vanity_address - UNION ALL - SELECT COUNT(*) AS row_count FROM swap - UNION ALL - SELECT COUNT(*) AS row_count FROM loan_contract - UNION ALL - SELECT COUNT(*) AS row_count FROM loan_payment - UNION ALL - SELECT COUNT(*) AS row_count FROM collateral_claim - ) AS combined; - "#, - &[], - ) - .await - { - Ok(r) => r, - Err(_) => return RpcResponse::Binary(vec![0; 8]), - }; - - let total: Option = row.get(0); - let result = (total.unwrap_or(0) as u32).to_le_bytes().to_vec(); - - RpcResponse::Binary(result) -} - + .query_one( + r#" + SELECT CAST(SUM(row_count) AS BIGINT) AS total_rows FROM ( + SELECT COUNT(*) AS row_count FROM transfer + UNION ALL + SELECT COUNT(*) AS row_count FROM token + UNION ALL + SELECT COUNT(*) AS row_count FROM issue_token + UNION ALL + SELECT COUNT(*) AS row_count FROM burn + UNION ALL + SELECT COUNT(*) AS row_count FROM nft + UNION ALL + SELECT COUNT(*) AS row_count FROM marketing + UNION ALL + SELECT COUNT(*) AS row_count FROM vanity_address + UNION ALL + SELECT COUNT(*) AS row_count FROM swap + UNION ALL + SELECT COUNT(*) AS row_count FROM loan_contract + UNION ALL + SELECT COUNT(*) AS row_count FROM loan_payment + UNION ALL + SELECT COUNT(*) AS row_count FROM collateral_claim + ) AS combined; + "#, + &[], + ) + .await + { + Ok(r) => r, + Err(_) => return RpcResponse::Binary(vec![0; 8]), + }; + + let total: Option = row.get(0); + let result = (total.unwrap_or(0) as u32).to_le_bytes().to_vec(); + + RpcResponse::Binary(result) +} diff --git a/src/records/memory/mempool/mod.rs b/src/records/memory/mempool/mod.rs index 62fcccd..084b51b 100644 --- a/src/records/memory/mempool/mod.rs +++ b/src/records/memory/mempool/mod.rs @@ -1,330 +1,326 @@ -use crate::blocks::loans::LoanContractTransaction; -use crate::common::binary_conversions::binary_to_string; -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::common::nft_assets::{nft_asset_name, nft_asset_parts}; -use crate::config::SETTINGS; -use crate::decode; -use crate::lazy_static; +use crate::blocks::loans::LoanContractTransaction; +use crate::common::binary_conversions::binary_to_string; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::common::nft_assets::{nft_asset_name, nft_asset_parts}; +use crate::config::SETTINGS; +use crate::decode; +use crate::lazy_static; use crate::records::memory::structs::BalanceKey; -use crate::records::wallet_registry::{ - resolve_canonical_registered_short_address, +use crate::records::wallet_registry::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 = 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, + originals: Vec>, +} + +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 = 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, - originals: Vec>, -} - -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::{ - 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, - spawn_processed_cleanup, delete_by_signatures, + spawn_processed_cleanup, }; -pub use schema::{clear_mempool, init_db, setup_mempool}; -pub use selection::{ - apply_selected_transaction_math, clear_selected_transaction_sql, delete_selected_transactions, - select_transactions_for_block, stream_selected_transaction_originals, -}; - -fn required_string(row: &tokio_postgres::Row, column: &str) -> Result { +pub use schema::{clear_mempool, init_db, setup_mempool}; +pub use selection::{ + apply_selected_transaction_math, clear_selected_transaction_sql, delete_selected_transactions, + select_transactions_for_block, stream_selected_transaction_originals, +}; + +fn required_string(row: &tokio_postgres::Row, column: &str) -> Result { row.try_get::<_, Option>(column)? .ok_or_else(|| anyhow!("Missing required column {column}")) -} - -fn add_balance_change( - db: &Db, - balance_changes: &mut HashMap, - address: &str, - coin: &str, - delta: i64, -) { - add_balance_change_bytes( - balance_changes, - address_key_bytes(db, address), - coin.as_bytes().to_vec(), - delta, - ); -} - -fn add_balance_change_bytes( - balance_changes: &mut HashMap, - address: Vec, - coin: Vec, - delta: i64, -) { - *balance_changes - .entry(BalanceKey { address, coin }) - .or_insert(0) += delta; -} - +} + +fn add_balance_change( + db: &Db, + balance_changes: &mut HashMap, + address: &str, + coin: &str, + delta: i64, +) { + add_balance_change_bytes( + balance_changes, + address_key_bytes(db, address), + coin.as_bytes().to_vec(), + delta, + ); +} + +fn add_balance_change_bytes( + balance_changes: &mut HashMap, + address: Vec, + coin: Vec, + delta: i64, +) { + *balance_changes + .entry(BalanceKey { address, coin }) + .or_insert(0) += delta; +} + fn address_key_bytes(db: &Db, address: &str) -> Vec { - resolve_canonical_registered_short_address(db, address) - .ok() - .flatten() - .or_else(|| Wallet::normalize_to_short_address(address)) - .map(|normalized| normalized.as_bytes().to_vec()) - .unwrap_or_else(|| address.as_bytes().to_vec()) -} - -fn canonical_mempool_addresses(db: &Db, address: &str) -> Vec { - let canonical = resolve_canonical_registered_short_address(db, address) - .ok() - .flatten() - .or_else(|| Wallet::normalize_to_short_address(address)) - .unwrap_or_else(|| address.to_string()); - - vec![canonical] -} - -async fn resolve_loan_details(db: &Db, contract_hash: &str) -> Result<(Vec, Vec)> { - let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await; - if bytes.is_empty() || bytes[0] != 7 { - return Ok((Vec::new(), Vec::new())); - } - match LoanContractTransaction::from_bytes(7, &bytes[1..]).await { - Ok(loan) => Ok(( - loan.unsigned_loan_contract.loan_coin.as_bytes().to_vec(), - address_key_bytes(db, &loan.unsigned_loan_contract.lender), - )), - Err(_) => Ok((Vec::new(), Vec::new())), - } -} - -async fn resolve_collateral_details(db: &Db, contract_hash: &str) -> Result<(Vec, i64)> { - let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await; - if bytes.is_empty() || bytes[0] != 7 { - return Ok((Vec::new(), 0)); - } - match LoanContractTransaction::from_bytes(7, &bytes[1..]).await { - Ok(loan) => Ok(( - loan.unsigned_loan_contract.collateral.as_bytes().to_vec(), - loan.unsigned_loan_contract.collateral_amount as i64, - )), - Err(_) => Ok((Vec::new(), 0)), - } -} - -fn ids_for_table(batch: &SelectedMempoolBatch, table: &str) -> Vec { - batch - .transactions - .iter() - .filter(|tx| tx.table_name() == table) - .map(SelectedMempoolTransaction::id) - .collect() -} - + resolve_canonical_registered_short_address(db, address) + .ok() + .flatten() + .or_else(|| Wallet::normalize_to_short_address(address)) + .map(|normalized| normalized.as_bytes().to_vec()) + .unwrap_or_else(|| address.as_bytes().to_vec()) +} + +fn canonical_mempool_addresses(db: &Db, address: &str) -> Vec { + let canonical = resolve_canonical_registered_short_address(db, address) + .ok() + .flatten() + .or_else(|| Wallet::normalize_to_short_address(address)) + .unwrap_or_else(|| address.to_string()); + + vec![canonical] +} + +async fn resolve_loan_details(db: &Db, contract_hash: &str) -> Result<(Vec, Vec)> { + let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await; + if bytes.is_empty() || bytes[0] != 7 { + return Ok((Vec::new(), Vec::new())); + } + match LoanContractTransaction::from_bytes(7, &bytes[1..]).await { + Ok(loan) => Ok(( + loan.unsigned_loan_contract.loan_coin.as_bytes().to_vec(), + address_key_bytes(db, &loan.unsigned_loan_contract.lender), + )), + Err(_) => Ok((Vec::new(), Vec::new())), + } +} + +async fn resolve_collateral_details(db: &Db, contract_hash: &str) -> Result<(Vec, i64)> { + let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, decode(contract_hash)?).await; + if bytes.is_empty() || bytes[0] != 7 { + return Ok((Vec::new(), 0)); + } + match LoanContractTransaction::from_bytes(7, &bytes[1..]).await { + Ok(loan) => Ok(( + loan.unsigned_loan_contract.collateral.as_bytes().to_vec(), + loan.unsigned_loan_contract.collateral_amount as i64, + )), + Err(_) => Ok((Vec::new(), 0)), + } +} + +fn ids_for_table(batch: &SelectedMempoolBatch, table: &str) -> Vec { + batch + .transactions + .iter() + .filter(|tx| tx.table_name() == table) + .map(SelectedMempoolTransaction::id) + .collect() +} + async fn mark_rows_by_ids( - client: &Client, - table: &str, - ids: &[i64], - block_number: i32, -) -> Result<()> { - if ids.is_empty() { - return Ok(()); - } - - let statement = format!( - "UPDATE {table} SET processed=true, processed_block_number=$1 WHERE id = ANY($2)" - ); - client.execute(&statement, &[&block_number, &ids]).await?; - Ok(()) + client: &Client, + table: &str, + ids: &[i64], + block_number: i32, +) -> Result<()> { + if ids.is_empty() { + return Ok(()); + } + + let statement = + format!("UPDATE {table} SET processed=true, processed_block_number=$1 WHERE id = ANY($2)"); + client.execute(&statement, &[&block_number, &ids]).await?; + Ok(()) } 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(()); } - let statement = - format!("UPDATE {table} SET processed=false, processed_block_number=NULL WHERE id = ANY($1)"); + let statement = format!( + "UPDATE {table} SET processed=false, processed_block_number=NULL WHERE id = ANY($1)" + ); client.execute(&statement, &[&ids]).await?; Ok(()) } async fn delete_rows(client: &Client, table: &str, ids: &[i64]) -> Result<()> { - if ids.is_empty() { - return Ok(()); - } - - let statement = format!("DELETE FROM {table} WHERE id = ANY($1)"); - client.execute(&statement, &[&ids]).await?; - Ok(()) -} - -async fn unmark_by_signatures( - client: &Client, - table: &str, - signature_column: &str, - signatures: &[String], -) -> Result { - let statement = format!( - "UPDATE {table} SET processed=false, processed_block_number=NULL WHERE {signature_column} = ANY($1) AND processed = true" - ); - Ok(client.execute(&statement, &[&signatures]).await?) -} - -async fn delete_processed_before_or_at(block_number: u32, limit: i64) -> Result<()> { - // Periodic cleanup deletes processed mempool rows in bounded batches - // so long-lived nodes do not accumulate infinite processed history. - let client = DB.get().expect("DB not initialized"); - let bn = block_number as i32; - - delete_processed_rows_limited(client, "transfer", 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, "burn", 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, "vanity_address", 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_payment", bn, limit).await?; - delete_processed_rows_limited(client, "collateral_claim", bn, limit).await?; - - Ok(()) -} - -async fn delete_processed_rows_limited( - client: &Client, - table: &str, - block_number: i32, - limit: i64, -) -> Result { - let statement = format!( - r#" - DELETE FROM {table} - WHERE id IN ( - SELECT id - FROM {table} - WHERE processed = true - AND processed_block_number IS NOT NULL - AND processed_block_number <= $1 - ORDER BY processed_block_number ASC, id ASC - LIMIT $2 - ) - "# - ); - - Ok(client.execute(&statement, &[&block_number, &limit]).await?) -} + if ids.is_empty() { + return Ok(()); + } + + let statement = format!("DELETE FROM {table} WHERE id = ANY($1)"); + client.execute(&statement, &[&ids]).await?; + Ok(()) +} + +async fn unmark_by_signatures( + client: &Client, + table: &str, + signature_column: &str, + signatures: &[String], +) -> Result { + let statement = format!( + "UPDATE {table} SET processed=false, processed_block_number=NULL WHERE {signature_column} = ANY($1) AND processed = true" + ); + Ok(client.execute(&statement, &[&signatures]).await?) +} + +async fn delete_processed_before_or_at(block_number: u32, limit: i64) -> Result<()> { + // Periodic cleanup deletes processed mempool rows in bounded batches + // so long-lived nodes do not accumulate infinite processed history. + let client = DB.get().expect("DB not initialized"); + let bn = block_number as i32; + + delete_processed_rows_limited(client, "transfer", 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, "burn", 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, "vanity_address", 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_payment", bn, limit).await?; + delete_processed_rows_limited(client, "collateral_claim", bn, limit).await?; + + Ok(()) +} + +async fn delete_processed_rows_limited( + client: &Client, + table: &str, + block_number: i32, + limit: i64, +) -> Result { + let statement = format!( + r#" + DELETE FROM {table} + WHERE id IN ( + SELECT id + FROM {table} + WHERE processed = true + AND processed_block_number IS NOT NULL + AND processed_block_number <= $1 + ORDER BY processed_block_number ASC, id ASC + LIMIT $2 + ) + "# + ); + + Ok(client.execute(&statement, &[&block_number, &limit]).await?) +} diff --git a/src/records/memory/mempool/processing.rs b/src/records/memory/mempool/processing.rs index ca3473f..a321b09 100644 --- a/src/records/memory/mempool/processing.rs +++ b/src/records/memory/mempool/processing.rs @@ -1,57 +1,57 @@ -use super::*; - +use super::*; + pub async fn mark_selected_transactions_processed( batch: &SelectedMempoolBatch, block_number: u32, ) -> Result<()> { - // Mark each selected mempool row as processed under the saved block - // number so it can be cleaned up or restored later if needed. + // Mark each selected mempool row as processed under the saved block + // number so it can be cleaned up or restored later if needed. let client = DB.get().expect("DB not initialized"); let bn = block_number as i32; // Selected batches are grouped by table, then marked with one UPDATE per // 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, "token", &ids_for_table(batch, "token"), bn).await?; - mark_rows_by_ids( - client, - "issue_token", - &ids_for_table(batch, "issue_token"), - 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, "marketing", &ids_for_table(batch, "marketing"), bn).await?; - mark_rows_by_ids( - client, - "vanity_address", - &ids_for_table(batch, "vanity_address"), - bn, - ) - .await?; - mark_rows_by_ids(client, "swap", &ids_for_table(batch, "swap"), bn).await?; - mark_rows_by_ids( - client, - "loan_contract", - &ids_for_table(batch, "loan_contract"), - bn, - ) - .await?; - mark_rows_by_ids( - client, - "loan_payment", - &ids_for_table(batch, "loan_payment"), - bn, - ) - .await?; - mark_rows_by_ids( - client, - "collateral_claim", - &ids_for_table(batch, "collateral_claim"), - bn, - ) - .await?; + mark_rows_by_ids(client, "token", &ids_for_table(batch, "token"), bn).await?; + mark_rows_by_ids( + client, + "issue_token", + &ids_for_table(batch, "issue_token"), + 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, "marketing", &ids_for_table(batch, "marketing"), bn).await?; + mark_rows_by_ids( + client, + "vanity_address", + &ids_for_table(batch, "vanity_address"), + bn, + ) + .await?; + mark_rows_by_ids(client, "swap", &ids_for_table(batch, "swap"), bn).await?; + mark_rows_by_ids( + client, + "loan_contract", + &ids_for_table(batch, "loan_contract"), + bn, + ) + .await?; + mark_rows_by_ids( + client, + "loan_payment", + &ids_for_table(batch, "loan_payment"), + bn, + ) + .await?; + mark_rows_by_ids( + client, + "collateral_claim", + &ids_for_table(batch, "collateral_claim"), + bn, + ) + .await?; 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, "token", &ids_for_table(batch, "token")).await?; - unmark_rows_by_ids( - client, - "issue_token", - &ids_for_table(batch, "issue_token"), - ) - .await?; + unmark_rows_by_ids(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, "nft", &ids_for_table(batch, "nft")).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 { - // Orphan correction can revive recently processed mempool rows by - // signature when a saved block is rolled back out of the chain. - if signatures.is_empty() { - return Ok(false); - } - + // Orphan correction can revive recently processed mempool rows by + // signature when a saved block is rolled back out of the chain. + if signatures.is_empty() { + return Ok(false); + } + let client = DB.get().expect("DB not initialized"); let mut restored = 0_u64; // Each table keeps its own signature columns, so rollback unmarks every // column that could contain one of the rolled-back signatures. restored += unmark_by_signatures(client, "transfer", "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, "burn", "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, "vanity_address", "signature", 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, "loan_contract", "signature1", 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, "collateral_claim", "signature", signatures).await?; - - Ok(restored > 0) -} - -pub fn spawn_processed_cleanup(saved_block_number: u32) { - // Cleanup trails the chain tip by a small depth so recent processed - // mempool rows can still be restored during short orphan events. - if saved_block_number <= CLEANUP_DEPTH { - return; - } - - if CLEANUP_RUNNING - .compare_exchange( - false, - true, - crate::AtomicOrdering::SeqCst, - crate::AtomicOrdering::SeqCst, - ) - .is_err() - { - return; - } - + restored += unmark_by_signatures(client, "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, "nft", "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, "swap", "signature1", 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", "signature2", signatures).await?; + restored += unmark_by_signatures(client, "loan_payment", "signature", signatures).await?; + restored += unmark_by_signatures(client, "collateral_claim", "signature", signatures).await?; + + Ok(restored > 0) +} + +pub fn spawn_processed_cleanup(saved_block_number: u32) { + // Cleanup trails the chain tip by a small depth so recent processed + // mempool rows can still be restored during short orphan events. + if saved_block_number <= CLEANUP_DEPTH { + return; + } + + if CLEANUP_RUNNING + .compare_exchange( + false, + true, + crate::AtomicOrdering::SeqCst, + crate::AtomicOrdering::SeqCst, + ) + .is_err() + { + return; + } + task::spawn(async move { let safe_block = saved_block_number.saturating_sub(CLEANUP_DEPTH); // Cleanup is deliberately delayed behind the tip so short reorgs can // still restore recently processed rows. if let Err(err) = delete_processed_before_or_at(safe_block, CLEANUP_BATCH_LIMIT).await { - eprintln!( - "[mempool_cleanup] failed: saved_block={saved_block_number} safe_block={safe_block} err={err}" - ); - } - CLEANUP_RUNNING.store(false, crate::AtomicOrdering::SeqCst); - }); -} - - -pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u32) -> Result<()> { - // Synced blocks arrive with signatures instead of selected-row IDs, - // so processed marking on the updating path works by signature. - if signatures.is_empty() { - return Ok(()); - } - + eprintln!( + "[mempool_cleanup] failed: saved_block={saved_block_number} safe_block={safe_block} err={err}" + ); + } + CLEANUP_RUNNING.store(false, crate::AtomicOrdering::SeqCst); + }); +} + +pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u32) -> Result<()> { + // Synced blocks arrive with signatures instead of selected-row IDs, + // so processed marking on the updating path works by signature. + if signatures.is_empty() { + return Ok(()); + } + let client = DB.get().expect("DB not initialized"); let bn = block_number as i32; // Remote/synced blocks do not know local row IDs, so they mark by // transaction signatures instead. client - .execute( - "UPDATE transfer SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE issue_token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE burn SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE nft SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE marketing SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE vanity_address SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE loan_payment SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", - &[&bn, &signatures], - ) - .await?; - client - .execute( - "UPDATE collateral_claim SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", - &[&bn, &signatures], - ) - .await?; - - Ok(()) -} - -pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> { - // Some validation failures need to remove mempool rows directly by - // signature regardless of which table the transaction lives in. - if signatures.is_empty() { - return Ok(()); - } - + .execute( + "UPDATE transfer SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE issue_token SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE burn SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE nft SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE marketing SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE vanity_address SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE swap SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature1 = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE loan_contract SET processed=true, processed_block_number=$1 WHERE signature2 = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE loan_payment SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + &[&bn, &signatures], + ) + .await?; + client + .execute( + "UPDATE collateral_claim SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + &[&bn, &signatures], + ) + .await?; + + Ok(()) +} + +pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> { + // Some validation failures need to remove mempool rows directly by + // signature regardless of which table the transaction lives in. + if signatures.is_empty() { + return Ok(()); + } + let client = DB.get().expect("DB not initialized"); // Failed validation removes every matching pending row no matter which // transaction table currently owns the signature. client - .execute( - "DELETE FROM transfer WHERE signature = ANY($1)", - &[&signatures], - ) - .await?; - client - .execute( - "DELETE FROM token WHERE signature = ANY($1)", - &[&signatures], - ) - .await?; - client - .execute( - "DELETE FROM issue_token WHERE signature = ANY($1)", - &[&signatures], - ) - .await?; - client - .execute("DELETE FROM burn WHERE signature = ANY($1)", &[&signatures]) - .await?; - client - .execute("DELETE FROM nft WHERE signature = ANY($1)", &[&signatures]) - .await?; - client - .execute( - "DELETE FROM marketing WHERE signature = ANY($1)", - &[&signatures], - ) - .await?; - client - .execute( - "DELETE FROM vanity_address WHERE signature = ANY($1)", - &[&signatures], - ) - .await?; - client - .execute( - "DELETE FROM swap WHERE signature1 = ANY($1) OR signature2 = ANY($1)", - &[&signatures], - ) - .await?; - client - .execute( - "DELETE FROM loan_contract WHERE signature1 = ANY($1) OR signature2 = ANY($1)", - &[&signatures], - ) - .await?; - client - .execute( - "DELETE FROM loan_payment WHERE signature = ANY($1)", - &[&signatures], - ) - .await?; - client - .execute( - "DELETE FROM collateral_claim WHERE signature = ANY($1)", - &[&signatures], - ) - .await?; - - Ok(()) -} - + .execute( + "DELETE FROM transfer WHERE signature = ANY($1)", + &[&signatures], + ) + .await?; + client + .execute( + "DELETE FROM token WHERE signature = ANY($1)", + &[&signatures], + ) + .await?; + client + .execute( + "DELETE FROM issue_token WHERE signature = ANY($1)", + &[&signatures], + ) + .await?; + client + .execute("DELETE FROM burn WHERE signature = ANY($1)", &[&signatures]) + .await?; + client + .execute("DELETE FROM nft WHERE signature = ANY($1)", &[&signatures]) + .await?; + client + .execute( + "DELETE FROM marketing WHERE signature = ANY($1)", + &[&signatures], + ) + .await?; + client + .execute( + "DELETE FROM vanity_address WHERE signature = ANY($1)", + &[&signatures], + ) + .await?; + client + .execute( + "DELETE FROM swap WHERE signature1 = ANY($1) OR signature2 = ANY($1)", + &[&signatures], + ) + .await?; + client + .execute( + "DELETE FROM loan_contract WHERE signature1 = ANY($1) OR signature2 = ANY($1)", + &[&signatures], + ) + .await?; + client + .execute( + "DELETE FROM loan_payment WHERE signature = ANY($1)", + &[&signatures], + ) + .await?; + client + .execute( + "DELETE FROM collateral_claim WHERE signature = ANY($1)", + &[&signatures], + ) + .await?; + + Ok(()) +} diff --git a/src/records/memory/mempool/schema.rs b/src/records/memory/mempool/schema.rs index 83876dc..d41c1ba 100644 --- a/src/records/memory/mempool/schema.rs +++ b/src/records/memory/mempool/schema.rs @@ -1,21 +1,21 @@ -use super::*; - -pub async fn init_db() -> Result<()> { - // Initialize the shared Postgres client used by the mempool tables. - if DB.get().is_some() { - return Ok(()); - } - - let password = SETTINGS - .pg_password - .as_deref() - .expect("Postgres password must be set in settings.ini"); - - let conn_str = format!( - "host={} port={} user={} password={} dbname={}", - SETTINGS.pg_host, SETTINGS.pg_port, SETTINGS.pg_user, password, SETTINGS.pg_dbname - ); - +use super::*; + +pub async fn init_db() -> Result<()> { + // Initialize the shared Postgres client used by the mempool tables. + if DB.get().is_some() { + return Ok(()); + } + + let password = SETTINGS + .pg_password + .as_deref() + .expect("Postgres password must be set in settings.ini"); + + let conn_str = format!( + "host={} port={} user={} password={} dbname={}", + SETTINGS.pg_host, SETTINGS.pg_port, SETTINGS.pg_user, password, SETTINGS.pg_dbname + ); + let (client, connection) = tokio_postgres::connect(&conn_str, NoTls) .await .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 // lifetime of the shared client. tokio::spawn(async move { - if let Err(e) = connection.await { - eprintln!("Postgres connection error: {e}"); - } - }); - - DB.set(client) - .map_err(|_| anyhow!("DB already initialized"))?; - - Ok(()) -} - -pub async fn setup_mempool() -> Result<()> { - // Create or migrate the mempool schema, deduplicate any stale rows, - // add the selection indexes, and start from an empty live mempool. - let client = DB.get().expect("DB not initialized"); - + if let Err(e) = connection.await { + eprintln!("Postgres connection error: {e}"); + } + }); + + DB.set(client) + .map_err(|_| anyhow!("DB already initialized"))?; + + Ok(()) +} + +pub async fn setup_mempool() -> Result<()> { + // Create or migrate the mempool schema, deduplicate any stale rows, + // add the selection indexes, and start from an empty live mempool. + let client = DB.get().expect("DB not initialized"); + let schema = r#" - CREATE TABLE IF NOT EXISTS transfer ( - id BIGSERIAL PRIMARY KEY, - time INTEGER NOT NULL, - fee BIGINT NOT NULL, - sender TEXT, - value BIGINT, - coin VARCHAR(15), - nft_series INTEGER NOT NULL DEFAULT 0, - receiver TEXT NOT NULL, - hash VARCHAR(64) NOT NULL, - signature TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS token ( - id BIGSERIAL PRIMARY KEY, - time INTEGER NOT NULL, - fee BIGINT NOT NULL, - creator TEXT NOT NULL, - number BIGINT NOT NULL, - hard_limit SMALLINT NOT NULL DEFAULT 1, - ticker VARCHAR(15) NOT NULL, - hash VARCHAR(64) NOT NULL, - signature TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS issue_token ( - id BIGSERIAL PRIMARY KEY, - time INTEGER NOT NULL, - fee BIGINT NOT NULL, - creator TEXT NOT NULL, - number BIGINT NOT NULL, - ticker VARCHAR(15) NOT NULL, - hash VARCHAR(64) NOT NULL, - signature TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS burn ( - id BIGSERIAL PRIMARY KEY, - time INTEGER NOT NULL, - fee BIGINT NOT NULL, - address TEXT NOT NULL, - coin VARCHAR(15) NOT NULL, - nft_series INTEGER NOT NULL DEFAULT 0, - value BIGINT NOT NULL, - hash VARCHAR(64) NOT NULL, - signature TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS nft ( - id BIGSERIAL PRIMARY KEY, - fee BIGINT NOT NULL, - time INTEGER NOT NULL, - creator TEXT NOT NULL, - nft_name VARCHAR(15), - series SMALLINT NOT NULL, - count BIGINT NOT NULL DEFAULT 1, - hash VARCHAR(64) NOT NULL, - signature TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS marketing ( - id BIGSERIAL PRIMARY KEY, - time INTEGER NOT NULL, - fee BIGINT NOT NULL, - advertiser TEXT NOT NULL, - hash VARCHAR(64) NOT NULL, - signature TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS vanity_address ( - id BIGSERIAL PRIMARY KEY, - time INTEGER NOT NULL, - fee BIGINT NOT NULL, - address TEXT NOT NULL, - vanity_address TEXT NOT NULL, - hash VARCHAR(64) NOT NULL, - signature TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS swap ( - id BIGSERIAL PRIMARY KEY, - fee1 BIGINT NOT NULL, - fee2 BIGINT NOT NULL, - time INTEGER NOT NULL, - ticker1 VARCHAR(15), - nft_series1 INTEGER NOT NULL DEFAULT 0, - ticker2 VARCHAR(15), - nft_series2 INTEGER NOT NULL DEFAULT 0, - value1 BIGINT NOT NULL, - value2 BIGINT NOT NULL, - sender1 TEXT NOT NULL, - tip1 BIGINT NOT NULL, - tip2 BIGINT NOT NULL, - sender2 TEXT NOT NULL, - hash VARCHAR(64) NOT NULL, - signature1 TEXT NOT NULL, - signature2 TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS loan_contract ( - id BIGSERIAL PRIMARY KEY, - fee BIGINT NOT NULL, - time INTEGER NOT NULL, - loan_coin VARCHAR(15), - loan_amount BIGINT NOT NULL, - lender TEXT NOT NULL, - collateral VARCHAR(15), - collateral_amount BIGINT NOT NULL, - borrower TEXT NOT NULL, - txid VARCHAR(64) NOT NULL, - hash VARCHAR(64) NOT NULL, - signature1 TEXT NOT NULL, - signature2 TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS loan_payment ( - id BIGSERIAL PRIMARY KEY, - fee BIGINT NOT NULL, - time INTEGER NOT NULL, - payback_amount BIGINT NOT NULL, - contract_hash VARCHAR(64) NOT NULL, - address TEXT NOT NULL, - tip BIGINT NOT NULL, - txid VARCHAR(64), - hash VARCHAR(64) NOT NULL, - signature TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - CREATE TABLE IF NOT EXISTS collateral_claim ( - id BIGSERIAL PRIMARY KEY, - time INTEGER NOT NULL, - fee BIGINT NOT NULL, - address TEXT NOT NULL, - contract_hash VARCHAR(64) NOT NULL, - hash VARCHAR(64) NOT NULL, - signature TEXT NOT NULL, - processed bool DEFAULT false, - processed_block_number INTEGER DEFAULT NULL, - original BYTEA NOT NULL - ); - - ALTER TABLE loan_payment ADD COLUMN IF NOT EXISTS txid VARCHAR(64); + CREATE TABLE IF NOT EXISTS transfer ( + id BIGSERIAL PRIMARY KEY, + time INTEGER NOT NULL, + fee BIGINT NOT NULL, + sender TEXT, + value BIGINT, + coin VARCHAR(15), + nft_series INTEGER NOT NULL DEFAULT 0, + receiver TEXT NOT NULL, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS token ( + id BIGSERIAL PRIMARY KEY, + time INTEGER NOT NULL, + fee BIGINT NOT NULL, + creator TEXT NOT NULL, + number BIGINT NOT NULL, + hard_limit SMALLINT NOT NULL DEFAULT 1, + ticker VARCHAR(15) NOT NULL, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS issue_token ( + id BIGSERIAL PRIMARY KEY, + time INTEGER NOT NULL, + fee BIGINT NOT NULL, + creator TEXT NOT NULL, + number BIGINT NOT NULL, + ticker VARCHAR(15) NOT NULL, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS burn ( + id BIGSERIAL PRIMARY KEY, + time INTEGER NOT NULL, + fee BIGINT NOT NULL, + address TEXT NOT NULL, + coin VARCHAR(15) NOT NULL, + nft_series INTEGER NOT NULL DEFAULT 0, + value BIGINT NOT NULL, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS nft ( + id BIGSERIAL PRIMARY KEY, + fee BIGINT NOT NULL, + time INTEGER NOT NULL, + creator TEXT NOT NULL, + nft_name VARCHAR(15), + series SMALLINT NOT NULL, + count BIGINT NOT NULL DEFAULT 1, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS marketing ( + id BIGSERIAL PRIMARY KEY, + time INTEGER NOT NULL, + fee BIGINT NOT NULL, + advertiser TEXT NOT NULL, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS vanity_address ( + id BIGSERIAL PRIMARY KEY, + time INTEGER NOT NULL, + fee BIGINT NOT NULL, + address TEXT NOT NULL, + vanity_address TEXT NOT NULL, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS swap ( + id BIGSERIAL PRIMARY KEY, + fee1 BIGINT NOT NULL, + fee2 BIGINT NOT NULL, + time INTEGER NOT NULL, + ticker1 VARCHAR(15), + nft_series1 INTEGER NOT NULL DEFAULT 0, + ticker2 VARCHAR(15), + nft_series2 INTEGER NOT NULL DEFAULT 0, + value1 BIGINT NOT NULL, + value2 BIGINT NOT NULL, + sender1 TEXT NOT NULL, + tip1 BIGINT NOT NULL, + tip2 BIGINT NOT NULL, + sender2 TEXT NOT NULL, + hash VARCHAR(64) NOT NULL, + signature1 TEXT NOT NULL, + signature2 TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS loan_contract ( + id BIGSERIAL PRIMARY KEY, + fee BIGINT NOT NULL, + time INTEGER NOT NULL, + loan_coin VARCHAR(15), + loan_amount BIGINT NOT NULL, + lender TEXT NOT NULL, + collateral VARCHAR(15), + collateral_amount BIGINT NOT NULL, + borrower TEXT NOT NULL, + txid VARCHAR(64) NOT NULL, + hash VARCHAR(64) NOT NULL, + signature1 TEXT NOT NULL, + signature2 TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS loan_payment ( + id BIGSERIAL PRIMARY KEY, + fee BIGINT NOT NULL, + time INTEGER NOT NULL, + payback_amount BIGINT NOT NULL, + contract_hash VARCHAR(64) NOT NULL, + address TEXT NOT NULL, + tip BIGINT NOT NULL, + txid VARCHAR(64), + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS collateral_claim ( + id BIGSERIAL PRIMARY KEY, + time INTEGER NOT NULL, + fee BIGINT NOT NULL, + address TEXT NOT NULL, + contract_hash VARCHAR(64) NOT NULL, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + 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 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 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 transfer ALTER COLUMN sender TYPE TEXT; - ALTER TABLE transfer ALTER COLUMN receiver TYPE TEXT; - ALTER TABLE transfer ALTER COLUMN signature TYPE TEXT; - ALTER TABLE token ALTER COLUMN creator 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 signature TYPE TEXT; - ALTER TABLE burn ALTER COLUMN address TYPE TEXT; - ALTER TABLE burn ALTER COLUMN signature TYPE TEXT; - ALTER TABLE nft ALTER COLUMN creator TYPE TEXT; - ALTER TABLE nft ALTER COLUMN signature TYPE TEXT; - ALTER TABLE marketing ALTER COLUMN advertiser 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 vanity_address TYPE TEXT; - ALTER TABLE vanity_address ALTER COLUMN signature TYPE TEXT; - ALTER TABLE swap ALTER COLUMN sender1 TYPE TEXT; - ALTER TABLE swap ALTER COLUMN sender2 TYPE TEXT; - ALTER TABLE swap ALTER COLUMN signature1 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 borrower TYPE TEXT; - ALTER TABLE loan_contract ALTER COLUMN signature1 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 signature TYPE TEXT; - ALTER TABLE collateral_claim ALTER COLUMN address TYPE TEXT; - ALTER TABLE collateral_claim ALTER COLUMN signature TYPE TEXT; + 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 receiver TYPE TEXT; + ALTER TABLE transfer ALTER COLUMN signature TYPE TEXT; + ALTER TABLE token ALTER COLUMN creator 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 signature TYPE TEXT; + ALTER TABLE burn ALTER COLUMN address TYPE TEXT; + ALTER TABLE burn ALTER COLUMN signature TYPE TEXT; + ALTER TABLE nft ALTER COLUMN creator TYPE TEXT; + ALTER TABLE nft ALTER COLUMN signature TYPE TEXT; + ALTER TABLE marketing ALTER COLUMN advertiser 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 vanity_address TYPE TEXT; + ALTER TABLE vanity_address ALTER COLUMN signature TYPE TEXT; + ALTER TABLE swap ALTER COLUMN sender1 TYPE TEXT; + ALTER TABLE swap ALTER COLUMN sender2 TYPE TEXT; + ALTER TABLE swap ALTER COLUMN signature1 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 borrower TYPE TEXT; + ALTER TABLE loan_contract ALTER COLUMN signature1 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 signature TYPE TEXT; + ALTER TABLE collateral_claim ALTER COLUMN address TYPE TEXT; + ALTER TABLE collateral_claim ALTER COLUMN signature TYPE TEXT; "#; // 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?; let dedupe = r#" - DELETE FROM transfer a - USING transfer b - WHERE a.id > b.id AND a.signature = b.signature; - - DELETE FROM token a - USING token b - WHERE a.id > b.id AND a.signature = b.signature; - - DELETE FROM issue_token a - USING issue_token b - WHERE a.id > b.id AND a.signature = b.signature; - - DELETE FROM burn a - USING burn b - WHERE a.id > b.id AND a.signature = b.signature; - - DELETE FROM nft a - USING nft b - WHERE a.id > b.id AND a.signature = b.signature; - - DELETE FROM marketing a - USING marketing b - WHERE a.id > b.id AND a.signature = b.signature; - - DELETE FROM vanity_address a - USING vanity_address b - WHERE a.id > b.id AND a.signature = b.signature; - - DELETE FROM swap a - USING swap b - WHERE a.id > b.id AND a.signature1 = b.signature1; - - DELETE FROM swap a - USING swap b - WHERE a.id > b.id AND a.signature2 = b.signature2; - - DELETE FROM loan_contract a - USING loan_contract b - WHERE a.id > b.id AND a.signature1 = b.signature1; - - DELETE FROM loan_contract a - USING loan_contract b - WHERE a.id > b.id AND a.signature2 = b.signature2; - - DELETE FROM loan_payment a - USING loan_payment b - WHERE a.id > b.id AND a.signature = b.signature; - - DELETE FROM collateral_claim a - USING collateral_claim b - WHERE a.id > b.id AND a.signature = b.signature; + DELETE FROM transfer a + USING transfer b + WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM token a + USING token b + WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM issue_token a + USING issue_token b + WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM burn a + USING burn b + WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM nft a + USING nft b + WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM marketing a + USING marketing b + WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM vanity_address a + USING vanity_address b + WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM swap a + USING swap b + WHERE a.id > b.id AND a.signature1 = b.signature1; + + DELETE FROM swap a + USING swap b + WHERE a.id > b.id AND a.signature2 = b.signature2; + + DELETE FROM loan_contract a + USING loan_contract b + WHERE a.id > b.id AND a.signature1 = b.signature1; + + DELETE FROM loan_contract a + USING loan_contract b + WHERE a.id > b.id AND a.signature2 = b.signature2; + + DELETE FROM loan_payment a + USING loan_payment b + WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM collateral_claim a + USING collateral_claim b + WHERE a.id > b.id AND a.signature = b.signature; "#; // 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?; 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_sig_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_sig_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_sig_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_sig_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_sig_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_sig_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_sig_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_sig1_idx ON swap (signature1); - 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_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_sig1_idx ON loan_contract (signature1); - 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_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_sig_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_sig_idx ON collateral_claim (signature); - CREATE UNIQUE INDEX IF NOT EXISTS collateral_claim_sig_unique_idx ON collateral_claim (signature); + 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 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_sig_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_sig_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_sig_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_sig_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_sig_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_sig_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_sig1_idx ON swap (signature1); + 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_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_sig1_idx ON loan_contract (signature1); + 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_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_sig_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_sig_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 // pending copy of each transaction. @@ -364,35 +364,34 @@ pub async fn setup_mempool() -> Result<()> { // Live mempool data is not restored across startup. clear_mempool().await?; - - Ok(()) -} - -pub async fn clear_mempool() -> Result<()> { - // Startup clears any leftover mempool rows so a node restart begins - // from a clean pending-transaction state. - let client = DB.get().expect("DB not initialized"); - - client - .batch_execute( - r#" - TRUNCATE TABLE - transfer, - token, - issue_token, - burn, - nft, - marketing, - vanity_address, - swap, - loan_contract, - loan_payment, - collateral_claim - RESTART IDENTITY; - "#, - ) - .await?; - - Ok(()) -} - + + Ok(()) +} + +pub async fn clear_mempool() -> Result<()> { + // Startup clears any leftover mempool rows so a node restart begins + // from a clean pending-transaction state. + let client = DB.get().expect("DB not initialized"); + + client + .batch_execute( + r#" + TRUNCATE TABLE + transfer, + token, + issue_token, + burn, + nft, + marketing, + vanity_address, + swap, + loan_contract, + loan_payment, + collateral_claim + RESTART IDENTITY; + "#, + ) + .await?; + + Ok(()) +} diff --git a/src/records/memory/mempool/selection.rs b/src/records/memory/mempool/selection.rs index be6e011..e8c2e23 100644 --- a/src/records/memory/mempool/selection.rs +++ b/src/records/memory/mempool/selection.rs @@ -1,209 +1,209 @@ use super::*; use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; - -pub async fn select_transactions_for_block(limit: i64) -> Result { - // Pull the highest-priority unprocessed rows across all mempool - // tables, keeping the original bytes for block-file streaming later. - let client = DB.get().expect("DB not initialized"); - let rows = client - .query( - r#" - SELECT * FROM ( - SELECT - 'transfer'::TEXT AS kind, id, fee AS priority_fee, time, - sender, receiver, coin, value, nft_series, - NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, - NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, - NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, - NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, - NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1, - NULL::BIGINT AS tip2, NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, - NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, NULL::TEXT AS collateral, - NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, - NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address, - hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM transfer - WHERE processed = false - - UNION ALL - - SELECT - 'token'::TEXT AS kind, id, fee AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, - number, creator, ticker, NULL::TEXT AS nft_name, NULL::SMALLINT AS series, - NULL::BIGINT AS count, NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, - NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, - NULL::BIGINT AS value1, NULL::BIGINT AS value2, NULL::TEXT AS sender1, - NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, NULL::TEXT AS sender2, - NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, - NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, - NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address, - hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, hard_limit, original - FROM token - WHERE processed = false - - UNION ALL - - SELECT - 'issue_token'::TEXT AS kind, id, fee AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, - number, creator, ticker, NULL::TEXT AS nft_name, NULL::SMALLINT AS series, - NULL::BIGINT AS count, NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, - NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, - NULL::BIGINT AS value1, NULL::BIGINT AS value2, NULL::TEXT AS sender1, - NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, NULL::TEXT AS sender2, - NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, - NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, - NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address, - hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM issue_token - WHERE processed = false - - UNION ALL - - SELECT - 'burn'::TEXT AS kind, id, fee AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, coin, value, nft_series, - NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, - NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, - NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, - NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, - NULL::BIGINT AS value1, NULL::BIGINT AS value2, NULL::TEXT AS sender1, - NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, NULL::TEXT AS sender2, - NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, - NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, - NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, address, - hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM burn - WHERE processed = false - - UNION ALL - - SELECT - 'nft'::TEXT AS kind, id, fee AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, - NULL::BIGINT AS number, creator, NULL::TEXT AS ticker, nft_name, series, count, - NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, - NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, - NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1, - NULL::BIGINT AS tip2, NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, - NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, NULL::TEXT AS collateral, - NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, - NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address, - hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM nft - WHERE processed = false - - UNION ALL - - SELECT - 'marketing'::TEXT AS kind, id, fee AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, - NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, - NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, - advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, - NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2, - NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, - NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, - NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, - NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, - NULL::TEXT AS address, hash, signature AS signature1, NULL::TEXT AS signature2, - NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM marketing - WHERE processed = false - - UNION ALL - - SELECT - 'vanity_address'::TEXT AS kind, id, fee AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, - NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, - NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, - NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, - NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2, - NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, - NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, - NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, - NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, - address, hash, signature AS signature1, NULL::TEXT AS signature2, - vanity_address AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM vanity_address - WHERE processed = false - - UNION ALL - - SELECT - 'swap'::TEXT AS kind, id, GREATEST(fee1, fee2) AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, - NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, - NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, - NULL::TEXT AS advertiser, fee1, fee2, ticker1, nft_series1, ticker2, nft_series2, value1, value2, - sender1, tip1, tip2, sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, - NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, - NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, - NULL::TEXT AS address, hash, signature1, signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM swap - WHERE processed = false - - UNION ALL - - SELECT - 'loan_contract'::TEXT AS kind, id, fee AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, - NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, - NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, - NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, - NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, - NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1, - NULL::BIGINT AS tip2, NULL::TEXT AS sender2, loan_coin, loan_amount, lender, - collateral, collateral_amount, borrower, NULL::BIGINT AS payback_amount, - NULL::TEXT AS contract_hash, NULL::TEXT AS address, hash, signature1, signature2, - txid AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM loan_contract - WHERE processed = false - - UNION ALL - - SELECT - 'loan_payment'::TEXT AS kind, id, fee AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, - NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, - NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, - NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, - NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, - NULL::BIGINT AS value2, NULL::TEXT AS sender1, tip AS tip1, NULL::BIGINT AS tip2, - NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, - NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, - NULL::TEXT AS borrower, payback_amount, contract_hash, address, hash, - signature AS signature1, NULL::TEXT AS signature2, txid AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM loan_payment - WHERE processed = false - - UNION ALL - - SELECT - 'collateral_claim'::TEXT AS kind, id, fee AS priority_fee, time, - NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, - NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, - NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, - NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, - NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, - NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1, - NULL::BIGINT AS tip2, NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, - NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, NULL::TEXT AS collateral, - NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, - NULL::BIGINT AS payback_amount, contract_hash, address, hash, - signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original - FROM collateral_claim - WHERE processed = false - ) AS combined - ORDER BY priority_fee DESC, time ASC, kind ASC, id ASC - LIMIT $1 - "#, - &[&limit], - ) - .await?; - + +pub async fn select_transactions_for_block(limit: i64) -> Result { + // Pull the highest-priority unprocessed rows across all mempool + // tables, keeping the original bytes for block-file streaming later. + let client = DB.get().expect("DB not initialized"); + let rows = client + .query( + r#" + SELECT * FROM ( + SELECT + 'transfer'::TEXT AS kind, id, fee AS priority_fee, time, + sender, receiver, coin, value, nft_series, + NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, + NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, + NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, + NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, + NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1, + NULL::BIGINT AS tip2, NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, + NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, NULL::TEXT AS collateral, + NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, + NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address, + hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM transfer + WHERE processed = false + + UNION ALL + + SELECT + 'token'::TEXT AS kind, id, fee AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, + number, creator, ticker, NULL::TEXT AS nft_name, NULL::SMALLINT AS series, + NULL::BIGINT AS count, NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, + NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, + NULL::BIGINT AS value1, NULL::BIGINT AS value2, NULL::TEXT AS sender1, + NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, NULL::TEXT AS sender2, + NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, + NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, + NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address, + hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, hard_limit, original + FROM token + WHERE processed = false + + UNION ALL + + SELECT + 'issue_token'::TEXT AS kind, id, fee AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, + number, creator, ticker, NULL::TEXT AS nft_name, NULL::SMALLINT AS series, + NULL::BIGINT AS count, NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, + NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, + NULL::BIGINT AS value1, NULL::BIGINT AS value2, NULL::TEXT AS sender1, + NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, NULL::TEXT AS sender2, + NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, + NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, + NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address, + hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM issue_token + WHERE processed = false + + UNION ALL + + SELECT + 'burn'::TEXT AS kind, id, fee AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, coin, value, nft_series, + NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, + NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, + NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, + NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, + NULL::BIGINT AS value1, NULL::BIGINT AS value2, NULL::TEXT AS sender1, + NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, NULL::TEXT AS sender2, + NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, + NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, + NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, address, + hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM burn + WHERE processed = false + + UNION ALL + + SELECT + 'nft'::TEXT AS kind, id, fee AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, + NULL::BIGINT AS number, creator, NULL::TEXT AS ticker, nft_name, series, count, + NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, + NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, + NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1, + NULL::BIGINT AS tip2, NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, + NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, NULL::TEXT AS collateral, + NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, + NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, NULL::TEXT AS address, + hash, signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM nft + WHERE processed = false + + UNION ALL + + SELECT + 'marketing'::TEXT AS kind, id, fee AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, + NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, + NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, + advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, + NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2, + NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, + NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, + NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, + NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, + NULL::TEXT AS address, hash, signature AS signature1, NULL::TEXT AS signature2, + NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM marketing + WHERE processed = false + + UNION ALL + + SELECT + 'vanity_address'::TEXT AS kind, id, fee AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, + NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, + NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, + NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, + NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2, + NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2, + NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, + NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, + NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, + address, hash, signature AS signature1, NULL::TEXT AS signature2, + vanity_address AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM vanity_address + WHERE processed = false + + UNION ALL + + SELECT + 'swap'::TEXT AS kind, id, GREATEST(fee1, fee2) AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, + NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, + NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, + NULL::TEXT AS advertiser, fee1, fee2, ticker1, nft_series1, ticker2, nft_series2, value1, value2, + sender1, tip1, tip2, sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, + NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, + NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash, + NULL::TEXT AS address, hash, signature1, signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM swap + WHERE processed = false + + UNION ALL + + SELECT + 'loan_contract'::TEXT AS kind, id, fee AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, + NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, + NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, + NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, + NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, + NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1, + NULL::BIGINT AS tip2, NULL::TEXT AS sender2, loan_coin, loan_amount, lender, + collateral, collateral_amount, borrower, NULL::BIGINT AS payback_amount, + NULL::TEXT AS contract_hash, NULL::TEXT AS address, hash, signature1, signature2, + txid AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM loan_contract + WHERE processed = false + + UNION ALL + + SELECT + 'loan_payment'::TEXT AS kind, id, fee AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, + NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, + NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, + NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, + NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, + NULL::BIGINT AS value2, NULL::TEXT AS sender1, tip AS tip1, NULL::BIGINT AS tip2, + NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount, + NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount, + NULL::TEXT AS borrower, payback_amount, contract_hash, address, hash, + signature AS signature1, NULL::TEXT AS signature2, txid AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM loan_payment + WHERE processed = false + + UNION ALL + + SELECT + 'collateral_claim'::TEXT AS kind, id, fee AS priority_fee, time, + NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series, + NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker, + NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count, + NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2, + NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2, NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, + NULL::BIGINT AS value2, NULL::TEXT AS sender1, NULL::BIGINT AS tip1, + NULL::BIGINT AS tip2, NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, + NULL::BIGINT AS loan_amount, NULL::TEXT AS lender, NULL::TEXT AS collateral, + NULL::BIGINT AS collateral_amount, NULL::TEXT AS borrower, + NULL::BIGINT AS payback_amount, contract_hash, address, hash, + signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original + FROM collateral_claim + WHERE processed = false + ) AS combined + ORDER BY priority_fee DESC, time ASC, kind ASC, id ASC + LIMIT $1 + "#, + &[&limit], + ) + .await?; + let mut transactions = Vec::with_capacity(rows.len()); let mut originals = Vec::with_capacity(rows.len()); for row in rows { @@ -215,118 +215,118 @@ pub async fn select_transactions_for_block(limit: i64) -> Result SelectedMempoolTransaction::Transfer { - id: row.get("id"), - fee: row.get("priority_fee"), - sender: required_string(&row, "sender")?, - value: row.get("value"), - coin: required_string(&row, "coin")?, - nft_series: row.get("nft_series"), - receiver: required_string(&row, "receiver")?, - hash: row.get("hash"), - }, - "token" => SelectedMempoolTransaction::Token { - id: row.get("id"), - fee: row.get("priority_fee"), - creator: required_string(&row, "creator")?, - number: row.get("number"), - ticker: required_string(&row, "ticker")?, - hash: row.get("hash"), - }, - "issue_token" => SelectedMempoolTransaction::IssueToken { - id: row.get("id"), - fee: row.get("priority_fee"), - creator: required_string(&row, "creator")?, - number: row.get("number"), - ticker: required_string(&row, "ticker")?, - hash: row.get("hash"), - }, - "burn" => SelectedMempoolTransaction::Burn { - id: row.get("id"), - fee: row.get("priority_fee"), - address: required_string(&row, "address")?, - coin: required_string(&row, "coin")?, - nft_series: row.get("nft_series"), - value: row.get("value"), - hash: row.get("hash"), - }, - "nft" => SelectedMempoolTransaction::Nft { - id: row.get("id"), - fee: row.get("priority_fee"), - creator: required_string(&row, "creator")?, - nft_name: required_string(&row, "nft_name")?, - series: row.get("series"), - count: row.get("count"), - hash: row.get("hash"), - }, - "marketing" => SelectedMempoolTransaction::Marketing { - id: row.get("id"), - fee: row.get("priority_fee"), - advertiser: required_string(&row, "advertiser")?, - hash: row.get("hash"), - }, - "vanity_address" => SelectedMempoolTransaction::Vanity { - id: row.get("id"), - fee: row.get("priority_fee"), - address: required_string(&row, "address")?, - vanity_address: required_string(&row, "stored_txid")?, - hash: row.get("hash"), - }, - "swap" => SelectedMempoolTransaction::Swap { - id: row.get("id"), - fee1: row.get("fee1"), - fee2: row.get("fee2"), - ticker1: required_string(&row, "ticker1")?, - nft_series1: row.get("nft_series1"), - ticker2: required_string(&row, "ticker2")?, - nft_series2: row.get("nft_series2"), - value1: row.get("value1"), - value2: row.get("value2"), - sender1: required_string(&row, "sender1")?, - tip1: row.get("tip1"), - tip2: row.get("tip2"), - sender2: required_string(&row, "sender2")?, - hash: row.get("hash"), - }, - "loan_contract" => SelectedMempoolTransaction::Lender { - id: row.get("id"), - fee: row.get("priority_fee"), - loan_coin: required_string(&row, "loan_coin")?, - loan_amount: row.get("loan_amount"), - lender: required_string(&row, "lender")?, - collateral: required_string(&row, "collateral")?, - collateral_amount: row.get("collateral_amount"), - borrower: required_string(&row, "borrower")?, - txid: required_string(&row, "stored_txid")?, - hash: row.get("hash"), - }, - "loan_payment" => SelectedMempoolTransaction::Borrower { - id: row.get("id"), - fee: row.get("priority_fee"), - payback_amount: row.get("payback_amount"), - contract_hash: required_string(&row, "contract_hash")?, - address: required_string(&row, "address")?, - tip: row.get("tip1"), - hash: row.get("hash"), - }, - "collateral_claim" => SelectedMempoolTransaction::Collateral { - id: row.get("id"), - fee: row.get("priority_fee"), - address: required_string(&row, "address")?, - contract_hash: required_string(&row, "contract_hash")?, - hash: row.get("hash"), - }, + "transfer" => SelectedMempoolTransaction::Transfer { + id: row.get("id"), + fee: row.get("priority_fee"), + sender: required_string(&row, "sender")?, + value: row.get("value"), + coin: required_string(&row, "coin")?, + nft_series: row.get("nft_series"), + receiver: required_string(&row, "receiver")?, + hash: row.get("hash"), + }, + "token" => SelectedMempoolTransaction::Token { + id: row.get("id"), + fee: row.get("priority_fee"), + creator: required_string(&row, "creator")?, + number: row.get("number"), + ticker: required_string(&row, "ticker")?, + hash: row.get("hash"), + }, + "issue_token" => SelectedMempoolTransaction::IssueToken { + id: row.get("id"), + fee: row.get("priority_fee"), + creator: required_string(&row, "creator")?, + number: row.get("number"), + ticker: required_string(&row, "ticker")?, + hash: row.get("hash"), + }, + "burn" => SelectedMempoolTransaction::Burn { + id: row.get("id"), + fee: row.get("priority_fee"), + address: required_string(&row, "address")?, + coin: required_string(&row, "coin")?, + nft_series: row.get("nft_series"), + value: row.get("value"), + hash: row.get("hash"), + }, + "nft" => SelectedMempoolTransaction::Nft { + id: row.get("id"), + fee: row.get("priority_fee"), + creator: required_string(&row, "creator")?, + nft_name: required_string(&row, "nft_name")?, + series: row.get("series"), + count: row.get("count"), + hash: row.get("hash"), + }, + "marketing" => SelectedMempoolTransaction::Marketing { + id: row.get("id"), + fee: row.get("priority_fee"), + advertiser: required_string(&row, "advertiser")?, + hash: row.get("hash"), + }, + "vanity_address" => SelectedMempoolTransaction::Vanity { + id: row.get("id"), + fee: row.get("priority_fee"), + address: required_string(&row, "address")?, + vanity_address: required_string(&row, "stored_txid")?, + hash: row.get("hash"), + }, + "swap" => SelectedMempoolTransaction::Swap { + id: row.get("id"), + fee1: row.get("fee1"), + fee2: row.get("fee2"), + ticker1: required_string(&row, "ticker1")?, + nft_series1: row.get("nft_series1"), + ticker2: required_string(&row, "ticker2")?, + nft_series2: row.get("nft_series2"), + value1: row.get("value1"), + value2: row.get("value2"), + sender1: required_string(&row, "sender1")?, + tip1: row.get("tip1"), + tip2: row.get("tip2"), + sender2: required_string(&row, "sender2")?, + hash: row.get("hash"), + }, + "loan_contract" => SelectedMempoolTransaction::Lender { + id: row.get("id"), + fee: row.get("priority_fee"), + loan_coin: required_string(&row, "loan_coin")?, + loan_amount: row.get("loan_amount"), + lender: required_string(&row, "lender")?, + collateral: required_string(&row, "collateral")?, + collateral_amount: row.get("collateral_amount"), + borrower: required_string(&row, "borrower")?, + txid: required_string(&row, "stored_txid")?, + hash: row.get("hash"), + }, + "loan_payment" => SelectedMempoolTransaction::Borrower { + id: row.get("id"), + fee: row.get("priority_fee"), + payback_amount: row.get("payback_amount"), + contract_hash: required_string(&row, "contract_hash")?, + address: required_string(&row, "address")?, + tip: row.get("tip1"), + hash: row.get("hash"), + }, + "collateral_claim" => SelectedMempoolTransaction::Collateral { + id: row.get("id"), + fee: row.get("priority_fee"), + address: required_string(&row, "address")?, + contract_hash: required_string(&row, "contract_hash")?, + hash: row.get("hash"), + }, _ => return Err(anyhow!("Unsupported mempool kind {kind}")), }; - transactions.push(transaction); - } - - Ok(SelectedMempoolBatch { - transactions, - originals, - }) -} - + transactions.push(transaction); + } + + Ok(SelectedMempoolBatch { + transactions, + originals, + }) +} + pub async fn apply_selected_transaction_math( batch: &SelectedMempoolBatch, db: &Db, @@ -338,37 +338,37 @@ pub async fn apply_selected_transaction_math( // Net balance deltas are collected first so multiple changes to the same // address/asset pair become one balance-sheet write. let mut balance_changes: HashMap = HashMap::new(); - let miner_address = address_key_bytes(db, &miner); - let base_coin = BASECOIN.as_bytes().to_vec(); - let mut tx_index = start_index; + let miner_address = address_key_bytes(db, &miner); + let base_coin = BASECOIN.as_bytes().to_vec(); + let mut tx_index = start_index; for tx in &batch.transactions { // Transaction index is stored with the block number for later txid // lookups inside the saved block payload. tx_index = tx_index.wrapping_add(1); match tx { - SelectedMempoolTransaction::Transfer { - fee, - sender, - value, - coin, - nft_series, - receiver, - hash, - .. + SelectedMempoolTransaction::Transfer { + fee, + sender, + value, + coin, + nft_series, + receiver, + hash, + .. } => { // Transfers move the asset, charge the base fee, and credit // that fee to the miner including this transaction. let transfer_asset = nft_asset_name(coin, *nft_series as u32); - add_balance_change(db, &mut balance_changes, sender, &transfer_asset, -*value); - add_balance_change(db, &mut balance_changes, receiver, &transfer_asset, *value); - add_balance_change(db, &mut balance_changes, sender, &BASECOIN, -*fee); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); + add_balance_change(db, &mut balance_changes, sender, &transfer_asset, -*value); + add_balance_change(db, &mut balance_changes, receiver, &transfer_asset, *value); + add_balance_change(db, &mut balance_changes, sender, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); let nft_tree = db.open_tree("nfts")?; if nft_tree.contains_key(transfer_asset.as_bytes())? { pending_effects.append_tree( @@ -383,101 +383,117 @@ pub async fn apply_selected_transaction_math( format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::Token { - fee, - creator, - number, - ticker, - hash, - .. + SelectedMempoolTransaction::Token { + fee, + creator, + number, + ticker, + hash, + .. } => { // Token creation mints the initial supply and records metadata // used by later issue and burn validation. add_balance_change(db, &mut balance_changes, creator, ticker, *number); - add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); - + add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); + let number_bytes = (*number as u64).to_le_bytes(); - pending_effects.set_tree("tokens", ticker.as_bytes().to_vec(), number_bytes.to_vec()); - - // Local mined blocks need to persist the token hard-limit - // metadata just like the downloaded-block save path does. - let client = DB.get().expect("DB not initialized"); - let token_row = client - .query_opt( - "SELECT hard_limit FROM token WHERE hash = $1 LIMIT 1", - &[hash], - ) - .await?; - let hard_limit = token_row - .and_then(|row| row.try_get::<_, Option>("hard_limit").ok().flatten()) - .unwrap_or(1) as u8; - pending_effects.set_tree("token_limits", ticker.as_bytes().to_vec(), vec![hard_limit]); + pending_effects.set_tree( + "tokens", + ticker.as_bytes().to_vec(), + number_bytes.to_vec(), + ); + + // Local mined blocks need to persist the token hard-limit + // metadata just like the downloaded-block save path does. + let client = DB.get().expect("DB not initialized"); + let token_row = client + .query_opt( + "SELECT hard_limit FROM token WHERE hash = $1 LIMIT 1", + &[hash], + ) + .await?; + let hard_limit = token_row + .and_then(|row| row.try_get::<_, Option>("hard_limit").ok().flatten()) + .unwrap_or(1) as u8; + pending_effects.set_tree( + "token_limits", + ticker.as_bytes().to_vec(), + vec![hard_limit], + ); pending_effects.set_tree( "token_origins", ticker.as_bytes().to_vec(), hash.as_bytes().to_vec(), ); - pending_effects.append_tree("token_history", ticker.as_bytes().to_vec(), decode(hash)?); + pending_effects.append_tree( + "token_history", + ticker.as_bytes().to_vec(), + decode(hash)?, + ); pending_effects.set_tree( "txid", decode(hash)?, format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::IssueToken { - fee, - creator, - number, - ticker, - hash, - .. - } => { - add_balance_change(db, &mut balance_changes, creator, ticker, *number); - add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); - - // Additional issuance increases the live fungible supply without - // changing the original token metadata entries. + SelectedMempoolTransaction::IssueToken { + fee, + creator, + number, + ticker, + hash, + .. + } => { + add_balance_change(db, &mut balance_changes, creator, ticker, *number); + add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); + + // Additional issuance increases the live fungible supply without + // changing the original token metadata entries. pending_effects.add_token_supply(ticker, *number as u64); - pending_effects.append_tree("token_history", ticker.as_bytes().to_vec(), decode(hash)?); + pending_effects.append_tree( + "token_history", + ticker.as_bytes().to_vec(), + decode(hash)?, + ); pending_effects.set_tree( "txid", decode(hash)?, format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::Burn { - fee, - address, - coin, - nft_series, - value, - hash, - .. - } => { - let burned_asset = nft_asset_name(coin, *nft_series as u32); - add_balance_change(db, &mut balance_changes, address, &burned_asset, -*value); - add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); - - // Live NFT state removes burned assets, while fungible token - // state reduces the stored supply by the burned amount. + SelectedMempoolTransaction::Burn { + fee, + address, + coin, + nft_series, + value, + hash, + .. + } => { + let burned_asset = nft_asset_name(coin, *nft_series as u32); + add_balance_change(db, &mut balance_changes, address, &burned_asset, -*value); + add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); + + // Live NFT state removes burned assets, while fungible token + // state reduces the stored supply by the burned amount. pending_effects.burn_asset_state(&burned_asset, coin, decode(hash)?, *value as u64); pending_effects.set_tree( "txid", @@ -485,28 +501,28 @@ pub async fn apply_selected_transaction_math( format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::Nft { - fee, - creator, - nft_name, - series, - count, - hash, - .. + SelectedMempoolTransaction::Nft { + fee, + creator, + nft_name, + series, + count, + hash, + .. } => { // Series-one NFT creation expands into numbered assets; later // series use the given NFT name as the asset key. if *series == 1 { - for item_number in 1..=*count { - let nft_save_name = nft_asset_name(nft_name, item_number as u32); - add_balance_change( - db, - &mut balance_changes, - creator, - &nft_save_name, - NFT_UNIT, - ); - + for item_number in 1..=*count { + let nft_save_name = nft_asset_name(nft_name, item_number as u32); + add_balance_change( + db, + &mut balance_changes, + creator, + &nft_save_name, + NFT_UNIT, + ); + pending_effects.set_tree( "nfts", nft_save_name.as_bytes().to_vec(), @@ -527,47 +543,55 @@ pub async fn apply_selected_transaction_math( add_balance_change(db, &mut balance_changes, creator, nft_name, NFT_UNIT); pending_effects.set_tree("nfts", nft_name.as_bytes().to_vec(), b"1".to_vec()); - pending_effects.set_tree("nft_origins", nft_name.as_bytes().to_vec(), decode(hash)?); - pending_effects.append_tree("nft_history", nft_name.as_bytes().to_vec(), decode(hash)?); + pending_effects.set_tree( + "nft_origins", + nft_name.as_bytes().to_vec(), + decode(hash)?, + ); + pending_effects.append_tree( + "nft_history", + nft_name.as_bytes().to_vec(), + decode(hash)?, + ); } - add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); + add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); pending_effects.set_tree( "txid", decode(hash)?, format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::Marketing { - fee, - advertiser, - hash, - .. - } => { - add_balance_change(db, &mut balance_changes, advertiser, &BASECOIN, -*fee); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); + SelectedMempoolTransaction::Marketing { + fee, + advertiser, + hash, + .. + } => { + add_balance_change(db, &mut balance_changes, advertiser, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); pending_effects.set_tree( "txid", decode(hash)?, format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::Vanity { - fee, - address, - vanity_address, - hash, - .. + SelectedMempoolTransaction::Vanity { + fee, + address, + vanity_address, + hash, + .. } => { // Vanity transactions update the alias registry during the // final block-effect commit so rollback can restore it. @@ -575,68 +599,68 @@ pub async fn apply_selected_transaction_math( add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); add_balance_change_bytes( &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); + miner_address.clone(), + base_coin.clone(), + *fee, + ); pending_effects.set_tree( "txid", decode(hash)?, format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::Swap { - fee1, - fee2, - ticker1, - nft_series1, - ticker2, - nft_series2, - value1, - value2, - sender1, - tip1, - tip2, - sender2, - hash, - .. + SelectedMempoolTransaction::Swap { + fee1, + fee2, + ticker1, + nft_series1, + ticker2, + nft_series2, + value1, + value2, + sender1, + tip1, + tip2, + sender2, + hash, + .. } => { // Swaps debit both sides first, then credit received assets, // miner fees, and optional asset tips. let asset1 = nft_asset_name(ticker1, *nft_series1 as u32); - let asset2 = nft_asset_name(ticker2, *nft_series2 as u32); - add_balance_change(db, &mut balance_changes, sender1, &asset1, -*value1); - add_balance_change(db, &mut balance_changes, sender1, &asset1, -*tip1); - add_balance_change(db, &mut balance_changes, sender2, &asset2, -*value2); - add_balance_change(db, &mut balance_changes, sender2, &asset2, -*tip2); - add_balance_change(db, &mut balance_changes, sender2, &asset1, *value1); - add_balance_change(db, &mut balance_changes, sender1, &asset2, *value2); - add_balance_change(db, &mut balance_changes, sender1, &BASECOIN, -*fee1); - add_balance_change(db, &mut balance_changes, sender2, &BASECOIN, -*fee2); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee1, - ); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee2, - ); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - asset1.as_bytes().to_vec(), - *tip1, - ); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - asset2.as_bytes().to_vec(), - *tip2, - ); + let asset2 = nft_asset_name(ticker2, *nft_series2 as u32); + add_balance_change(db, &mut balance_changes, sender1, &asset1, -*value1); + add_balance_change(db, &mut balance_changes, sender1, &asset1, -*tip1); + add_balance_change(db, &mut balance_changes, sender2, &asset2, -*value2); + add_balance_change(db, &mut balance_changes, sender2, &asset2, -*tip2); + add_balance_change(db, &mut balance_changes, sender2, &asset1, *value1); + add_balance_change(db, &mut balance_changes, sender1, &asset2, *value2); + add_balance_change(db, &mut balance_changes, sender1, &BASECOIN, -*fee1); + add_balance_change(db, &mut balance_changes, sender2, &BASECOIN, -*fee2); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee1, + ); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee2, + ); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + asset1.as_bytes().to_vec(), + *tip1, + ); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + asset2.as_bytes().to_vec(), + *tip2, + ); let nft_tree = db.open_tree("nfts")?; if nft_tree.contains_key(asset1.as_bytes())? { pending_effects.append_tree( @@ -658,46 +682,46 @@ pub async fn apply_selected_transaction_math( format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::Lender { - fee, - loan_coin, - loan_amount, - lender, - collateral, - collateral_amount, - borrower, - txid, - hash, - .. + SelectedMempoolTransaction::Lender { + fee, + loan_coin, + loan_amount, + lender, + collateral, + collateral_amount, + borrower, + txid, + hash, + .. } => { // Loan creation moves the loan asset to the borrower and locks // collateral under a contract-specific holding key. add_balance_change(db, &mut balance_changes, lender, &BASECOIN, -*fee); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); - add_balance_change(db, &mut balance_changes, lender, loan_coin, -*loan_amount); - add_balance_change(db, &mut balance_changes, borrower, loan_coin, *loan_amount); - add_balance_change( - db, - &mut balance_changes, - borrower, - collateral, - -*collateral_amount, - ); - - let collateral_holding = format!("collateral_{txid}"); - add_balance_change( - db, - &mut balance_changes, - &collateral_holding, - collateral, - *collateral_amount, - ); - + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); + add_balance_change(db, &mut balance_changes, lender, loan_coin, -*loan_amount); + add_balance_change(db, &mut balance_changes, borrower, loan_coin, *loan_amount); + add_balance_change( + db, + &mut balance_changes, + borrower, + collateral, + -*collateral_amount, + ); + + let collateral_holding = format!("collateral_{txid}"); + add_balance_change( + db, + &mut balance_changes, + &collateral_holding, + collateral, + *collateral_amount, + ); + pending_effects.set_tree("loan", decode(txid)?, b"true".to_vec()); let nft_tree = db.open_tree("nfts")?; if nft_tree.contains_key(collateral.as_bytes())? { @@ -720,51 +744,52 @@ pub async fn apply_selected_transaction_math( format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::Borrower { - fee, - payback_amount, - contract_hash, - address, - tip, - hash, - .. + SelectedMempoolTransaction::Borrower { + fee, + payback_amount, + contract_hash, + address, + tip, + hash, + .. } => { // Loan payments resolve the contract, then move repayment from // borrower to lender while paying any tip to the miner. let (loan_coin, lender) = resolve_loan_details(db, contract_hash).await?; - add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); - add_balance_change_bytes( - &mut balance_changes, - address_key_bytes(db, address), - loan_coin.clone(), - -*payback_amount, - ); - add_balance_change_bytes( - &mut balance_changes, - lender.clone(), - loan_coin.clone(), - *payback_amount, - ); - add_balance_change_bytes( - &mut balance_changes, - address_key_bytes(db, address), - loan_coin.clone(), - -*tip, - ); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - loan_coin.clone(), - *tip, - ); - - pending_effects.append_contract_payment(decode(contract_hash)?, *payback_amount as u64); + add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); + add_balance_change_bytes( + &mut balance_changes, + address_key_bytes(db, address), + loan_coin.clone(), + -*payback_amount, + ); + add_balance_change_bytes( + &mut balance_changes, + lender.clone(), + loan_coin.clone(), + *payback_amount, + ); + add_balance_change_bytes( + &mut balance_changes, + address_key_bytes(db, address), + loan_coin.clone(), + -*tip, + ); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + loan_coin.clone(), + *tip, + ); + + pending_effects + .append_contract_payment(decode(contract_hash)?, *payback_amount as u64); let loan_coin_name = binary_to_string(loan_coin.clone()); let nft_tree = db.open_tree("nfts")?; if nft_tree.contains_key(loan_coin_name.as_bytes())? { @@ -780,38 +805,38 @@ pub async fn apply_selected_transaction_math( format!("{block_number}:{tx_index}").into_bytes(), ); } - SelectedMempoolTransaction::Collateral { - fee, - address, - contract_hash, - hash, - .. + SelectedMempoolTransaction::Collateral { + fee, + address, + contract_hash, + hash, + .. } => { // Collateral claims release the contract holding to the claimant // and mark the loan as closed. let (collateral, collateral_amount) = - resolve_collateral_details(db, contract_hash).await?; - let collateral_holding = format!("collateral_{contract_hash}"); - add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); - add_balance_change_bytes( - &mut balance_changes, - miner_address.clone(), - base_coin.clone(), - *fee, - ); - add_balance_change_bytes( - &mut balance_changes, - collateral_holding.as_bytes().to_vec(), - collateral.clone(), - -collateral_amount, - ); - add_balance_change_bytes( - &mut balance_changes, - address_key_bytes(db, address), - collateral.clone(), - collateral_amount, - ); - + resolve_collateral_details(db, contract_hash).await?; + let collateral_holding = format!("collateral_{contract_hash}"); + add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); + add_balance_change_bytes( + &mut balance_changes, + collateral_holding.as_bytes().to_vec(), + collateral.clone(), + -collateral_amount, + ); + add_balance_change_bytes( + &mut balance_changes, + address_key_bytes(db, address), + collateral.clone(), + collateral_amount, + ); + pending_effects.set_tree("loan", decode(contract_hash)?, b"false".to_vec()); let collateral_name = binary_to_string(collateral.clone()); let nft_tree = db.open_tree("nfts")?; @@ -828,18 +853,18 @@ pub async fn apply_selected_transaction_math( format!("{block_number}:{tx_index}").into_bytes(), ); } - } - } + } + } for (key, change) in balance_changes { // The balance sheet stores string keys, so convert the internal byte // keys back into wallet/asset strings before applying the delta. let address = if key.address.len() == Wallet::SHORT_ADDRESS_BYTES_LENGTH { - Wallet::bytes_to_short_address(&key.address).unwrap_or_default() - } else { - binary_to_string(key.address) - }; - let coin = binary_to_string(key.coin); + Wallet::bytes_to_short_address(&key.address).unwrap_or_default() + } else { + binary_to_string(key.address) + }; + let coin = binary_to_string(key.coin); let operand = if change >= 0 { BalanceOperand::Addition } else { @@ -850,13 +875,13 @@ pub async fn apply_selected_transaction_math( Ok(()) } - -pub async fn clear_selected_transaction_sql( - db: &Db, - miner: String, - limit: i64, - block_number: u32, - start_index: usize, + +pub async fn clear_selected_transaction_sql( + db: &Db, + miner: String, + limit: i64, + block_number: u32, + start_index: usize, ) -> Result { // Selection and ledger math stay paired so the caller can stream this exact // selected batch into the block file afterward. @@ -874,57 +899,56 @@ pub async fn clear_selected_transaction_sql( pending_effects.apply(db).map_err(|err| anyhow!(err))?; Ok(batch) } - -pub async fn stream_selected_transaction_originals( - file: &mut File, - batch: &SelectedMempoolBatch, -) -> Result<()> { - if batch.originals.is_empty() { - file.flush()?; - return Ok(()); - } + +pub async fn stream_selected_transaction_originals( + file: &mut File, + batch: &SelectedMempoolBatch, +) -> Result<()> { + if batch.originals.is_empty() { + file.flush()?; + return Ok(()); + } // Block payloads store the original mempool bytes back-to-back in selected // order rather than rebuilding each transaction from parsed fields. let total_len: usize = batch.originals.iter().map(Vec::len).sum(); - let mut payload = Vec::with_capacity(total_len); - for original in &batch.originals { - payload.extend_from_slice(original); - } - file.write_all(&payload)?; - file.flush()?; - Ok(()) -} - + let mut payload = Vec::with_capacity(total_len); + for original in &batch.originals { + payload.extend_from_slice(original); + } + file.write_all(&payload)?; + file.flush()?; + Ok(()) +} + pub async fn delete_selected_transactions(batch: &SelectedMempoolBatch) -> Result<()> { let client = DB.get().expect("DB not initialized"); // Each transaction kind still lives in a separate SQL table, so deletion // groups the selected IDs by table after the block has been written. let transfer_ids = ids_for_table(batch, "transfer"); - let token_ids = ids_for_table(batch, "token"); - let issue_token_ids = ids_for_table(batch, "issue_token"); - let burn_ids = ids_for_table(batch, "burn"); - let nft_ids = ids_for_table(batch, "nft"); - let marketing_ids = ids_for_table(batch, "marketing"); - let vanity_ids = ids_for_table(batch, "vanity_address"); - let swap_ids = ids_for_table(batch, "swap"); - let lender_ids = ids_for_table(batch, "loan_contract"); - let borrower_ids = ids_for_table(batch, "loan_payment"); - let collateral_ids = ids_for_table(batch, "collateral_claim"); - - delete_rows(client, "transfer", &transfer_ids).await?; - delete_rows(client, "token", &token_ids).await?; - delete_rows(client, "issue_token", &issue_token_ids).await?; - delete_rows(client, "burn", &burn_ids).await?; - delete_rows(client, "nft", &nft_ids).await?; - delete_rows(client, "marketing", &marketing_ids).await?; - delete_rows(client, "vanity_address", &vanity_ids).await?; - delete_rows(client, "swap", &swap_ids).await?; - delete_rows(client, "loan_contract", &lender_ids).await?; - delete_rows(client, "loan_payment", &borrower_ids).await?; - delete_rows(client, "collateral_claim", &collateral_ids).await?; - - Ok(()) -} - + let token_ids = ids_for_table(batch, "token"); + let issue_token_ids = ids_for_table(batch, "issue_token"); + let burn_ids = ids_for_table(batch, "burn"); + let nft_ids = ids_for_table(batch, "nft"); + let marketing_ids = ids_for_table(batch, "marketing"); + let vanity_ids = ids_for_table(batch, "vanity_address"); + let swap_ids = ids_for_table(batch, "swap"); + let lender_ids = ids_for_table(batch, "loan_contract"); + let borrower_ids = ids_for_table(batch, "loan_payment"); + let collateral_ids = ids_for_table(batch, "collateral_claim"); + + delete_rows(client, "transfer", &transfer_ids).await?; + delete_rows(client, "token", &token_ids).await?; + delete_rows(client, "issue_token", &issue_token_ids).await?; + delete_rows(client, "burn", &burn_ids).await?; + delete_rows(client, "nft", &nft_ids).await?; + delete_rows(client, "marketing", &marketing_ids).await?; + delete_rows(client, "vanity_address", &vanity_ids).await?; + delete_rows(client, "swap", &swap_ids).await?; + delete_rows(client, "loan_contract", &lender_ids).await?; + delete_rows(client, "loan_payment", &borrower_ids).await?; + delete_rows(client, "collateral_claim", &collateral_ids).await?; + + Ok(()) +} diff --git a/src/records/memory/mod.rs b/src/records/memory/mod.rs index b3e181d..e5813e1 100644 --- a/src/records/memory/mod.rs +++ b/src/records/memory/mod.rs @@ -3,9 +3,9 @@ pub mod averages; pub mod connections; pub mod enums; -pub mod response_channels; pub mod mempool; pub mod network_mapping; +pub mod response_channels; pub mod structs; pub mod torrent_status; pub mod torrentmap; diff --git a/src/records/memory/network_mapping/add.rs b/src/records/memory/network_mapping/add.rs index c62f5ad..7f7e899 100644 --- a/src/records/memory/network_mapping/add.rs +++ b/src/records/memory/network_mapping/add.rs @@ -1,30 +1,30 @@ -use super::*; - -impl NodeInfo { - pub async fn broadcast_node( - map: Arc>, - edit: &SignedNodeEdit, - remote_ip: &str, - edittype: NodeEditType, - connections_key: &str, - ) { - // Re-broadcast signed node-map edits to connected peers while - // skipping the source peer that already sent the update. - let message_type = edittype.message_type(); +use super::*; + +impl NodeInfo { + pub async fn broadcast_node( + map: Arc>, + edit: &SignedNodeEdit, + remote_ip: &str, + edittype: NodeEditType, + connections_key: &str, + ) { + // Re-broadcast signed node-map edits to connected peers while + // skipping the source peer that already sent the update. + let message_type = edittype.message_type(); let ip_bytes = ip_to_binary(&edit.ip); - let address_bytes = match Wallet::short_address_to_bytes(&edit.address) { - Some(bytes) => bytes, - None => { - warn!( - "[network_map] skipping broadcast for invalid short node address {}", - edit.address - ); - return; - } - }; - 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_signature_bytes = decode(&edit.modified_signature).unwrap(); + let address_bytes = match Wallet::short_address_to_bytes(&edit.address) { + Some(bytes) => bytes, + None => { + warn!( + "[network_map] skipping broadcast for invalid short node address {}", + edit.address + ); + return; + } + }; + 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_signature_bytes = decode(&edit.modified_signature).unwrap(); let connections_lock = CONNECTIONS.read().await; let streams: Option>>> = connections_lock .as_ref() @@ -35,12 +35,12 @@ impl NodeInfo { let (hashmap_key, _hashmap_tx, hashmap_rx) = reserve_entry(map.clone()).await; let mut message: Vec = Vec::new(); message.push(message_type); - message.extend_from_slice(&hashmap_key); - message.extend_from_slice(&address_bytes); - message.extend_from_slice(&ip_bytes); - message.extend_from_slice(&modified_by_bytes); - message.extend_from_slice(&modified_timestamp_bytes); - message.extend_from_slice(&modified_signature_bytes); + message.extend_from_slice(&hashmap_key); + message.extend_from_slice(&address_bytes); + message.extend_from_slice(&ip_bytes); + message.extend_from_slice(&modified_by_bytes); + message.extend_from_slice(&modified_timestamp_bytes); + message.extend_from_slice(&modified_signature_bytes); let peer_addr = { let stream = unlocked_stream.lock().await; stream.peer_addr() @@ -65,182 +65,182 @@ impl NodeInfo { } } } - None => { - warn!("No active connections found."); - } - } - } - - pub async fn add_address(params: AddAddressParams) -> RpcResponse { + None => { + warn!("No active connections found."); + } + } + } + + pub async fn add_address(params: AddAddressParams) -> RpcResponse { let AddAddressParams { map, mut edit, mut blocks_mined, - remote_ip, - db, + remote_ip, + db, wallet_key, connections_key, } = params; let current_timestamp = Utc::now().timestamp_millis() as u64; let direct_peer_announcement = !remote_ip.is_empty() && edit.ip == remote_ip; - - if !is_public_network_address(&edit.ip) { - return RpcResponse::Binary(b"Error: Invalid network address".to_vec()); - } - - // Locally initiated edits are re-signed with the local wallet and - // current timestamp so they can be propagated as fresh node events. + + if !is_public_network_address(&edit.ip) { + return RpcResponse::Binary(b"Error: Invalid network address".to_vec()); + } + + // Locally initiated edits are re-signed with the local wallet and + // current timestamp so they can be propagated as fresh node events. if edit.ip == remote_ip { - edit.modified_timestamp = current_timestamp; - let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { - Ok(wallet) => wallet, - Err(err) => { - error!("Wallet decryption failed while adding node address: {err}"); - return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec()); - } - }; - edit.modified_by = wallet.saved.long_address; - edit.modified_signature = - Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key) - .await; - } - - if !remote_ip.is_empty() { - blocks_mined = 0; - } - - let data = format!( - "{}{}{}{}", - edit.address, edit.ip, edit.modified_by, edit.modified_timestamp - ); - let hashed_data = skein_256_hash_data(&data); - - // Every add/delete edit is signed, so the network map accepts - // only node changes backed by a valid wallet signature. - if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by) - .await - { - return RpcResponse::Binary(b"Error: Could not validate signature".to_vec()); - } - - let mut penalize_duplicate_ip = false; - - { - let mut address_map = ADDRESS_MAP.lock().await; - - // Once the chain is mature, adding nodes is restricted to older - // active participants with sufficient mined history. - if get_height(&db) > 10000 { - let signer_key = Wallet::normalize_to_short_address(&edit.modified_by) - .unwrap_or_else(|| edit.modified_by.clone()); - let signer_node = address_map.get(&signer_key); - let valid_added_by = signer_node - .map(|node| { - (current_timestamp - node.added_timestamp) >= 3600 - && node.deleted_by.is_empty() - }) - .unwrap_or(false); - if !valid_added_by { - return RpcResponse::Binary( - b"Error: This address cannot add nodes. It must exist for at least 60 minutes and not be marked for deletion" - .to_vec(), - ); - } - let mined_count = signer_node.map(|node| node.blocks_mined).unwrap_or(0); - if mined_count < 100 { - return RpcResponse::Binary( - b"Error: This address cannot add nodes. It must mined 100 blocks before adding new nodes to the network" - .to_vec(), - ); - } - } - - let added_by_count_in_last_hour = address_map - .values() - .filter(|node| { - node.added_by == edit.modified_by - && (current_timestamp - node.added_timestamp) <= 3600 - }) - .count(); - - if added_by_count_in_last_hour >= 10 { - return RpcResponse::Binary( - b"Error: Cannot add more than 10 nodes in 60 minutes".to_vec(), - ); - } - - // Existing deleted entries can be revived in place when the same - // address/IP pair is re-announced, otherwise the older record is - // discarded and replaced. - if let Some(existing_node) = address_map.get_mut(&edit.address) { - if !existing_node.deleted_by.is_empty() { - if existing_node.ip == edit.ip { - existing_node.deleted_by = "".to_string(); - existing_node.deleted_timestamp = 0_u64; - existing_node.deleted_block = 0_u32; - existing_node.deleted_signature = "".to_string(); - return RpcResponse::Binary(b"Success".to_vec()); - } else { - address_map.remove(&edit.address); - } - } else { - if edit.modified_timestamp < existing_node.added_timestamp { - *existing_node = NodeInfo::new( - edit.ip.clone(), - blocks_mined, - edit.modified_by.clone(), - edit.modified_timestamp, - edit.modified_signature.clone(), - ); - } - return RpcResponse::Binary(b"Success".to_vec()); - } - } - - if let Some(existing_node) = address_map.values_mut().find(|node| node.ip == edit.ip) { - if !existing_node.deleted_by.is_empty() { - address_map.retain(|_, node| node.ip != edit.ip); - } else if edit.ip != GENESIS_IP { - penalize_duplicate_ip = true; - } - } - - if !penalize_duplicate_ip { + edit.modified_timestamp = current_timestamp; + let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { + Ok(wallet) => wallet, + Err(err) => { + error!("Wallet decryption failed while adding node address: {err}"); + return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec()); + } + }; + edit.modified_by = wallet.saved.long_address; + edit.modified_signature = + Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key) + .await; + } + + if !remote_ip.is_empty() { + blocks_mined = 0; + } + + let data = format!( + "{}{}{}{}", + edit.address, edit.ip, edit.modified_by, edit.modified_timestamp + ); + let hashed_data = skein_256_hash_data(&data); + + // Every add/delete edit is signed, so the network map accepts + // only node changes backed by a valid wallet signature. + if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by) + .await + { + return RpcResponse::Binary(b"Error: Could not validate signature".to_vec()); + } + + let mut penalize_duplicate_ip = false; + + { + let mut address_map = ADDRESS_MAP.lock().await; + + // Once the chain is mature, adding nodes is restricted to older + // active participants with sufficient mined history. + if get_height(&db) > 10000 { + let signer_key = Wallet::normalize_to_short_address(&edit.modified_by) + .unwrap_or_else(|| edit.modified_by.clone()); + let signer_node = address_map.get(&signer_key); + let valid_added_by = signer_node + .map(|node| { + (current_timestamp - node.added_timestamp) >= 3600 + && node.deleted_by.is_empty() + }) + .unwrap_or(false); + if !valid_added_by { + return RpcResponse::Binary( + b"Error: This address cannot add nodes. It must exist for at least 60 minutes and not be marked for deletion" + .to_vec(), + ); + } + let mined_count = signer_node.map(|node| node.blocks_mined).unwrap_or(0); + if mined_count < 100 { + return RpcResponse::Binary( + b"Error: This address cannot add nodes. It must mined 100 blocks before adding new nodes to the network" + .to_vec(), + ); + } + } + + let added_by_count_in_last_hour = address_map + .values() + .filter(|node| { + node.added_by == edit.modified_by + && (current_timestamp - node.added_timestamp) <= 3600 + }) + .count(); + + if added_by_count_in_last_hour >= 10 { + return RpcResponse::Binary( + b"Error: Cannot add more than 10 nodes in 60 minutes".to_vec(), + ); + } + + // Existing deleted entries can be revived in place when the same + // address/IP pair is re-announced, otherwise the older record is + // discarded and replaced. + if let Some(existing_node) = address_map.get_mut(&edit.address) { + if !existing_node.deleted_by.is_empty() { + if existing_node.ip == edit.ip { + existing_node.deleted_by = "".to_string(); + existing_node.deleted_timestamp = 0_u64; + existing_node.deleted_block = 0_u32; + existing_node.deleted_signature = "".to_string(); + return RpcResponse::Binary(b"Success".to_vec()); + } else { + address_map.remove(&edit.address); + } + } else { + if edit.modified_timestamp < existing_node.added_timestamp { + *existing_node = NodeInfo::new( + edit.ip.clone(), + blocks_mined, + edit.modified_by.clone(), + edit.modified_timestamp, + edit.modified_signature.clone(), + ); + } + return RpcResponse::Binary(b"Success".to_vec()); + } + } + + if let Some(existing_node) = address_map.values_mut().find(|node| node.ip == edit.ip) { + if !existing_node.deleted_by.is_empty() { + address_map.retain(|_, node| node.ip != edit.ip); + } else if edit.ip != GENESIS_IP { + penalize_duplicate_ip = true; + } + } + + if !penalize_duplicate_ip { // Persist the new node locally. Network-map entries are bare // IP membership records, separate from live socket keys. address_map.insert( edit.address.clone(), NodeInfo::new( - edit.ip.clone(), - blocks_mined, - edit.modified_by.clone(), - edit.modified_timestamp, - edit.modified_signature.clone(), - ), - ); - } - } - - if penalize_duplicate_ip { - let now = Utc::now().timestamp() as u32; - let _ = update_ip_score( + edit.ip.clone(), + blocks_mined, + edit.modified_by.clone(), + edit.modified_timestamp, + edit.modified_signature.clone(), + ), + ); + } + } + + if penalize_duplicate_ip { + let now = Utc::now().timestamp() as u32; + let _ = update_ip_score( &remote_ip, "miner", InfractionType::BadMinerIpUpdate, - now, - &db, - &wallet_key, - ) - .await; - return RpcResponse::Binary(b"Error: Ip Already exists.".to_vec()); - } - - Self::broadcast_node( - map.clone(), - &edit, - &remote_ip, - NodeEditType::Add, - &connections_key, + now, + &db, + &wallet_key, + ) + .await; + return RpcResponse::Binary(b"Error: Ip Already exists.".to_vec()); + } + + Self::broadcast_node( + map.clone(), + &edit, + &remote_ip, + NodeEditType::Add, + &connections_key, ) .await; @@ -259,5 +259,4 @@ impl NodeInfo { RpcResponse::Binary(b"Success".to_vec()) } - -} +} diff --git a/src/records/memory/network_mapping/delete.rs b/src/records/memory/network_mapping/delete.rs index 09c45bf..2de2f60 100644 --- a/src/records/memory/network_mapping/delete.rs +++ b/src/records/memory/network_mapping/delete.rs @@ -1,5 +1,5 @@ -use super::*; - +use super::*; + impl NodeInfo { pub fn ping(params: PingMonitorParams) { tokio::spawn(async move { @@ -37,37 +37,37 @@ impl NodeInfo { } let mut failures = 0; - - // Periodically ping the node and remove it after repeated - // failures, then recursively reattach any inherited children. - loop { - sleep(Duration::from_secs(120)).await; - + + // Periodically ping the node and remove it after repeated + // failures, then recursively reattach any inherited children. + loop { + sleep(Duration::from_secs(120)).await; + { let monitors = PING_MONITORS.lock().await; // Stop this task if another monitor replaced it. if !monitors - .get(&task_addr) - .map(|current| current == &task_signature) - .unwrap_or(false) - { - break; - } - } - + .get(&task_addr) + .map(|current| current == &task_signature) + .unwrap_or(false) + { + break; + } + } + { let map_lock = ADDRESS_MAP.lock().await; if let Some(node) = map_lock.get(&task_addr) { // Stop monitoring stale node data after the map entry // has been replaced by a newer edit. if node.added_by != task_wallet || node.added_signature != task_signature { - break; - } - } else { - break; - } - } - + break; + } + } else { + break; + } + } + if let Some(unlocked_stream) = 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 // reserved reply channel to receive any response. let mut message = vec![command]; - message.extend_from_slice(&ping_key); - RpcResponse::send_raw(&unlocked_stream, Some(&task_connections_key), &message) - .await; - - let response = timeout(Duration::from_secs(10), async { - let mut rx = ping_rx.lock().await; - rx.recv().await - }) - .await; - - match response { - Ok(Some(_buffer)) => { - failures = 0; - } + message.extend_from_slice(&ping_key); + RpcResponse::send_raw(&unlocked_stream, Some(&task_connections_key), &message) + .await; + + let response = timeout(Duration::from_secs(10), async { + let mut rx = ping_rx.lock().await; + rx.recv().await + }) + .await; + + match response { + Ok(Some(_buffer)) => { + failures = 0; + } _ => { failures += 1; warn!("[network_map] ping failure: address={task_addr} ip={task_ip} failures={failures}"); if failures >= 3 { warn!("[network_map] deleting node after ping failures: address={task_addr} ip={task_ip} responsible_by={task_wallet}"); - let _ = Self::delete_address(DeleteAddressParams { - map: map.clone(), - edit: SignedNodeEdit { - address: task_addr.clone(), - ip: task_ip.clone(), - modified_by: task_wallet.clone(), - modified_timestamp: added_timestamp, - modified_signature: task_signature.clone(), - }, - remote_ip: task_remote_ip.clone(), - db: task_db.clone(), - wallet_key: task_wallet_key.clone(), - connections_key: task_connections_key.clone(), - }) - .await; - + let _ = Self::delete_address(DeleteAddressParams { + map: map.clone(), + edit: SignedNodeEdit { + address: task_addr.clone(), + ip: task_ip.clone(), + modified_by: task_wallet.clone(), + modified_timestamp: added_timestamp, + modified_signature: task_signature.clone(), + }, + remote_ip: task_remote_ip.clone(), + db: task_db.clone(), + wallet_key: task_wallet_key.clone(), + connections_key: task_connections_key.clone(), + }) + .await; + break; } } - } + } } else { // The direct socket disappeared before this monitor fired. // Connection cleanup is owned by the connection manager, so @@ -123,151 +123,151 @@ impl NodeInfo { break; } } - - Self::release_ping_monitor(&task_addr, &task_signature).await; - }); - } - - pub async fn delete_address(params: DeleteAddressParams) -> RpcResponse { - let DeleteAddressParams { - map, - mut edit, - remote_ip, - db, - wallet_key, - connections_key, - } = params; - let current_timestamp = Utc::now().timestamp_millis() as u64; - - // Locally initiated deletions are re-signed with fresh metadata - // before they are applied and broadcast. - if remote_ip.is_empty() { - edit.modified_timestamp = current_timestamp; - let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { - Ok(wallet) => wallet, - Err(err) => { - error!("Wallet decryption failed while deleting node address: {err}"); - return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec()); - } - }; - edit.modified_by = wallet.saved.long_address; - edit.modified_signature = - Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key) - .await; - } - - let data = format!( - "{}{}{}{}", - edit.address, edit.ip, edit.modified_by, edit.modified_timestamp - ); - let hashed_data = skein_256_hash_data(&data); - - if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by) - .await - { - return RpcResponse::Binary(b"Error: Could not validate signature".to_vec()); - } - - { - let mut address_map = ADDRESS_MAP.lock().await; - + + Self::release_ping_monitor(&task_addr, &task_signature).await; + }); + } + + pub async fn delete_address(params: DeleteAddressParams) -> RpcResponse { + let DeleteAddressParams { + map, + mut edit, + remote_ip, + db, + wallet_key, + connections_key, + } = params; + let current_timestamp = Utc::now().timestamp_millis() as u64; + + // Locally initiated deletions are re-signed with fresh metadata + // before they are applied and broadcast. + if remote_ip.is_empty() { + edit.modified_timestamp = current_timestamp; + let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { + Ok(wallet) => wallet, + Err(err) => { + error!("Wallet decryption failed while deleting node address: {err}"); + return RpcResponse::Binary(b"Error: Wallet decryption failed".to_vec()); + } + }; + edit.modified_by = wallet.saved.long_address; + edit.modified_signature = + Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet_key) + .await; + } + + let data = format!( + "{}{}{}{}", + edit.address, edit.ip, edit.modified_by, edit.modified_timestamp + ); + let hashed_data = skein_256_hash_data(&data); + + if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by) + .await + { + return RpcResponse::Binary(b"Error: Could not validate signature".to_vec()); + } + + { + let mut address_map = ADDRESS_MAP.lock().await; + if get_height(&db) > 10_000 { // Mature chains only allow established miners to remove nodes. let signer_key = Wallet::normalize_to_short_address(&edit.modified_by) - .unwrap_or_else(|| edit.modified_by.clone()); - let signer_node = address_map.get(&signer_key); - let valid_added_by = signer_node - .map(|node| { - (current_timestamp - node.added_timestamp) >= 3600 - && node.deleted_by.is_empty() - }) - .unwrap_or(false); - - if !valid_added_by { - return RpcResponse::Binary( - 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); - if mined_count < 100 { - return RpcResponse::Binary( - b"Error: Must mine 100 blocks to remove nodes".to_vec(), - ); - } - } - + .unwrap_or_else(|| edit.modified_by.clone()); + let signer_node = address_map.get(&signer_key); + let valid_added_by = signer_node + .map(|node| { + (current_timestamp - node.added_timestamp) >= 3600 + && node.deleted_by.is_empty() + }) + .unwrap_or(false); + + if !valid_added_by { + return RpcResponse::Binary( + 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); + if mined_count < 100 { + return RpcResponse::Binary( + b"Error: Must mine 100 blocks to remove nodes".to_vec(), + ); + } + } + let deleted_count_last_hour = address_map .values() .filter(|node| { - node.deleted_by == edit.modified_by - && (current_timestamp - node.deleted_timestamp) <= 3600 + node.deleted_by == edit.modified_by + && (current_timestamp - node.deleted_timestamp) <= 3600 }) .count(); // Rate limit delete events per signer to prevent churn in the // shared network map. 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 !existing_node.deleted_by.is_empty() { - return RpcResponse::Binary( - b"Error: This address has already been deleted".to_vec(), - ); - } + return RpcResponse::Binary( + b"Error: This address has already been deleted".to_vec(), + ); + } // Deletion is recorded as metadata rather than immediately // removing the node, preserving historical validation context. existing_node.deleted_by = edit.modified_by.clone(); - existing_node.deleted_timestamp = current_timestamp; - existing_node.deleted_block = get_height(&db) + 1; - existing_node.deleted_signature = edit.modified_signature.clone(); - info!( - "[network_map] node marked deleted: address={} ip={} deleted_by={} timestamp={} deleted_block={}", - edit.address, - edit.ip, - edit.modified_by, - current_timestamp, - existing_node.deleted_block - ); - } else { - return RpcResponse::Binary(b"Error: Address not found".to_vec()); - } + existing_node.deleted_timestamp = current_timestamp; + existing_node.deleted_block = get_height(&db) + 1; + existing_node.deleted_signature = edit.modified_signature.clone(); + info!( + "[network_map] node marked deleted: address={} ip={} deleted_by={} timestamp={} deleted_block={}", + edit.address, + edit.ip, + edit.modified_by, + current_timestamp, + existing_node.deleted_block + ); + } else { + return RpcResponse::Binary(b"Error: Address not found".to_vec()); + } } // Stop any ping task owned by the deleted node record. Self::release_ping_monitor(&edit.address, &edit.modified_signature).await; - - // Deletions propagate to peers and also tear down any live - // outgoing connection so bootstrap can recover a replacement. - Self::broadcast_node( - map.clone(), - &edit, - &remote_ip, - NodeEditType::Delete, - &connections_key, - ) - .await; - + + // Deletions propagate to peers and also tear down any live + // outgoing connection so bootstrap can recover a replacement. + Self::broadcast_node( + map.clone(), + &edit, + &remote_ip, + NodeEditType::Delete, + &connections_key, + ) + .await; + if let Some(port) = CONNECTIONS - .read() - .await - .as_ref() - .and_then(|conn| conn.find_outgoing_port(&edit.ip)) + .read() + .await + .as_ref() + .and_then(|conn| conn.find_outgoing_port(&edit.ip)) { let mut writer = CONNECTIONS.write().await; if let Some(conn) = writer.as_mut() { // Drop the live outgoing socket after marking the node deleted. conn.drop_connection(ConnectionType::Outgoing, edit.ip.clone(), port); - info!( - "[connection_manager] dropped dead outgoing connection: {}:{}", - edit.ip, port - ); - } - drop(writer); - + info!( + "[connection_manager] dropped dead outgoing connection: {}:{}", + edit.ip, port + ); + } + drop(writer); + let live_connection = { let guard = CONNECTIONS.read().await; guard.as_ref().and_then(|conn| { @@ -287,15 +287,14 @@ impl NodeInfo { wallet_key: wallet_key.clone(), db: db.clone(), map: map.clone(), - first: false, - }; - spawn_reconnect_bootstrap(bootstrap_params); - } else { - warn!("[reconnect] No live stream found to bootstrap from"); - } - } - - RpcResponse::Binary(b"Success: Node marked as deleted".to_vec()) - } - -} + first: false, + }; + spawn_reconnect_bootstrap(bootstrap_params); + } else { + warn!("[reconnect] No live stream found to bootstrap from"); + } + } + + RpcResponse::Binary(b"Success: Node marked as deleted".to_vec()) + } +} diff --git a/src/records/memory/network_mapping/mined_counts.rs b/src/records/memory/network_mapping/mined_counts.rs index c545824..5702c65 100644 --- a/src/records/memory/network_mapping/mined_counts.rs +++ b/src/records/memory/network_mapping/mined_counts.rs @@ -1,50 +1,50 @@ -use super::*; - -impl NodeInfo { +use super::*; + +impl NodeInfo { pub async fn increment_mined(address: &str) { let mut map = ADDRESS_MAP.lock().await; if let Some(node_info) = map.get_mut(address) { // Counts are capped at u8-safe policy maximum used by node rules. if node_info.blocks_mined < 250 { - node_info.blocks_mined += 1; - } - } - } - + node_info.blocks_mined += 1; + } + } + } + pub async fn decrement_mined(address: &str) { let mut map = ADDRESS_MAP.lock().await; if let Some(node_info) = map.get_mut(address) { // Rollback can undo mined credit, but never below zero. if node_info.blocks_mined > 0 { - node_info.blocks_mined -= 1; - } - } - } - - pub async fn get_mined_count(address: &str) -> u8 { - let map = ADDRESS_MAP.lock().await; - if let Some(node_info) = map.get(address) { - node_info.blocks_mined - } else { - 0 - } - } - + node_info.blocks_mined -= 1; + } + } + } + + pub async fn get_mined_count(address: &str) -> u8 { + let map = ADDRESS_MAP.lock().await; + if let Some(node_info) = map.get(address) { + node_info.blocks_mined + } else { + 0 + } + } + pub async fn set_deleted_block_from_mapping(address: &str, deleted_block: u32) { let mut map = ADDRESS_MAP.lock().await; if let Some(node_info) = map.get_mut(address) { // The deletion height is filled in once the chain knows the block // where the delete action becomes active. node_info.deleted_block = deleted_block; - } - } - - pub async fn rebuild_mined_counts_from_chain(db: &Db) -> Result<(), String> { - // Recompute node mined counts directly from saved block headers - // so startup and recovery can rebuild memory-only state. - let current_height = get_height(db); - let mut mined_counts: HashMap = HashMap::new(); - + } + } + + pub async fn rebuild_mined_counts_from_chain(db: &Db) -> Result<(), String> { + // Recompute node mined counts directly from saved block headers + // so startup and recovery can rebuild memory-only state. + let current_height = get_height(db); + let mut mined_counts: HashMap = HashMap::new(); + let start_height = if current_height > 0 { 1 } else { 0 }; for block_number in start_height..=current_height { let header = load_block_header(block_number).await?; @@ -52,26 +52,25 @@ impl NodeInfo { let entry = mined_counts.entry(miner).or_insert(0); // Keep the rebuilt value under the same cap as live increments. if *entry < 250 { - *entry += 1; - } - } - - { - let mut map = ADDRESS_MAP.lock().await; - + *entry += 1; + } + } + + { + let mut map = ADDRESS_MAP.lock().await; + for node_info in map.values_mut() { // Clear memory-only counts before applying the rebuilt chain // totals so removed or inactive miners do not keep stale data. node_info.blocks_mined = 0; } - - for (address, mined_count) in mined_counts { - if let Some(node_info) = map.get_mut(&address) { - node_info.blocks_mined = mined_count; - } - } - } - Ok(()) - } - -} + + for (address, mined_count) in mined_counts { + if let Some(node_info) = map.get_mut(&address) { + node_info.blocks_mined = mined_count; + } + } + } + Ok(()) + } +} diff --git a/src/records/memory/network_mapping/mod.rs b/src/records/memory/network_mapping/mod.rs index d905ead..4ac28e9 100644 --- a/src/records/memory/network_mapping/mod.rs +++ b/src/records/memory/network_mapping/mod.rs @@ -1,83 +1,83 @@ use crate::common::binary_conversions::ip_to_binary; -use crate::common::network_startup::is_public_network_address; -use crate::common::skein::skein_256_hash_data; -use crate::common::types::GENESIS_IP; -use crate::decode; -use crate::lazy_static; -use crate::log::{error, info, warn}; -use crate::records::block_height::get_block_height::get_height; -use crate::records::ip_score::enums::InfractionType; -use crate::records::ip_score::score::update_ip_score; +use crate::common::network_startup::is_public_network_address; +use crate::common::skein::skein_256_hash_data; +use crate::common::types::GENESIS_IP; +use crate::decode; +use crate::lazy_static; +use crate::log::{error, info, warn}; +use crate::records::block_height::get_block_height::get_height; +use crate::records::ip_score::enums::InfractionType; +use crate::records::ip_score::score::update_ip_score; use crate::records::memory::connections::{spawn_reconnect_bootstrap, CONNECTIONS}; 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::structs::{ AddAddressParams, DeleteAddressParams, PingMonitorParams, SignedNodeEdit, NODE_RECORD_BYTES, }; -use crate::records::memory::structs::Connection; -use crate::records::unpack_block::unpack_header::load_block_header; -use crate::rpc::client::handshake_processing::BootstrapParams; -use crate::rpc::command_maps::RPC_BLOCK_HEIGHT; -use crate::rpc::responses::RpcResponse; -use crate::sled::Db; -use crate::sleep; -use crate::timeout; -use crate::wallets::structures::Wallet; -use crate::Arc; -use crate::Duration; -use crate::HashMap; -use crate::Mutex; -use crate::TcpStream; -use crate::Utc; - -lazy_static! { - static ref ADDRESS_MAP: Mutex> = Mutex::new(HashMap::new()); - static ref PING_MONITORS: Mutex> = Mutex::new(HashMap::new()); -} - -#[derive(Debug)] -pub struct NodeInfo { - ip: String, - blocks_mined: u8, - added_by: String, - added_timestamp: u64, - added_signature: String, - deleted_by: String, - deleted_timestamp: u64, - deleted_block: u32, - deleted_signature: String, -} - -impl NodeInfo { - async fn release_ping_monitor(address: &str, signature: &str) { - let mut monitors = PING_MONITORS.lock().await; - if monitors - .get(address) - .map(|current| current == signature) - .unwrap_or(false) - { - monitors.remove(address); - } - } - - fn new( - ip: String, - blocks_mined: u8, - added_by: String, - added_timestamp: u64, - added_signature: String, - ) -> Self { - NodeInfo { - ip, - blocks_mined, - added_by, - added_timestamp, - added_signature, - deleted_by: "".to_string(), - deleted_timestamp: 0_u64, - deleted_block: 0_u32, - deleted_signature: "".to_string(), +use crate::records::memory::response_channels::{reserve_entry, Command}; +use crate::records::memory::structs::Connection; +use crate::records::unpack_block::unpack_header::load_block_header; +use crate::rpc::client::handshake_processing::BootstrapParams; +use crate::rpc::command_maps::RPC_BLOCK_HEIGHT; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::sleep; +use crate::timeout; +use crate::wallets::structures::Wallet; +use crate::Arc; +use crate::Duration; +use crate::HashMap; +use crate::Mutex; +use crate::TcpStream; +use crate::Utc; + +lazy_static! { + static ref ADDRESS_MAP: Mutex> = Mutex::new(HashMap::new()); + static ref PING_MONITORS: Mutex> = Mutex::new(HashMap::new()); +} + +#[derive(Debug)] +pub struct NodeInfo { + ip: String, + blocks_mined: u8, + added_by: String, + added_timestamp: u64, + added_signature: String, + deleted_by: String, + deleted_timestamp: u64, + deleted_block: u32, + deleted_signature: String, +} + +impl NodeInfo { + async fn release_ping_monitor(address: &str, signature: &str) { + let mut monitors = PING_MONITORS.lock().await; + if monitors + .get(address) + .map(|current| current == signature) + .unwrap_or(false) + { + monitors.remove(address); + } + } + + fn new( + ip: String, + blocks_mined: u8, + added_by: String, + added_timestamp: u64, + added_signature: String, + ) -> Self { + NodeInfo { + ip, + blocks_mined, + added_by, + added_timestamp, + added_signature, + deleted_by: "".to_string(), + deleted_timestamp: 0_u64, + deleted_block: 0_u32, + deleted_signature: "".to_string(), } } } @@ -88,4 +88,3 @@ pub mod enums; mod mined_counts; mod queries; pub mod structs; - diff --git a/src/records/memory/network_mapping/queries.rs b/src/records/memory/network_mapping/queries.rs index 0107987..a159028 100644 --- a/src/records/memory/network_mapping/queries.rs +++ b/src/records/memory/network_mapping/queries.rs @@ -11,24 +11,23 @@ impl NodeInfo { false } - pub async fn find_address_by_ip(ip: &str) -> Option { let map = ADDRESS_MAP.lock().await; for (address, node_info) in map.iter() { // Reverse lookup is needed when a peer is identified by socket IP // but the node map is keyed by wallet short address. if node_info.ip == ip { - return Some(address.clone()); - } - } - None - } - - pub async fn find_ip_by_address(address: &str) -> Option { - let map = ADDRESS_MAP.lock().await; - map.get(address).map(|node_info| node_info.ip.clone()) - } - + return Some(address.clone()); + } + } + None + } + + pub async fn find_ip_by_address(address: &str) -> Option { + let map = ADDRESS_MAP.lock().await; + map.get(address).map(|node_info| node_info.ip.clone()) + } + pub async fn active_node_ips() -> Vec { let map = ADDRESS_MAP.lock().await; map.values() @@ -37,8 +36,8 @@ impl NodeInfo { .filter(|node_info| node_info.deleted_by.is_empty()) .map(|node_info| node_info.ip.clone()) .collect() - } - + } + pub async fn get_deleted_addresses() -> Vec { let map = ADDRESS_MAP.lock().await; map.iter() @@ -46,25 +45,25 @@ impl NodeInfo { // The RPC response is a packed list of deleted short-address // bytes, so invalid address keys are skipped. if node_info.deleted_timestamp > 0 { - Wallet::short_address_to_bytes(address) - } else { - None - } - }) - .flatten() - .collect() - } - - pub async fn request_valid_nodes() -> RpcResponse { - // Serialize the in-memory node map into the fixed binary layout - // used by peer bootstrap and node-list synchronization. - let map = ADDRESS_MAP.lock().await; - let mut data: Vec = Vec::with_capacity(map.len() * NODE_RECORD_BYTES); - + Wallet::short_address_to_bytes(address) + } else { + None + } + }) + .flatten() + .collect() + } + + pub async fn request_valid_nodes() -> RpcResponse { + // Serialize the in-memory node map into the fixed binary layout + // used by peer bootstrap and node-list synchronization. + let map = ADDRESS_MAP.lock().await; + let mut data: Vec = Vec::with_capacity(map.len() * NODE_RECORD_BYTES); + for (address, node_info) in map.iter() { let address_bytes = match Wallet::short_address_to_bytes(address) { - Some(bytes) => bytes, - None => continue, + Some(bytes) => bytes, + None => continue, }; let ip_bytes = ip_to_binary(&node_info.ip); let blocks_mined = node_info.blocks_mined; @@ -74,58 +73,58 @@ impl NodeInfo { // Empty deletion fields serialize as zero-filled fixed-width values // so every node record stays the same size on the wire. let deleted_by_bytes = if node_info.deleted_by.is_empty() { - vec![0u8; Wallet::ADDRESS_BYTES_LENGTH] - } else { - Wallet::long_address_to_bytes(node_info.deleted_by.to_string()) - }; + vec![0u8; Wallet::ADDRESS_BYTES_LENGTH] + } else { + Wallet::long_address_to_bytes(node_info.deleted_by.to_string()) + }; let deleted_timestamp_bytes = node_info.deleted_timestamp.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() { - vec![0u8; Wallet::SIGNATURE_LENGTH] - } else { - decode(node_info.deleted_signature.clone()).unwrap() - }; - + vec![0u8; Wallet::SIGNATURE_LENGTH] + } else { + decode(node_info.deleted_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 // synchronization. data.extend_from_slice(&address_bytes); - data.extend_from_slice(&ip_bytes); - data.push(blocks_mined); - data.extend_from_slice(&added_by_bytes); - data.extend_from_slice(&added_timestamp_bytes); - data.extend_from_slice(&added_signature_bytes); - data.extend_from_slice(&deleted_by_bytes); - data.extend_from_slice(&deleted_timestamp_bytes); - data.extend_from_slice(&deleted_block_bytes); - data.extend_from_slice(&deleted_signature_bytes); - } - RpcResponse::Binary(data) - } - - pub async fn added_signature( - address: &str, - ip: &str, - current_timestamp: u64, - wallet_key: &str, - ) -> String { - // Node edits are signed over address, IP, signer, and timestamp - // so peers can independently verify the advertised change. - let wallet = match Wallet::try_obtain_wallet(wallet_key.to_string(), None).await { - Ok(wallet) => wallet, - Err(err) => { - error!("Wallet decryption failed while signing node edit: {err}"); - return String::new(); - } - }; - let added_by = wallet.saved.long_address; - let private_key = wallet.saved.private_key; - - let data = format!("{address}{ip}{added_by}{current_timestamp}"); - let hashed_data = skein_256_hash_data(&data); - Wallet::sign_transaction(&hashed_data, &private_key).await - } -} + data.extend_from_slice(&ip_bytes); + data.push(blocks_mined); + data.extend_from_slice(&added_by_bytes); + data.extend_from_slice(&added_timestamp_bytes); + data.extend_from_slice(&added_signature_bytes); + data.extend_from_slice(&deleted_by_bytes); + data.extend_from_slice(&deleted_timestamp_bytes); + data.extend_from_slice(&deleted_block_bytes); + data.extend_from_slice(&deleted_signature_bytes); + } + RpcResponse::Binary(data) + } + + pub async fn added_signature( + address: &str, + ip: &str, + current_timestamp: u64, + wallet_key: &str, + ) -> String { + // Node edits are signed over address, IP, signer, and timestamp + // so peers can independently verify the advertised change. + let wallet = match Wallet::try_obtain_wallet(wallet_key.to_string(), None).await { + Ok(wallet) => wallet, + Err(err) => { + error!("Wallet decryption failed while signing node edit: {err}"); + return String::new(); + } + }; + let added_by = wallet.saved.long_address; + let private_key = wallet.saved.private_key; + + let data = format!("{address}{ip}{added_by}{current_timestamp}"); + let hashed_data = skein_256_hash_data(&data); + Wallet::sign_transaction(&hashed_data, &private_key).await + } +} diff --git a/src/records/memory/response_channels.rs b/src/records/memory/response_channels.rs index edbb956..1331489 100644 --- a/src/records/memory/response_channels.rs +++ b/src/records/memory/response_channels.rs @@ -21,7 +21,7 @@ fn random_3_byte_number() -> [u8; 3] { let num: u32 = rng.gen_range(0..=0xFFFFFF); // The protocol UID is three bytes on the wire, so the random u32 is sliced // down to the same fixed-width little-endian layout used by requests. - + num.to_le_bytes()[1..4].try_into().unwrap() } diff --git a/src/records/record_chain/lender_tx.rs b/src/records/record_chain/lender_tx.rs index 083a9ae..dd3b047 100644 --- a/src/records/record_chain/lender_tx.rs +++ b/src/records/record_chain/lender_tx.rs @@ -80,13 +80,21 @@ pub async fn process_lender( pending_effects.append_tree_if_key_exists( "nfts", "nft_history", - transaction.unsigned_loan_contract.collateral.as_bytes().to_vec(), + transaction + .unsigned_loan_contract + .collateral + .as_bytes() + .to_vec(), txhash_bytes.clone(), ); pending_effects.append_tree_if_key_exists( "nfts", "nft_history", - transaction.unsigned_loan_contract.loan_coin.as_bytes().to_vec(), + transaction + .unsigned_loan_contract + .loan_coin + .as_bytes() + .to_vec(), txhash_bytes, ); Ok(binary_data) diff --git a/src/records/record_chain/nft_tx.rs b/src/records/record_chain/nft_tx.rs index 994e0e0..ccb3d0f 100644 --- a/src/records/record_chain/nft_tx.rs +++ b/src/records/record_chain/nft_tx.rs @@ -60,8 +60,16 @@ pub async fn process_nft( BalanceOperand::Addition, ); 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.append_tree("nft_history", nft_save_name.into_bytes(), txhash_bytes.clone()); + pending_effects.set_tree( + "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 { let nft_save_name = transaction.unsigned_create_nft.nft_name.clone(); @@ -72,8 +80,16 @@ pub async fn process_nft( BalanceOperand::Addition, ); 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.append_tree("nft_history", nft_save_name.into_bytes(), txhash_bytes.clone()); + pending_effects.set_tree( + "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 diff --git a/src/records/record_chain/pending_effects.rs b/src/records/record_chain/pending_effects.rs index ecb677a..714f21d 100644 --- a/src/records/record_chain/pending_effects.rs +++ b/src/records/record_chain/pending_effects.rs @@ -373,12 +373,8 @@ fn apply_effect(db: &Db, effect: &PendingEffect) -> Result { - let previous = append_tree_value( - db, - "contract_payments", - contract_id, - &payment.to_le_bytes(), - )?; + let previous = + append_tree_value(db, "contract_payments", contract_id, &payment.to_le_bytes())?; Ok(AppliedEffect::TreeMutation { tree: "contract_payments", key: contract_id.clone(), @@ -400,8 +396,10 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> { amount, coin, 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 { tree, key, @@ -428,7 +426,12 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> { previous_rollback, } => { 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(()), } @@ -581,8 +584,8 @@ fn apply_vanity_effect( ) -> Result { let previous_vanity = get_registered_vanity_for_owner(db, owner_address) .map_err(|err| format!("Could not read existing vanity mapping: {err}"))?; - let rollback_key = - crate::decode(txhash).map_err(|_| "Could not decode vanity transaction hash".to_string())?; + let rollback_key = crate::decode(txhash) + .map_err(|_| "Could not decode vanity transaction hash".to_string())?; let rollback_tree = db .open_tree(WALLET_VANITY_ROLLBACK_TREE) .map_err(|err| format!("Could not open vanity rollback tree: {err}"))?; diff --git a/src/records/record_chain/save.rs b/src/records/record_chain/save.rs index b94c9dd..7329dfe 100644 --- a/src/records/record_chain/save.rs +++ b/src/records/record_chain/save.rs @@ -1,6 +1,11 @@ use crate::common::check_genesis::genesis_checkup; 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::records::block_height::get_block_height::get_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::{ apply_selected_transaction_math, mark_processed_by_signatures, mark_selected_transactions_processed, restore_processed_by_signatures, - restore_selected_transactions_processed, select_transactions_for_block, spawn_processed_cleanup, - stream_selected_transaction_originals, + restore_selected_transactions_processed, select_transactions_for_block, + spawn_processed_cleanup, stream_selected_transaction_originals, }; use crate::records::memory::network_mapping::NodeInfo; 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::torrenting_system::save_torrent::prune_staged_torrents; use crate::torrent::torrenting_system::torrent_cache::prune_recent_torrents; -use crate::log::{error, info}; use crate::Arc; -use crate::decode; -use crate::fs; use crate::Mutex; use crate::PathBuf; use crate::Utc; @@ -337,7 +339,9 @@ async fn save_binary_data_with_mempool_stream( let _ = update_snapshot(db, next_number).await; if let Some(snapshot_height) = snapshot_height(db).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_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; if let Some(snapshot_height) = snapshot_height(db).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_torrent_statuses_through_height(snapshot_height).await; diff --git a/src/records/record_chain/token_tx.rs b/src/records/record_chain/token_tx.rs index c165ef2..722c227 100644 --- a/src/records/record_chain/token_tx.rs +++ b/src/records/record_chain/token_tx.rs @@ -23,8 +23,7 @@ pub async fn process_token( // Serialize the token-creation transaction and compute its txid // before applying the balance-sheet and token-registry updates. let txhash = transaction.unsigned_create_token.hash().await; - let txhash_bytes = - decode(&txhash).map_err(|e| format!("Error decoding token txhash: {e}"))?; + let txhash_bytes = decode(&txhash).map_err(|e| format!("Error decoding token txhash: {e}"))?; let transaction_bytes = match transaction.to_bytes().await { Ok(bytes) => bytes, 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_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( "token_history", transaction.unsigned_create_token.ticker.as_bytes().to_vec(), diff --git a/src/records/wallet_registry/mod.rs b/src/records/wallet_registry/mod.rs index f2b61ba..1a33dc1 100644 --- a/src/records/wallet_registry/mod.rs +++ b/src/records/wallet_registry/mod.rs @@ -1,26 +1,27 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::decode; -use crate::sled::Db; -use crate::wallets::structures::Wallet; - -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_OWNER_TREE: &str = "wallet_vanity_owner"; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::decode; +use crate::sled::Db; +use crate::wallets::structures::Wallet; + +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_OWNER_TREE: &str = "wallet_vanity_owner"; pub(crate) const WALLET_VANITY_ROLLBACK_TREE: &str = "wallet_vanity_rollback"; - -mod helpers; -mod mappings; -mod storage; -pub mod structs; - -pub use helpers::{get_registered_pubkey, is_registered_short_address, short_address_exists}; -pub use mappings::{ - get_registered_vanity_for_owner, list_registered_wallets, require_canonical_registered_short_address, - resolve_canonical_registered_short_address, 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, - store_previous_vanity_for_txid, -}; -pub use structs::{VanityRegistrationResult, WalletRegistrationResult}; + +mod helpers; +mod mappings; +mod storage; +pub mod structs; + +pub use helpers::{get_registered_pubkey, is_registered_short_address, short_address_exists}; +pub use mappings::{ + get_registered_vanity_for_owner, list_registered_wallets, + require_canonical_registered_short_address, resolve_canonical_registered_short_address, + 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, + store_previous_vanity_for_txid, +}; +pub use structs::{VanityRegistrationResult, WalletRegistrationResult}; diff --git a/src/rpc/client/handshake.rs b/src/rpc/client/handshake.rs index 6ea82e2..65fe1cb 100644 --- a/src/rpc/client/handshake.rs +++ b/src/rpc/client/handshake.rs @@ -1,15 +1,15 @@ 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_processing::process_handshake_response; use crate::rpc::client::structs::{Connect, Handshake}; use crate::rpc::command_maps::{MAX_RPC_REPLY_BYTES, RPC_REPLY}; use crate::rpc::handshake_constants::HANDSHAKE_RESPONSE_BYTES; use crate::wallets::structures::Wallet; -use crate::{AsyncReadExt, AsyncWriteExt}; -use crate::io; use crate::IpAddr; use crate::SocketAddr; use crate::TcpStream; +use crate::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpSocket; pub async fn connect_and_handshake(params: Connect) -> Result<(), Box> { diff --git a/src/rpc/client/handshake_message.rs b/src/rpc/client/handshake_message.rs index f48ca69..7927d3c 100644 --- a/src/rpc/client/handshake_message.rs +++ b/src/rpc/client/handshake_message.rs @@ -1,12 +1,12 @@ use crate::common::binary_conversions::ip_port_to_binary; use crate::common::network_startup::get_ip_and_port; use crate::common::skein::skein_256_hash_data; +use crate::decode; +use crate::io; use crate::rpc::commands::time::request_time; use crate::rpc::handshake_constants::HANDSHAKE_REQUEST_BYTES; use crate::rpc::responses::RpcResponse; use crate::wallets::structures::Wallet; -use crate::decode; -use crate::io; pub async fn prepare_handshake_message(wallet: &Wallet, message: &str) -> io::Result> { // Client handshakes are assembled from the signed message, wallet identity, timestamp, diff --git a/src/rpc/client/handshake_processing.rs b/src/rpc/client/handshake_processing.rs index 363c3ce..3676236 100644 --- a/src/rpc/client/handshake_processing.rs +++ b/src/rpc/client/handshake_processing.rs @@ -1,40 +1,46 @@ use crate::common::binary_conversions::binary_to_ip_port; 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::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::sync_check::sync_checkup; use crate::orphans::torrent_candidates::hydrate_torrent_candidates; use crate::records::block_height::get_block_height::get_height; use crate::records::memory::connections::{set_reconnect_context, CONNECTIONS}; 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::response_channels::{reserve_entry, Command}; use crate::records::memory::structs::{Connection, StoreConnectionParams}; 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::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::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::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::remote_height::request_remote_height; +use crate::timeout; use crate::wallets::structures::Wallet; -use crate::log::{error, info, warn}; -use crate::sled::Db; use crate::Arc; use crate::Duration; -use crate::encode; -use crate::io; use crate::Mutex; use crate::SocketAddr; use crate::TcpStream; -use crate::timeout; #[derive(Clone)] pub struct BootstrapParams { @@ -104,7 +110,10 @@ pub async fn bootstrap_peer_discovery(mut params: BootstrapParams) -> Result<(), 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; continue; } diff --git a/src/rpc/client/register_wallet.rs b/src/rpc/client/register_wallet.rs index 6479f93..e3d1a62 100644 --- a/src/rpc/client/register_wallet.rs +++ b/src/rpc/client/register_wallet.rs @@ -1,15 +1,15 @@ 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::rpc::command_maps::RPC_REGISTER_WALLET; use crate::rpc::responses::RpcResponse; +use crate::timeout; use crate::wallets::structures::Wallet; -use crate::log::warn; use crate::Arc; -use crate::decode; use crate::Duration; use crate::Mutex; use crate::TcpStream; -use crate::timeout; pub async fn register_connected_wallet( stream: Arc>, diff --git a/src/rpc/client/syncing.rs b/src/rpc/client/syncing.rs index 187cd49..0f6af1d 100644 --- a/src/rpc/client/syncing.rs +++ b/src/rpc/client/syncing.rs @@ -1,19 +1,21 @@ use crate::common::check_genesis::genesis_checkup; +use crate::io; +use crate::log::{error, info, warn}; use crate::orphans::structs::OrphanCheckup2; use crate::orphans::sync_check::sync_checkup; use crate::records::block_height::get_block_height::get_height; use crate::records::memory::response_channels::reserve_entry; use crate::records::memory::response_channels::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::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::Duration; -use crate::io; use crate::Mutex; use crate::TcpStream; -use crate::timeout; pub async fn node_syncing( stream: Arc>, diff --git a/src/rpc/client/wallet_registry_sync.rs b/src/rpc/client/wallet_registry_sync.rs index 3d62316..cbfee32 100644 --- a/src/rpc/client/wallet_registry_sync.rs +++ b/src/rpc/client/wallet_registry_sync.rs @@ -1,16 +1,16 @@ +use crate::log::warn; use crate::records::memory::response_channels::{reserve_entry, Command}; 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::commands::wallet_registry_sync::WALLET_REGISTRY_RECORD_BYTES; use crate::rpc::responses::RpcResponse; -use crate::log::warn; use crate::sled::Db; +use crate::timeout; use crate::wallets::structures::Wallet; use crate::Arc; use crate::Duration; use crate::Mutex; use crate::TcpStream; -use crate::timeout; pub async fn sync_wallet_registry( stream: Arc>, diff --git a/src/rpc/commands/add_network_node.rs b/src/rpc/commands/add_network_node.rs index bcd0188..782afbc 100644 --- a/src/rpc/commands/add_network_node.rs +++ b/src/rpc/commands/add_network_node.rs @@ -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::NodeInfo; +use crate::records::memory::response_channels::Command; use crate::rpc::read_bytes_from_stream; use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; use crate::sled::Db; +use crate::wallets::structures::Wallet; use crate::Arc; use crate::Mutex; use crate::TcpStream; @@ -21,9 +21,11 @@ pub async fn add_network_node( let (uid, _) = read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone()) .await?; - let address_bytes = - read_bytes_from_stream::read_short_address_from_stream(connections_key, stream_locked.clone()) - .await?; + let address_bytes = read_bytes_from_stream::read_short_address_from_stream( + connections_key, + stream_locked.clone(), + ) + .await?; let address = Wallet::bytes_to_short_address(&address_bytes) .ok_or_else(|| "error: Invalid short address bytes".to_string())?; let ip = diff --git a/src/rpc/commands/address_coin_lookup.rs b/src/rpc/commands/address_coin_lookup.rs index 4f2ff13..66863bf 100644 --- a/src/rpc/commands/address_coin_lookup.rs +++ b/src/rpc/commands/address_coin_lookup.rs @@ -1,8 +1,8 @@ use crate::records::balance_sheet::get_wallet_balance::get_balance; use crate::records::wallet_registry::resolve_canonical_registered_short_address; use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; use crate::sled::Db; +use crate::wallets::structures::Wallet; pub async fn lookup_wallet_coin(db: &Db, address: String, coin: String) -> RpcResponse { // Return the saved confirmed balance for a specific address/asset pair. diff --git a/src/rpc/commands/address_complete_balance_sheet.rs b/src/rpc/commands/address_complete_balance_sheet.rs index 18f9663..1b39c2d 100644 --- a/src/rpc/commands/address_complete_balance_sheet.rs +++ b/src/rpc/commands/address_complete_balance_sheet.rs @@ -1,14 +1,14 @@ 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::wallet_registry::resolve_canonical_registered_short_address; use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; -use crate::log::error; use crate::sled::Db; +use crate::wallets::structures::Wallet; use crate::AsyncReadExt; use crate::File; use crate::Path; -use crate::read_dir; pub async fn get_token_balances(db: &Db, address: String) -> RpcResponse { // Walk the hierarchical balance-sheet tree for one address and emit diff --git a/src/rpc/commands/block_by_hash.rs b/src/rpc/commands/block_by_hash.rs index d9c9520..68759cf 100644 --- a/src/rpc/commands/block_by_hash.rs +++ b/src/rpc/commands/block_by_hash.rs @@ -9,9 +9,7 @@ pub async fn request_block(db: &Db, hash: &str) -> RpcResponse { let hash_bytes = match decode(hash) { Ok(bytes) => bytes, Err(err) => { - return RpcResponse::Binary( - format!("error: Failed to decode hash: {err}").into_bytes(), - ) + return RpcResponse::Binary(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(), ), }, - Err(err) => { - RpcResponse::Binary(format!("error: Failed to load block: {err}").into_bytes()) - } + Err(err) => RpcResponse::Binary(format!("error: Failed to load block: {err}").into_bytes()), } } diff --git a/src/rpc/commands/block_header_by_hash.rs b/src/rpc/commands/block_header_by_hash.rs index da87fb4..af0ca53 100644 --- a/src/rpc/commands/block_header_by_hash.rs +++ b/src/rpc/commands/block_header_by_hash.rs @@ -1,6 +1,6 @@ +use crate::decode; use crate::rpc::responses::RpcResponse; use crate::sled::Db; -use crate::decode; pub async fn lookup_by_hash(db: &Db, hash: &str) -> RpcResponse { // Resolve the block hash through the block-hash index and then fetch diff --git a/src/rpc/commands/block_peer_ip.rs b/src/rpc/commands/block_peer_ip.rs index 99660a2..85763f6 100644 --- a/src/rpc/commands/block_peer_ip.rs +++ b/src/rpc/commands/block_peer_ip.rs @@ -1,6 +1,6 @@ use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; use crate::sled::Db; +use crate::wallets::structures::Wallet; 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 diff --git a/src/rpc/commands/contract.rs b/src/rpc/commands/contract.rs index c548171..97469c0 100644 --- a/src/rpc/commands/contract.rs +++ b/src/rpc/commands/contract.rs @@ -1,12 +1,12 @@ use crate::blocks::collateral::CollateralClaimTransaction; use crate::blocks::loan_payment::ContractPaymentTransaction; use crate::blocks::loans::LoanContractTransaction; +use crate::encode; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; use crate::sled::Db; +use crate::wallets::structures::Wallet; use crate::{DateTime, Datelike, Local, TimeZone, Utc}; -use crate::encode; fn format_amount(value: u64) -> f64 { // 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 = None; for entry in tree.iter() { - let (txid_bytes, _) = - entry.map_err(|e| format!("error: Failed to read txid tree: {e}"))?; + let (txid_bytes, _) = entry.map_err(|e| format!("error: Failed to read txid tree: {e}"))?; let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, txid_bytes.to_vec()).await; if bytes.is_empty() { continue; @@ -243,8 +242,7 @@ pub async fn contract_details(hash: Vec, 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 // lender or borrower, each expanded into the same text summary view. - let Some(address) = Wallet::normalize_to_short_address(&address) - else { + let Some(address) = Wallet::normalize_to_short_address(&address) else { return RpcResponse::Binary(b"error: Invalid wallet address".to_vec()); }; diff --git a/src/rpc/commands/delete_network_node.rs b/src/rpc/commands/delete_network_node.rs index 71a0b3d..b937a9d 100644 --- a/src/rpc/commands/delete_network_node.rs +++ b/src/rpc/commands/delete_network_node.rs @@ -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::NodeInfo; +use crate::records::memory::response_channels::Command; use crate::rpc::read_bytes_from_stream; use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; use crate::sled::Db; +use crate::wallets::structures::Wallet; use crate::Arc; use crate::Mutex; use crate::TcpStream; @@ -21,9 +21,11 @@ pub async fn delete_network_node( let (uid, _) = read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone()) .await?; - let address_bytes = - read_bytes_from_stream::read_short_address_from_stream(connections_key, stream_locked.clone()) - .await?; + let address_bytes = read_bytes_from_stream::read_short_address_from_stream( + connections_key, + stream_locked.clone(), + ) + .await?; let address = Wallet::bytes_to_short_address(&address_bytes) .ok_or_else(|| "error: Invalid short address bytes".to_string())?; let ip = diff --git a/src/rpc/commands/mod.rs b/src/rpc/commands/mod.rs index 1545414..8b6a6d2 100644 --- a/src/rpc/commands/mod.rs +++ b/src/rpc/commands/mod.rs @@ -1,7 +1,7 @@ // 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_complete_balance_sheet; -pub mod add_network_node; pub mod bad_rpc_call; pub mod block_by_hash; pub mod block_by_height; @@ -11,10 +11,10 @@ pub mod block_headers; pub mod block_height; pub mod block_peer_ip; pub mod contract; -pub mod difficulty; pub mod delete_network_node; -pub mod latest_block; +pub mod difficulty; pub mod largest_tx_fee; +pub mod latest_block; pub mod memory_by_signature; pub mod network_info; pub mod nft_list; @@ -31,14 +31,14 @@ pub mod torrent; pub mod torrent_by_block; pub mod torrent_candidates; pub mod transaction_by_txid; +pub mod transactions_by_address; pub mod tx_count; pub mod tx_count_from_mempool; pub mod tx_submit; -pub mod transactions_by_address; pub mod unblock_peer_ip; pub mod validate_address; -pub mod validate_torrent; pub mod validate_message; +pub mod validate_torrent; pub mod wallet_register; pub mod wallet_registry_sync; pub mod wallet_vanity_lookup; diff --git a/src/rpc/commands/nft_list.rs b/src/rpc/commands/nft_list.rs index 09eac83..6ee0cb7 100644 --- a/src/rpc/commands/nft_list.rs +++ b/src/rpc/commands/nft_list.rs @@ -1,7 +1,7 @@ use crate::common::nft_assets::nft_asset_parts; +use crate::encode; use crate::rpc::responses::RpcResponse; use crate::sled::Db; -use crate::encode; pub async fn get_nfts(db: &Db) -> RpcResponse { // Serialize every NFT asset as origin hash, padded asset name, and diff --git a/src/rpc/commands/nft_lookup.rs b/src/rpc/commands/nft_lookup.rs index 523d42f..0aa2214 100644 --- a/src/rpc/commands/nft_lookup.rs +++ b/src/rpc/commands/nft_lookup.rs @@ -6,14 +6,16 @@ use crate::blocks::swap::SwapTransaction; use crate::blocks::transfer::TransferTransaction; use crate::common::binary_conversions::binary_to_string; 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::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_TRANSFER: u8 = 2; @@ -102,7 +104,8 @@ async fn find_nft_origin( let txid_tree = db.open_tree("txid").ok()?; for entry in txid_tree.iter() { 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 { 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> { // Expand a raw NFT history txid into a fixed-width history entry that // 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 { return None; @@ -267,7 +271,8 @@ async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Op .await .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) .await .ok()?; @@ -285,7 +290,8 @@ async fn build_history_entry(db: &Db, asset_name: &str, txid_bytes: &[u8]) -> Op .await .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) .await .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()); }; - 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 { return RpcResponse::Binary(b"error: NFT genesis transaction not found".to_vec()); } diff --git a/src/rpc/commands/receive_torrent.rs b/src/rpc/commands/receive_torrent.rs index f4a6298..2cc11ba 100644 --- a/src/rpc/commands/receive_torrent.rs +++ b/src/rpc/commands/receive_torrent.rs @@ -1,5 +1,7 @@ use crate::common::check_genesis::genesis_checkup; 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::orphans::structs::OrphanCheckup2; 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::responses::RpcResponse; 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::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::AtomicBool; use crate::AtomicOrdering; -use crate::lazy_static; use crate::Mutex; lazy_static! { @@ -156,8 +160,7 @@ pub async fn torrent_submission( 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) => { let _ = remember_recent_torrent(&torrent_hash, height).await; if let Some((torrent, staged_path)) = stage_result { @@ -273,7 +276,8 @@ pub async fn receive_torrent( ) -> Result<(u32, RpcResponse), String> { let (uid, _) = 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 // are the torrent metadata that will be parsed and staged. diff --git a/src/rpc/commands/route_reply.rs b/src/rpc/commands/route_reply.rs index 9ccd7cd..f709e5b 100644 --- a/src/rpc/commands/route_reply.rs +++ b/src/rpc/commands/route_reply.rs @@ -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::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::Arc; 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()) .await?; let message_length = - read_bytes_from_stream::read_u32_from_stream(connections_key, stream_locked.clone()) - .await? as usize; + read_bytes_from_stream::read_u32_from_stream(connections_key, stream_locked.clone()).await? + as usize; if message_length > MAX_RPC_REPLY_BYTES { bad_rpc_call::record(ip, client_type, db, wallet_key).await; return Err(format!( @@ -44,9 +46,7 @@ pub async fn route_reply( ) .await?; if tx.send(buffer).await.is_err() { - warn!( - "[rpc] reply receiver dropped before payload delivery: {uid:?}" - ); + warn!("[rpc] reply receiver dropped before payload delivery: {uid:?}"); } delete_entry(map, uid).await; diff --git a/src/rpc/commands/token_lookup.rs b/src/rpc/commands/token_lookup.rs index 12f59ef..e7b745f 100644 --- a/src/rpc/commands/token_lookup.rs +++ b/src/rpc/commands/token_lookup.rs @@ -1,14 +1,14 @@ use crate::blocks::token::CreateTokenTransaction; 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::tokens_to_lower::strip_spaces_and_lowercase; use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; use crate::sled::{Db, Tree}; -use crate::{decode, encode}; -use crate::fs; +use crate::wallets::structures::Wallet; use crate::PathBuf; +use crate::{decode, encode}; fn parse_token_supply(value: &[u8]) -> Option { // 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, }; - 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 // 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()), }; - 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() { return RpcResponse::Binary(b"error: Token contract transaction not found".to_vec()); diff --git a/src/rpc/commands/torrent.rs b/src/rpc/commands/torrent.rs index bcab1e8..79efd99 100644 --- a/src/rpc/commands/torrent.rs +++ b/src/rpc/commands/torrent.rs @@ -1,211 +1,184 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::common::skein::skein_128_hash_bytes; -use crate::rpc::responses::RpcResponse; -use crate::torrent::structs::Torrent; -use crate::sled::Db; -use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom}; -use crate::{decode, encode}; -use crate::File; -use crate::Path; -use crate::PathBuf; - -fn remove_block_pieces_from_db(db: &Db, block_number: u32, info_hash: &str) { - // When the canonical torrent exists, temporary cached pieces for that - // block are no longer needed and can be dropped from the piece cache. - let Ok(tree) = db.open_tree("block_pieces") else { - return; - }; - let prefix = format!("{block_number}-{info_hash}-"); - let iter = tree.range(prefix.as_bytes()..); - - for (key, _value) in iter.flatten() { - if !key.starts_with(prefix.as_bytes()) { - break; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::common::skein::skein_128_hash_bytes; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::torrent::structs::Torrent; +use crate::File; +use crate::PathBuf; +use crate::{decode, encode}; +use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom}; + +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 _ = 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); - } -} diff --git a/src/rpc/commands/torrent_by_block.rs b/src/rpc/commands/torrent_by_block.rs index 5e77771..b0cf153 100644 --- a/src/rpc/commands/torrent_by_block.rs +++ b/src/rpc/commands/torrent_by_block.rs @@ -1,41 +1,41 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::rpc::responses::RpcResponse; -use crate::Path; -use crate::read; - -pub async fn request_block_torrent(height: &u32) -> RpcResponse { - // Torrent files live alongside blocks under a predictable - // `.torrent` naming convention. - let filename = format!("{height}.torrent"); - let ( - _network_name, - _padded_base_coin, - _block_ext, - torrent_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let file_path = Path::new(&torrent_path).join(&filename); - - if !file_path.exists() { - let msg = format!("error: Block {height} not found") - .to_string() - .as_bytes() - .to_vec(); - return RpcResponse::Binary(msg); - } - - match read(&file_path).await { - Ok(binary_data) => RpcResponse::Binary(binary_data), - Err(_) => { - let msg = "error: Error reading torrent file" - .to_string() - .as_bytes() - .to_vec(); - RpcResponse::Binary(msg) - } - } -} +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::read; +use crate::rpc::responses::RpcResponse; +use crate::Path; + +pub async fn request_block_torrent(height: &u32) -> RpcResponse { + // Torrent files live alongside blocks under a predictable + // `.torrent` naming convention. + let filename = format!("{height}.torrent"); + let ( + _network_name, + _padded_base_coin, + _block_ext, + torrent_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let file_path = Path::new(&torrent_path).join(&filename); + + if !file_path.exists() { + let msg = format!("error: Block {height} not found") + .to_string() + .as_bytes() + .to_vec(); + return RpcResponse::Binary(msg); + } + + match read(&file_path).await { + Ok(binary_data) => RpcResponse::Binary(binary_data), + Err(_) => { + let msg = "error: Error reading torrent file" + .to_string() + .as_bytes() + .to_vec(); + RpcResponse::Binary(msg) + } + } +} diff --git a/src/rpc/commands/transaction_by_txid.rs b/src/rpc/commands/transaction_by_txid.rs index f674888..e8ca365 100644 --- a/src/rpc/commands/transaction_by_txid.rs +++ b/src/rpc/commands/transaction_by_txid.rs @@ -1,153 +1,153 @@ -use crate::blocks::block::VRF_BLOCK_BYTES; -use crate::common::binary_conversions::binary_to_string; -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::rpc::command_maps; -use crate::rpc::responses::RpcResponse; -use crate::sled::Db; -use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom}; -use crate::File; -use crate::io; -use crate::PathBuf; - -const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64; - -pub async fn request_transaction_by_txid(db: &Db, txid: Vec) -> RpcResponse { - // Resolve the saved transaction bytes directly from the txid lookup - // tree and the referenced block file. - match lookup_transaction_location(db, txid).await { - Ok((_block, _position, block_filename)) => { - let bytes = calculate_offset(&block_filename, _position).await; - match bytes { - Some(vec) => RpcResponse::Binary(vec), - None => { - let msg = "error: Error parsing block".to_string().as_bytes().to_vec(); - RpcResponse::Binary(msg) - } - } - } - Err(msg) => RpcResponse::Binary(msg.into_bytes()), - } -} - -pub async fn request_transaction_by_txid_with_block(db: &Db, txid: Vec) -> RpcResponse { - // Some callers need the block number alongside the raw transaction - // bytes, so this variant prefixes the payload with the block height. - match lookup_transaction_location(db, txid).await { - Ok((block, position, block_filename)) => { - let bytes = calculate_offset(&block_filename, position).await; - match bytes { - Some(vec) => { - let mut response = Vec::with_capacity(4 + vec.len()); - response.extend_from_slice(&(block as u32).to_le_bytes()); - response.extend_from_slice(&vec); - RpcResponse::Binary(response) - } - None => { - let msg = "error: Error parsing block".to_string().as_bytes().to_vec(); - RpcResponse::Binary(msg) - } - } - } - Err(msg) => RpcResponse::Binary(msg.into_bytes()), - } -} - -async fn lookup_transaction_location(db: &Db, txid: Vec) -> Result<(u64, u32, String), String> { - // The txid tree stores `block:index`, which is enough to locate the - // transaction inside the saved block file on disk. - let tree = db.open_tree("txid").unwrap(); - let value = match tree.get(txid) { - Ok(Some(result)) => result.to_vec(), - Ok(None) => { - return Err("error: Key not found".to_string()); - } - Err(_) => { - return Err("error: Errpr retrieving value".to_string()); - } - }; - - let value_str = binary_to_string(value.to_vec()); - - // Stored txid locations are saved as ASCII `height:index`. - let parts: Vec<&str> = value_str.split(':').collect(); - - let block: u64 = parts[0].parse().unwrap_or_default(); - - let position: u32 = parts[1].parse().unwrap_or_default(); - - let ( - _network_name, - _padded_base_coin, - block_ext, - _torrent_path, - _wallet_path, - block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - - let block_filename = PathBuf::from(block_path) - .join(format!("{block}.{block_ext}")) - .to_string_lossy() - .into_owned(); - Ok((block, position, block_filename)) -} - -async fn read_transaction_type(file_path: &str, position: u64) -> Option { - // Transaction offsets are located by repeatedly reading the type byte - // so the fixed encoded size for each saved transaction can be applied. - let mut file = match File::open(file_path).await { - Ok(file) => file, - Err(_) => return None, - }; - - file.seek(SeekFrom::Start(position)).await.ok()?; - - let mut transaction_type_byte = [0u8; 1]; - file.read_exact(&mut transaction_type_byte).await.ok()?; - - Some(transaction_type_byte[0]) -} - -async fn calculate_offset(file_path: &str, position: u32) -> Option> { - // Walk forward through the serialized block body until the requested - // transaction index is reached, then read exactly that transaction. - let mut total_bytes_to_skip: u64 = HEADER_SIZE; - - let mut current_position: u32 = 1; - - let mut transaction_type = read_transaction_type(file_path, HEADER_SIZE).await?; - - while current_position < position { - // Transaction bodies are fixed-size by type, so the type byte at - // each offset tells us how far to jump to reach the next record. - let size = command_maps::get_bytes(transaction_type) as u64; - - total_bytes_to_skip += size; - - transaction_type = read_transaction_type(file_path, total_bytes_to_skip).await?; - - current_position += 1; - } - - let size = command_maps::get_bytes(transaction_type) as u64; - - let mut file = match File::open(file_path).await { - Ok(file) => file, - Err(_) => { - return None; - } - }; - - file.seek(io::SeekFrom::Start(total_bytes_to_skip)) - .await - .ok()?; - - let mut transaction_bytes = vec![0u8; size as usize]; - file.read_exact(&mut transaction_bytes).await.ok()?; - - // Returned bytes include the transaction type byte at the front so - // callers can parse the payload without extra lookup state. - Some(transaction_bytes) -} +use crate::blocks::block::VRF_BLOCK_BYTES; +use crate::common::binary_conversions::binary_to_string; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::io; +use crate::rpc::command_maps; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::File; +use crate::PathBuf; +use crate::{AsyncReadExt, AsyncSeekExt, SeekFrom}; + +const HEADER_SIZE: u64 = VRF_BLOCK_BYTES as u64; + +pub async fn request_transaction_by_txid(db: &Db, txid: Vec) -> RpcResponse { + // Resolve the saved transaction bytes directly from the txid lookup + // tree and the referenced block file. + match lookup_transaction_location(db, txid).await { + Ok((_block, _position, block_filename)) => { + let bytes = calculate_offset(&block_filename, _position).await; + match bytes { + Some(vec) => RpcResponse::Binary(vec), + None => { + let msg = "error: Error parsing block".to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + } + } + Err(msg) => RpcResponse::Binary(msg.into_bytes()), + } +} + +pub async fn request_transaction_by_txid_with_block(db: &Db, txid: Vec) -> RpcResponse { + // Some callers need the block number alongside the raw transaction + // bytes, so this variant prefixes the payload with the block height. + match lookup_transaction_location(db, txid).await { + Ok((block, position, block_filename)) => { + let bytes = calculate_offset(&block_filename, position).await; + match bytes { + Some(vec) => { + let mut response = Vec::with_capacity(4 + vec.len()); + response.extend_from_slice(&(block as u32).to_le_bytes()); + response.extend_from_slice(&vec); + RpcResponse::Binary(response) + } + None => { + let msg = "error: Error parsing block".to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + } + } + Err(msg) => RpcResponse::Binary(msg.into_bytes()), + } +} + +async fn lookup_transaction_location(db: &Db, txid: Vec) -> Result<(u64, u32, String), String> { + // The txid tree stores `block:index`, which is enough to locate the + // transaction inside the saved block file on disk. + let tree = db.open_tree("txid").unwrap(); + let value = match tree.get(txid) { + Ok(Some(result)) => result.to_vec(), + Ok(None) => { + return Err("error: Key not found".to_string()); + } + Err(_) => { + return Err("error: Errpr retrieving value".to_string()); + } + }; + + let value_str = binary_to_string(value.to_vec()); + + // Stored txid locations are saved as ASCII `height:index`. + let parts: Vec<&str> = value_str.split(':').collect(); + + let block: u64 = parts[0].parse().unwrap_or_default(); + + let position: u32 = parts[1].parse().unwrap_or_default(); + + let ( + _network_name, + _padded_base_coin, + block_ext, + _torrent_path, + _wallet_path, + block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + + let block_filename = PathBuf::from(block_path) + .join(format!("{block}.{block_ext}")) + .to_string_lossy() + .into_owned(); + Ok((block, position, block_filename)) +} + +async fn read_transaction_type(file_path: &str, position: u64) -> Option { + // Transaction offsets are located by repeatedly reading the type byte + // so the fixed encoded size for each saved transaction can be applied. + let mut file = match File::open(file_path).await { + Ok(file) => file, + Err(_) => return None, + }; + + file.seek(SeekFrom::Start(position)).await.ok()?; + + let mut transaction_type_byte = [0u8; 1]; + file.read_exact(&mut transaction_type_byte).await.ok()?; + + Some(transaction_type_byte[0]) +} + +async fn calculate_offset(file_path: &str, position: u32) -> Option> { + // Walk forward through the serialized block body until the requested + // transaction index is reached, then read exactly that transaction. + let mut total_bytes_to_skip: u64 = HEADER_SIZE; + + let mut current_position: u32 = 1; + + let mut transaction_type = read_transaction_type(file_path, HEADER_SIZE).await?; + + while current_position < position { + // Transaction bodies are fixed-size by type, so the type byte at + // each offset tells us how far to jump to reach the next record. + let size = command_maps::get_bytes(transaction_type) as u64; + + total_bytes_to_skip += size; + + transaction_type = read_transaction_type(file_path, total_bytes_to_skip).await?; + + current_position += 1; + } + + let size = command_maps::get_bytes(transaction_type) as u64; + + let mut file = match File::open(file_path).await { + Ok(file) => file, + Err(_) => { + return None; + } + }; + + file.seek(io::SeekFrom::Start(total_bytes_to_skip)) + .await + .ok()?; + + let mut transaction_bytes = vec![0u8; size as usize]; + file.read_exact(&mut transaction_bytes).await.ok()?; + + // Returned bytes include the transaction type byte at the front so + // callers can parse the payload without extra lookup state. + Some(transaction_bytes) +} diff --git a/src/rpc/commands/tx_submit.rs b/src/rpc/commands/tx_submit.rs index 3b696b6..eabeb26 100644 --- a/src/rpc/commands/tx_submit.rs +++ b/src/rpc/commands/tx_submit.rs @@ -18,8 +18,8 @@ use crate::records::memory::enums::ClientType; use crate::records::memory::response_channels::generate_uid; use crate::rpc::command_maps::RPC_SUBMIT_TRANSACTION; use crate::rpc::responses::RpcResponse; -use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory; use crate::sled::Db; +use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory; async fn broadcast_tx(tx_bytes: Vec) { // Broadcast newly accepted mempool transactions only to miner peers, diff --git a/src/rpc/commands/unblock_peer_ip.rs b/src/rpc/commands/unblock_peer_ip.rs index 28a9971..ee41548 100644 --- a/src/rpc/commands/unblock_peer_ip.rs +++ b/src/rpc/commands/unblock_peer_ip.rs @@ -1,6 +1,6 @@ use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; use crate::sled::Db; +use crate::wallets::structures::Wallet; pub async fn unblock_peer( db: &Db, diff --git a/src/rpc/commands/validate_torrent.rs b/src/rpc/commands/validate_torrent.rs index 2d62dba..21afa89 100644 --- a/src/rpc/commands/validate_torrent.rs +++ b/src/rpc/commands/validate_torrent.rs @@ -1,8 +1,8 @@ use crate::rpc::read_bytes_from_stream; use crate::rpc::responses::RpcResponse; use crate::rpc::server::flood_protection::MAX_TORRENT_METADATA_BYTES; -use crate::torrent::structs::Torrent; use crate::sled::Db; +use crate::torrent::structs::Torrent; use crate::Arc; use crate::Mutex; use crate::TcpStream; diff --git a/src/rpc/commands/wallet_register.rs b/src/rpc/commands/wallet_register.rs index 9229d36..8d5d4fd 100644 --- a/src/rpc/commands/wallet_register.rs +++ b/src/rpc/commands/wallet_register.rs @@ -1,17 +1,17 @@ use crate::common::skein::skein_256_hash_bytes; +use crate::decode; +use crate::log::warn; use crate::records::memory::connections::CONNECTIONS; use crate::records::memory::response_channels::{reserve_entry, Command}; use crate::records::wallet_registry::{register_short_address, WalletRegistrationResult}; use crate::rpc::command_maps::RPC_REGISTER_WALLET; use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; -use crate::log::warn; use crate::sled::Db; -use crate::decode; +use crate::timeout; +use crate::wallets::structures::Wallet; use crate::Arc; use crate::Duration; use crate::Mutex; -use crate::timeout; async fn broadcast_wallet_registration( short_address: &[u8], diff --git a/src/rpc/commands/wallet_registry_sync.rs b/src/rpc/commands/wallet_registry_sync.rs index 897192a..cce98f6 100644 --- a/src/rpc/commands/wallet_registry_sync.rs +++ b/src/rpc/commands/wallet_registry_sync.rs @@ -1,7 +1,7 @@ use crate::records::wallet_registry::list_registered_wallets; use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; use crate::sled::Db; +use crate::wallets::structures::Wallet; pub const WALLET_REGISTRY_RECORD_BYTES: usize = Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::PUBLIC_KEY_LENGTH; diff --git a/src/rpc/server/command_loop_state.rs b/src/rpc/server/command_loop_state.rs index 526ad50..6410d27 100644 --- a/src/rpc/server/command_loop_state.rs +++ b/src/rpc/server/command_loop_state.rs @@ -1,19 +1,21 @@ +use crate::io::ErrorKind; +use crate::log::warn; use crate::records::memory::connections::get_client_type_from_memory; use crate::records::memory::enums::ClientType; use crate::rpc::command_maps::RPC_REPLY; use crate::rpc::server::connection_memory_manager::remove_stream_from_memory; use crate::rpc::server::flood_protection::check_request_frequency_with_client_type; use crate::rpc::server::structs::IncomingCommand; -use crate::io::ErrorKind; -use crate::log::warn; use crate::sled::Db; +use crate::sleep; use crate::Arc; use crate::Duration; use crate::Mutex; -use crate::sleep; use crate::TcpStream; -async fn read_next_command_byte(stream_locked: &Arc>) -> Result, String> { +async fn read_next_command_byte( + stream_locked: &Arc>, +) -> Result, String> { // Poll for a command byte with a nonblocking read. This avoids holding // the stream lock across an awaited read while still removing the need // for peek-based readiness checks. diff --git a/src/rpc/server/connection_memory_manager.rs b/src/rpc/server/connection_memory_manager.rs index d9cf397..403b601 100644 --- a/src/rpc/server/connection_memory_manager.rs +++ b/src/rpc/server/connection_memory_manager.rs @@ -101,17 +101,22 @@ pub async fn remove_stream_from_memory(stream: &Arc>) { // Stream cleanup is used when only the socket handle is known, so // search the connection map for the matching Arc before dropping it. - let matching_connection = connection - .connection_map - .iter() - .find_map(|(connection_key, connection_info)| { - let connection_type = ConnectionType::from_bytes(&connection_key.connection_type)?; - if Arc::ptr_eq(&connection_info.stream, stream) { - Some((connection_type, connection_key.ip.clone(), connection_key.port)) - } else { - None - } - }); + let matching_connection = + connection + .connection_map + .iter() + .find_map(|(connection_key, connection_info)| { + let connection_type = ConnectionType::from_bytes(&connection_key.connection_type)?; + if Arc::ptr_eq(&connection_info.stream, stream) { + Some(( + connection_type, + connection_key.ip.clone(), + connection_key.port, + )) + } else { + None + } + }); if let Some((connection_type, ip_bytes, port)) = matching_connection { let ip = crate::common::binary_conversions::binary_to_ip(ip_bytes); diff --git a/src/rpc/server/handshake.rs b/src/rpc/server/handshake.rs index 1c74c5f..f9f5c06 100644 --- a/src/rpc/server/handshake.rs +++ b/src/rpc/server/handshake.rs @@ -1,10 +1,10 @@ -use crate::records::memory::response_channels::Command; use crate::records::memory::response_channels::generate_uid; +use crate::records::memory::response_channels::Command; use crate::rpc::responses::RpcResponse; use crate::rpc::server::connection_memory_manager::write_to_memory; use crate::rpc::server::handshake_processing::{combine_and_send_data, parse_received_data}; -use crate::rpc::server::structs::{CombineAndSendDataParams, HandshakeTestParams}; use crate::rpc::server::handshake_verifications::{connection_count, perform_handshake_tests}; +use crate::rpc::server::structs::{CombineAndSendDataParams, HandshakeTestParams}; use crate::rpc::server::tests::{endpoint_port, is_port_open}; use crate::sled::Db; use crate::Arc; diff --git a/src/rpc/server/handshake_processing.rs b/src/rpc/server/handshake_processing.rs index fb3d69f..33ff8f9 100644 --- a/src/rpc/server/handshake_processing.rs +++ b/src/rpc/server/handshake_processing.rs @@ -1,5 +1,7 @@ use crate::common::binary_conversions::binary_to_ip_port; use crate::common::skein::skein_256_hash_data; +use crate::io::ErrorKind; +use crate::log::error; use crate::rpc::handshake_constants::{ HANDSHAKE_ADDRESS_OFFSET, HANDSHAKE_IP_OFFSET, HANDSHAKE_MESSAGE_BYTES, HANDSHAKE_REQUEST_BYTES, HANDSHAKE_RESPONSE_BYTES, HANDSHAKE_SIGNATURE_OFFSET, @@ -9,10 +11,8 @@ use crate::rpc::server::connection_memory_manager::remove_key_from_memory; use crate::rpc::server::rpc_command_loop::start_loop; use crate::rpc::server::structs::CombineAndSendDataParams; use crate::wallets::structures::Wallet; -use crate::log::error; -use crate::io::ErrorKind; -use crate::AsyncReadExt; use crate::Arc; +use crate::AsyncReadExt; use crate::AsyncWriteExt; use crate::Mutex; use crate::TcpStream; diff --git a/src/rpc/server/mod.rs b/src/rpc/server/mod.rs index dc3d35e..b1ce6b4 100644 --- a/src/rpc/server/mod.rs +++ b/src/rpc/server/mod.rs @@ -1,10 +1,10 @@ // The rpc server module contains the listener, handshake, and command-loop entrypoints. -pub mod connection_memory_manager; pub mod command_loop_state; +pub mod connection_memory_manager; +pub mod flood_protection; pub mod handshake; pub mod handshake_processing; pub mod handshake_verifications; -pub mod flood_protection; pub mod rpc_command_loop; pub mod start_rpc; pub mod structs; diff --git a/src/rpc/server/rpc_command_loop.rs b/src/rpc/server/rpc_command_loop.rs index 023ce00..563c5b2 100644 --- a/src/rpc/server/rpc_command_loop.rs +++ b/src/rpc/server/rpc_command_loop.rs @@ -1,4 +1,5 @@ use crate::common::binary_conversions::binary_to_string; +use crate::encode; use crate::records::memory::enums::ClientType; use crate::records::memory::response_channels::Command; use crate::rpc::server::command_loop_state::next_incoming_command; @@ -7,11 +8,9 @@ use crate::rpc::*; use crate::sled::Db; use crate::Arc; use crate::AsyncWriteExt; -use crate::encode; use crate::Mutex; use crate::TcpStream; - pub async fn start_loop( stream_locked: Arc>, db: Db, @@ -20,13 +19,9 @@ pub async fn start_loop( map: Arc>, ) -> Result<(), String> { 'outer: loop { - let Some(incoming_command) = next_incoming_command( - stream_locked.clone(), - &db, - &connections_key, - &wallet_key, - ) - .await? + let Some(incoming_command) = + next_incoming_command(stream_locked.clone(), &db, &connections_key, &wallet_key) + .await? else { break 'outer Ok(()); }; @@ -309,9 +304,10 @@ pub async fn start_loop( ) .await?; - let result = - commands::transactions_by_address::request_transactions_by_address(&db, &address) - .await; + let result = commands::transactions_by_address::request_transactions_by_address( + &db, &address, + ) + .await; result .send(&stream_locked, Some(&connections_key), uid) .await; diff --git a/src/rpc/server/start_rpc.rs b/src/rpc/server/start_rpc.rs index 1b03014..16dd964 100644 --- a/src/rpc/server/start_rpc.rs +++ b/src/rpc/server/start_rpc.rs @@ -1,6 +1,6 @@ +use crate::log::error; use crate::records::memory::response_channels::Command; use crate::rpc::server::handshake::handle_handshake; -use crate::log::error; use crate::sled::Db; use crate::Arc; use crate::Mutex; @@ -54,13 +54,7 @@ async fn rpc_server( let wallet_key_clone = wallet_key.clone(); let map_clone = map.clone(); tokio::spawn(async move { - handle_handshake( - stream, - db_clone, - wallet_key_clone, - map_clone, - ) - .await; + handle_handshake(stream, db_clone, wallet_key_clone, map_clone).await; }); } Err(e) => { diff --git a/src/rpc/server/structs.rs b/src/rpc/server/structs.rs index 3aaa581..ab3a1a0 100644 --- a/src/rpc/server/structs.rs +++ b/src/rpc/server/structs.rs @@ -1,6 +1,8 @@ use crate::records::memory::enums::ClientType; use crate::records::memory::response_channels::Command; -use crate::rpc::server::flood_protection::{RPC_LONG_WINDOW_LIMIT, RPC_LONG_WINDOW_SECS, RPC_SHORT_WINDOW_SECS}; +use crate::rpc::server::flood_protection::{ + RPC_LONG_WINDOW_LIMIT, RPC_LONG_WINDOW_SECS, RPC_SHORT_WINDOW_SECS, +}; use crate::sled::Db; use crate::Arc; use crate::Mutex; @@ -39,7 +41,6 @@ pub struct HandshakeTestParams<'a> { pub received_ip: &'a str, } - // RpcFloodState is stored as a compact sled value so flood counters can // survive across commands without keeping a separate in-memory map. pub struct RpcFloodState { diff --git a/src/standalone_tools/connections/handshake.rs b/src/standalone_tools/connections/handshake.rs index b5c90bc..d4e2fd2 100644 --- a/src/standalone_tools/connections/handshake.rs +++ b/src/standalone_tools/connections/handshake.rs @@ -1,21 +1,21 @@ use crate::common::binary_conversions::ip_port_to_binary; use crate::common::network_startup::get_ip_and_port; use crate::common::skein::skein_256_hash_data; +use crate::io; use crate::records::memory::response_channels::Byte3; use crate::rpc::handshake_constants::{ HANDSHAKE_ADDRESS_OFFSET, HANDSHAKE_MESSAGE_BYTES, HANDSHAKE_REQUEST_BYTES, HANDSHAKE_RESPONSE_BYTES, HANDSHAKE_SIGNATURE_OFFSET, }; use crate::standalone_tools::connections::sending_request::request; +use crate::timeout; use crate::wallets::structures::Wallet; -use crate::{AsyncReadExt, AsyncWriteExt}; -use crate::{decode, encode}; use crate::Duration; -use crate::io; use crate::SocketAddr; use crate::TcpStream; -use crate::timeout; use crate::Utc; +use crate::{decode, encode}; +use crate::{AsyncReadExt, AsyncWriteExt}; pub enum HandshakeWallet { // Most standalone tools only have the wallet password and need to load the saved wallet. @@ -188,13 +188,9 @@ async fn perform_handshake( // At this point the handshake is complete, so the stream can carry the RPC request. request(&mut stream, json, rpc_command, hashmap_key).await } else { - Err(io::Error::other( - "Handshake failed: Invalid response", - )) + Err(io::Error::other("Handshake failed: Invalid response")) } } else { - Err(io::Error::other( - "Handshake failed: Invalid response", - )) + Err(io::Error::other("Handshake failed: Invalid response")) } } diff --git a/src/standalone_tools/connections/sending_request.rs b/src/standalone_tools/connections/sending_request.rs index 4f1398a..4a8ef95 100644 --- a/src/standalone_tools/connections/sending_request.rs +++ b/src/standalone_tools/connections/sending_request.rs @@ -1,22 +1,21 @@ use crate::common::binary_conversions::ip_to_binary; +use crate::decode; +use crate::io; use crate::records::memory::response_channels::Byte3; use crate::rpc::command_maps::{ - RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_BY_HASH, RPC_BLOCK_BY_HEIGHT, RPC_BLOCK_HEIGHT, - RPC_BLOCK_IP, RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, RPC_LARGEST_TX_FEE, - MAX_RPC_REPLY_BYTES, + MAX_RPC_REPLY_BYTES, RPC_ADDRESS_TOTAL_BALANCE, RPC_BLOCK_BY_HASH, RPC_BLOCK_BY_HEIGHT, + RPC_BLOCK_HEIGHT, RPC_BLOCK_IP, RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, RPC_LARGEST_TX_FEE, RPC_LOAN_CONTRACT, RPC_MEMPOOL_TX_BY_ADDRESS, RPC_MEMPOOL_TX_BY_SIGNATURE, RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_TIME, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VANITY_LOOKUP, }; use crate::standalone_tools::transaction_creator::create_transaction_request; -use crate::wallets::structures::Wallet; -use crate::{AsyncReadExt, AsyncWriteExt}; -use crate::decode; -use crate::Duration; -use crate::io; -use crate::TcpStream; use crate::timeout; +use crate::wallets::structures::Wallet; +use crate::Duration; +use crate::TcpStream; +use crate::{AsyncReadExt, AsyncWriteExt}; pub async fn request( stream: &mut TcpStream, @@ -120,20 +119,22 @@ async fn build_request_bytes( } // Broadcast a signed transaction built by the standalone transaction creator. 8 => { - let msg = create_transaction_request(command_input, hashmap_key).await.map_err(|err| { - io::Error::new( - io::ErrorKind::InvalidInput, - format!("Failed to build broadcast request: {err}"), - ) - })?; + let msg = create_transaction_request(command_input, hashmap_key) + .await + .map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Failed to build broadcast request: {err}"), + ) + })?; bin_msg.extend_from_slice(&msg); } // Lookup a block by height. 9 => { let command_number: u8 = RPC_BLOCK_BY_HEIGHT; - let block_number = command_input.parse::().map_err(|_| { - io::Error::new(io::ErrorKind::InvalidInput, "Invalid block number") - })?; + let block_number = command_input + .parse::() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid block number"))?; bin_msg.extend_from_slice(&command_number.to_le_bytes()); bin_msg.extend_from_slice(&hashmap_key); bin_msg.extend_from_slice(&block_number.to_le_bytes()); @@ -259,7 +260,10 @@ async fn build_request_bytes( })?; let ip_bytes = ip_to_binary(ip); if ip_bytes.len() != 16 { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid IP address")); + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid IP address", + )); } let signature_bytes = decode(signature).map_err(|err| { io::Error::new( @@ -289,7 +293,10 @@ async fn build_request_bytes( })?; let ip_bytes = ip_to_binary(ip); if ip_bytes.len() != 16 { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid IP address")); + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid IP address", + )); } let signature_bytes = decode(signature).map_err(|err| { io::Error::new( @@ -372,9 +379,9 @@ async fn build_request_bytes( "NFT name cannot be empty", )); } - let series = series_text.parse::().map_err(|_| { - io::Error::new(io::ErrorKind::InvalidInput, "Invalid NFT series") - })?; + let series = series_text + .parse::() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid NFT series"))?; let mut nft_name_bytes = name.as_bytes().to_vec(); nft_name_bytes.truncate(15); if nft_name_bytes.len() < 15 { @@ -412,9 +419,10 @@ async fn build_request_bytes( "Wallet registration input must be short|long|signature", ) })?; - let short_address_bytes = Wallet::short_address_to_bytes(short_address).ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, "Invalid short wallet address") - })?; + let short_address_bytes = + Wallet::short_address_to_bytes(short_address).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "Invalid short wallet address") + })?; let long_address_bytes = Wallet::long_address_to_bytes(long_address.to_string()); if long_address_bytes.len() != Wallet::ADDRESS_BYTES_LENGTH { return Err(io::Error::new( diff --git a/src/startup/initialize_startup.rs b/src/startup/initialize_startup.rs index 5c5d79d..7c4b3c2 100644 --- a/src/startup/initialize_startup.rs +++ b/src/startup/initialize_startup.rs @@ -1,109 +1,109 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::common::cli_prompts::cli_options; -use crate::exit; -use crate::fs; -use crate::log::error; -use crate::miner::flag::{ - request_mining_stop, set_mining_state, set_node_mode, MiningState, NodeMode, -}; -use crate::records::block_height::get_block_height::get_height; -use crate::records::memory::averages::{load_initial_blocks, DIFFICULTY_AVERAGE_WINDOW}; -use crate::records::memory::connections::initialize_connection; -use crate::wallets::structures::Wallet; -use crate::Path; - -pub async fn create_file_paths() { - // get all final network-scoped paths from the shared path helper - let ( - _network_name, - _padded_base_coin, - _short_base_coin, - torrent_path, - wallet_path, +use crate::common::cli_prompts::cli_options; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::exit; +use crate::fs; +use crate::log::error; +use crate::miner::flag::{ + request_mining_stop, set_mining_state, set_node_mode, MiningState, NodeMode, +}; +use crate::records::block_height::get_block_height::get_height; +use crate::records::memory::averages::{load_initial_blocks, DIFFICULTY_AVERAGE_WINDOW}; +use crate::records::memory::connections::initialize_connection; +use crate::wallets::structures::Wallet; +use crate::Path; + +pub async fn create_file_paths() { + // get all final network-scoped paths from the shared path helper + let ( + _network_name, + _padded_base_coin, + _short_base_coin, + torrent_path, + wallet_path, block_path, db_path, balance_path, log_path, ) = block_extension_and_paths(); - - // create_dir_all creates missing parent folders too, so the base - // settings paths do not need to be created separately. - fs::create_dir_all(&block_path).expect("Failed to create blocks folder"); - fs::create_dir_all(&torrent_path).expect("Failed to create torrents folder"); - fs::create_dir_all(&db_path).expect("Failed to create db folder"); - // wallet_path points to the wallet file itself, so only its parent directory is created here. - if let Some(wallet_parent) = Path::new(&wallet_path).parent() { - if !wallet_parent.as_os_str().is_empty() { - fs::create_dir_all(wallet_parent).expect("Failed to create wallet folder"); - } + + // create_dir_all creates missing parent folders too, so the base + // settings paths do not need to be created separately. + fs::create_dir_all(&block_path).expect("Failed to create blocks folder"); + fs::create_dir_all(&torrent_path).expect("Failed to create torrents folder"); + fs::create_dir_all(&db_path).expect("Failed to create db folder"); + // wallet_path points to the wallet file itself, so only its parent directory is created here. + if let Some(wallet_parent) = Path::new(&wallet_path).parent() { + if !wallet_parent.as_os_str().is_empty() { + fs::create_dir_all(wallet_parent).expect("Failed to create wallet folder"); + } } fs::create_dir_all(&balance_path).expect("Failed to create balance sheet folder"); fs::create_dir_all(&log_path).expect("Failed to create log folder"); } - -pub async fn obtain_valid_wallet() -> (String, Wallet) { - // keep prompting until a wallet can be opened or created - // using the supplied key material - let wallet_path = Wallet::get_wallet_path().await; - let wallet_exists = Path::new(&wallet_path).exists(); - - loop { - let wallet_key = cli_options().await; - match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { - Ok(wallet) => return (wallet_key, wallet), - Err(e) => { - // Existing wallets fail closed because a bad key cannot safely create a replacement. - if wallet_exists { - eprintln!("Wallet error: {e}."); - error!("Wallet error: {e}."); - exit(1); - } - println!("Wallet error: {e}. Please try again.\n"); - } - } - } -} - -pub async fn prepare_pre_wallet_startup() { - // Prepare the local filesystem and in-memory startup state before any - // wallet-dependent node identity or network activity begins. - set_node_mode(NodeMode::Startup); - request_mining_stop(); - set_mining_state(MiningState::Idle); - initialize_connection().await; - create_file_paths().await; -} - -pub async fn obtain_startup_wallet_key() -> String { - // Open or create the configured wallet and return the validated - // encryption key once the wallet can be used safely. - let (wallet_key, _wallet) = obtain_valid_wallet().await; - wallet_key -} - -pub async fn open_chain_state() -> sled::Db { - // Open the sled state database and warm the rolling averages cache - // once the process is fully ready to continue startup. - let ( - _network_name, - _padded_base_coin, - _suffix, - _torrent_path, - _wallet_path, - _block_path, - db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let db = sled::Config::new() - .path(db_path) - .open() - .expect("Failed to open the database"); - - let latest_block = get_height(&db); - let start_block = latest_block.saturating_sub(DIFFICULTY_AVERAGE_WINDOW.saturating_sub(1)); - // Warm the rolling difficulty cache from the newest window before mining starts. - load_initial_blocks(start_block, latest_block).await; - - db -} + +pub async fn obtain_valid_wallet() -> (String, Wallet) { + // keep prompting until a wallet can be opened or created + // using the supplied key material + let wallet_path = Wallet::get_wallet_path().await; + let wallet_exists = Path::new(&wallet_path).exists(); + + loop { + let wallet_key = cli_options().await; + match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { + Ok(wallet) => return (wallet_key, wallet), + Err(e) => { + // Existing wallets fail closed because a bad key cannot safely create a replacement. + if wallet_exists { + eprintln!("Wallet error: {e}."); + error!("Wallet error: {e}."); + exit(1); + } + println!("Wallet error: {e}. Please try again.\n"); + } + } + } +} + +pub async fn prepare_pre_wallet_startup() { + // Prepare the local filesystem and in-memory startup state before any + // wallet-dependent node identity or network activity begins. + set_node_mode(NodeMode::Startup); + request_mining_stop(); + set_mining_state(MiningState::Idle); + initialize_connection().await; + create_file_paths().await; +} + +pub async fn obtain_startup_wallet_key() -> String { + // Open or create the configured wallet and return the validated + // encryption key once the wallet can be used safely. + let (wallet_key, _wallet) = obtain_valid_wallet().await; + wallet_key +} + +pub async fn open_chain_state() -> sled::Db { + // Open the sled state database and warm the rolling averages cache + // once the process is fully ready to continue startup. + let ( + _network_name, + _padded_base_coin, + _suffix, + _torrent_path, + _wallet_path, + _block_path, + db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let db = sled::Config::new() + .path(db_path) + .open() + .expect("Failed to open the database"); + + let latest_block = get_height(&db); + let start_block = latest_block.saturating_sub(DIFFICULTY_AVERAGE_WINDOW.saturating_sub(1)); + // Warm the rolling difficulty cache from the newest window before mining starts. + load_initial_blocks(start_block, latest_block).await; + + db +} diff --git a/src/startup/network_broadcast.rs b/src/startup/network_broadcast.rs index 3ddbc86..1cc678c 100644 --- a/src/startup/network_broadcast.rs +++ b/src/startup/network_broadcast.rs @@ -1,19 +1,19 @@ use crate::common::binary_conversions::{binary_to_ip, binary_to_string, ip_to_binary}; use crate::common::network_startup::get_ip_and_port; -use crate::records::memory::response_channels::{reserve_entry, Command}; -use crate::records::memory::network_mapping::NodeInfo; +use crate::encode; use crate::records::memory::network_mapping::structs::{ AddAddressParams, DeleteAddressParams, SignedNodeEdit, NODE_ADDED_BY_OFFSET, NODE_ADDED_SIGNATURE_OFFSET, NODE_ADDED_TIMESTAMP_OFFSET, NODE_BLOCKS_MINED_OFFSET, NODE_DELETED_BLOCK_OFFSET, NODE_DELETED_BY_OFFSET, NODE_DELETED_SIGNATURE_OFFSET, NODE_DELETED_TIMESTAMP_OFFSET, NODE_IP_OFFSET, NODE_RECORD_BYTES, }; +use crate::records::memory::network_mapping::NodeInfo; +use crate::records::memory::response_channels::{reserve_entry, Command}; use crate::rpc::command_maps::{RPC_ADD_NETWORK_NODE, RPC_REQUEST_NODE_LIST}; use crate::rpc::responses::RpcResponse; -use crate::wallets::structures::Wallet; use crate::sled::Db; +use crate::wallets::structures::Wallet; use crate::Arc; -use crate::encode; use crate::Mutex; use crate::TcpStream; use crate::Utc; diff --git a/src/startup/node_runtime.rs b/src/startup/node_runtime.rs index a27b163..0b2e077 100644 --- a/src/startup/node_runtime.rs +++ b/src/startup/node_runtime.rs @@ -1,27 +1,26 @@ use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::network_startup::get_listen_socket; +use crate::create_dir_all; +use crate::exit; +use crate::flexi_logger::{ + Cleanup, Criterion, DeferredNow, FileSpec, Logger, LoggerHandle, Naming, Record, WriteMode, +}; +use crate::log::{error, info}; use crate::miner::genesis::create_genesis_transaction; use crate::miner::mining::mine_block; -use crate::records::memory::response_channels::Command; +use crate::panic; use crate::records::memory::mempool::{init_db, setup_mempool}; +use crate::records::memory::response_channels::Command; use crate::rpc::server::start_rpc::start_rpc; +use crate::sled::Db; use crate::startup::connections::handle_connections; use crate::startup::daemonize::{install_shutdown_cleanup, remove_registered_pid_file}; use crate::startup::initialize_startup::open_chain_state; use crate::verifications::verification_service::initialize_global_verification_service; -use crate::flexi_logger::{ - Cleanup, Criterion, DeferredNow, FileSpec, - Logger, LoggerHandle, Naming, Record, WriteMode -}; -use crate::log::{error, info}; -use crate::sled::Db; use crate::Arc; -use crate::create_dir_all; use crate::Error; -use crate::exit; use crate::HashMap; use crate::Mutex; -use crate::panic; use crate::Settings; use std::io::Write; use std::path::Path; @@ -61,11 +60,7 @@ pub async fn initialize_node_logging() -> Result> { // Runtime logging goes to a rotating node log shared by service and console startup paths. let handle = Logger::try_with_str(&Settings::load()?.log_level)? .format(format_log) - .log_to_file( - FileSpec::default() - .directory(&log_path) - .basename("node"), - ) + .log_to_file(FileSpec::default().directory(&log_path).basename("node")) .rotate( Criterion::Size(10_000_000), Naming::Numbers, @@ -138,13 +133,7 @@ pub async fn run_unlocked_node(wallet_key: String, install_shutdown: bool) -> Re // The RPC server starts first so handshake traffic can begin while the rest of the // node initialization continues. tokio::spawn(async move { - start_rpc( - &db_server, - server_address, - wallet_key_clone, - map_cloned, - ) - .await; + start_rpc(&db_server, server_address, wallet_key_clone, map_cloned).await; }); // Connection management, genesis creation, and mining then proceed in the same diff --git a/src/torrent/create_metadata.rs b/src/torrent/create_metadata.rs index 6eabc91..2d9c602 100644 --- a/src/torrent/create_metadata.rs +++ b/src/torrent/create_metadata.rs @@ -1,84 +1,86 @@ -use crate::blocks::block::{NONCE_OFFSET, VRF_OFFSET}; +use crate::blocks::block::{NONCE_OFFSET, VRF_OFFSET}; use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::skein::skein_128_hash_bytes; use crate::log::error; use crate::records::memory::response_channels::{reserve_entry, Command}; -use crate::rpc::command_maps::RPC_SUBMIT_TORRENT; -use crate::rpc::responses::RpcResponse; -use crate::torrent::structs::{Info, Torrent}; -use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory; -use crate::torrent::torrenting_system::torrent_cache::should_broadcast_torrent; -use crate::Arc; -use crate::File; -use crate::HashMap; -use crate::Mutex; +use crate::rpc::command_maps::RPC_SUBMIT_TORRENT; +use crate::rpc::responses::RpcResponse; +use crate::torrent::structs::{Info, Torrent}; +use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory; +use crate::torrent::torrenting_system::torrent_cache::should_broadcast_torrent; +use crate::Arc; +use crate::File; +use crate::HashMap; +use crate::Mutex; use crate::{AsyncReadExt, AsyncWriteExt}; - + pub async fn metadata_from_file( file_path: &str, block_height: u32, difficulty: u64, - timestamp: u32, + timestamp: u32, block_hash: &str, miner_wallet: String, _map: Arc>, ) -> Result, String> { - // The torrent is built from the mined block file and saved under the network torrent path. - let ( - _network_name, - _padded_base_coin, - _block_ext, - out_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); + // The torrent is built from the mined block file and saved under the network torrent path. + let ( + _network_name, + _padded_base_coin, + _block_ext, + out_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); let mut file = File::open(file_path) .await .map_err(|err| format!("Failed to open saved block file for torrent metadata: {err}"))?; let mut content = Vec::new(); if let Err(err) = file.read_to_end(&mut content).await { error!("Failed to read file content: {err}"); - return Err(format!("Failed to read saved block file for torrent metadata: {err}")); + return Err(format!( + "Failed to read saved block file for torrent metadata: {err}" + )); } - - // Pick larger piece sizes for larger blocks so torrents do not exceed the u8 piece index limit. - let mut piece_length: u32; - if !content.is_empty() && content.len() < 1000 { - piece_length = 500_u32; - } else if content.len() >= 1000 && content.len() < 10000 { - piece_length = 1000_u32; - } else if content.len() >= 10000 && content.len() < 100000 { - piece_length = 10000_u32; - } else if content.len() >= 100000 && content.len() < 1000000 { - piece_length = 100000_u32; - } else if content.len() >= 1000000 && content.len() < 10000000 { - piece_length = 1000000_u32; - } else { - piece_length = 2000000_u32; - } - - while !content.is_empty() && content.chunks(piece_length as usize).len() > u8::MAX as usize { - // If the rough size bucket still creates too many pieces, keep doubling until it fits. - piece_length = piece_length.saturating_mul(2); - } - - // The info hash is the 128-bit hash of the full block bytes. - let block_hashed = skein_128_hash_bytes(&content); - - // Nonce and VRF are copied from the serialized block header at fixed offsets. - let nonce = content[NONCE_OFFSET]; - - let vrf_bytes = &content[VRF_OFFSET..VRF_OFFSET + 16]; - let vrf = u128::from_le_bytes( - vrf_bytes - .try_into() - .expect("Slice must be exactly 16 bytes"), - ); - - // Hash each piece separately so peers can verify downloaded chunks before assembly. + + // Pick larger piece sizes for larger blocks so torrents do not exceed the u8 piece index limit. + let mut piece_length: u32; + if !content.is_empty() && content.len() < 1000 { + piece_length = 500_u32; + } else if content.len() >= 1000 && content.len() < 10000 { + piece_length = 1000_u32; + } else if content.len() >= 10000 && content.len() < 100000 { + piece_length = 10000_u32; + } else if content.len() >= 100000 && content.len() < 1000000 { + piece_length = 100000_u32; + } else if content.len() >= 1000000 && content.len() < 10000000 { + piece_length = 1000000_u32; + } else { + piece_length = 2000000_u32; + } + + while !content.is_empty() && content.chunks(piece_length as usize).len() > u8::MAX as usize { + // If the rough size bucket still creates too many pieces, keep doubling until it fits. + piece_length = piece_length.saturating_mul(2); + } + + // The info hash is the 128-bit hash of the full block bytes. + let block_hashed = skein_128_hash_bytes(&content); + + // Nonce and VRF are copied from the serialized block header at fixed offsets. + let nonce = content[NONCE_OFFSET]; + + let vrf_bytes = &content[VRF_OFFSET..VRF_OFFSET + 16]; + let vrf = u128::from_le_bytes( + vrf_bytes + .try_into() + .expect("Slice must be exactly 16 bytes"), + ); + + // Hash each piece separately so peers can verify downloaded chunks before assembly. let mut piece_hashes: Vec> = Vec::new(); for (index, piece) in content.chunks(piece_length as usize).enumerate() { let hash = skein_128_hash_bytes(piece); @@ -88,25 +90,25 @@ pub async fn metadata_from_file( map.insert(piece_number, hash); piece_hashes.push(map); } - - // Torrent info mirrors the data needed to verify the downloaded block later. - let info = Info { - length: content.len() as u64, - this_block_difficulty: difficulty, - timestamp, - nonce, - vrf, - block_hash: block_hash.to_string(), - piece_length, - info_hash: block_hashed, - pieces: piece_hashes, - }; - - let torrent = Torrent { - info, - mined_by: miner_wallet, - }; - + + // Torrent info mirrors the data needed to verify the downloaded block later. + let info = Info { + length: content.len() as u64, + this_block_difficulty: difficulty, + timestamp, + nonce, + vrf, + block_hash: block_hash.to_string(), + piece_length, + info_hash: block_hashed, + pieces: piece_hashes, + }; + + let torrent = Torrent { + info, + mined_by: miner_wallet, + }; + // Save the compact binary torrent metadata so the save path can // broadcast it only after the block height has committed. let torrent_bytes = torrent.to_bytes().await; @@ -115,7 +117,7 @@ pub async fn metadata_from_file( .map_err(|err| format!("Failed to create torrent metadata file: {err}"))?; Ok(torrent_bytes) } - + async fn create_torrent_file( out_path: String, block_height: u32, @@ -123,7 +125,7 @@ async fn create_torrent_file( ) -> Result<(), Box> { // The torrent filename follows the committed block height. let torrent_file_path = crate::Path::new(&out_path).join(format!("{block_height}.torrent")); - + // Write the canonical local .torrent file through a temporary path // so peers never see partial metadata for a committed block. let temp_torrent_file_path = torrent_file_path.with_extension("torrent.tmp"); @@ -134,35 +136,35 @@ async fn create_torrent_file( Ok(()) } - -pub async fn broadcast_new_torrent_to_peers( - block_height: u32, - torrent_bytes: &[u8], - map: Arc>, -) { - // Command byte for "submit torrent". - let command: u8 = RPC_SUBMIT_TORRENT; - - // The cache suppresses repeated broadcasts of identical torrent bytes. - let torrent_hash = skein_128_hash_bytes(torrent_bytes); - if !should_broadcast_torrent(&torrent_hash, block_height).await { - return; - } - let torrent_len = 4 + torrent_bytes.len() as u32; - - // Send the torrent to all currently connected miner peers. - let peers = get_nodes_from_memory().await; - for (connections_key, stream) in peers { - // Each peer needs its own reply mapping entry and UID. - let (uid_bytes, _tx, _rx) = reserve_entry(map.clone()).await; - - let mut message = Vec::with_capacity(1 + 3 + 4 + 4 + torrent_bytes.len()); - message.push(command); // Command byte - message.extend_from_slice(&uid_bytes); // UID - message.extend_from_slice(&torrent_len.to_le_bytes()); // Torrent byte size - message.extend_from_slice(&block_height.to_le_bytes()); // Block height - message.extend_from_slice(torrent_bytes); // Torrent file contents - - RpcResponse::send_raw(&stream, Some(&connections_key), &message).await; - } -} + +pub async fn broadcast_new_torrent_to_peers( + block_height: u32, + torrent_bytes: &[u8], + map: Arc>, +) { + // Command byte for "submit torrent". + let command: u8 = RPC_SUBMIT_TORRENT; + + // The cache suppresses repeated broadcasts of identical torrent bytes. + let torrent_hash = skein_128_hash_bytes(torrent_bytes); + if !should_broadcast_torrent(&torrent_hash, block_height).await { + return; + } + let torrent_len = 4 + torrent_bytes.len() as u32; + + // Send the torrent to all currently connected miner peers. + let peers = get_nodes_from_memory().await; + for (connections_key, stream) in peers { + // Each peer needs its own reply mapping entry and UID. + let (uid_bytes, _tx, _rx) = reserve_entry(map.clone()).await; + + let mut message = Vec::with_capacity(1 + 3 + 4 + 4 + torrent_bytes.len()); + message.push(command); // Command byte + message.extend_from_slice(&uid_bytes); // UID + message.extend_from_slice(&torrent_len.to_le_bytes()); // Torrent byte size + message.extend_from_slice(&block_height.to_le_bytes()); // Block height + message.extend_from_slice(torrent_bytes); // Torrent file contents + + RpcResponse::send_raw(&stream, Some(&connections_key), &message).await; + } +} diff --git a/src/torrent/structs.rs b/src/torrent/structs.rs index 8ed312b..1ccf4c2 100644 --- a/src/torrent/structs.rs +++ b/src/torrent/structs.rs @@ -1,14 +1,14 @@ use crate::records::memory::response_channels::Command; use crate::records::memory::torrentmap::TorrentMap; +use crate::sled::Db; use crate::verifications::verification_service::VerificationService; use crate::wallets::structures::Wallet; -use crate::sled::Db; use crate::Arc; -use crate::{decode, encode}; use crate::HashMap; use crate::Mutex; use crate::Serialize; use crate::TcpStream; +use crate::{decode, encode}; // PieceDownloadJob keeps the network request and the post-download save context together // while still allowing the request bytes to be consumed by request_piece_from_node. @@ -51,6 +51,7 @@ pub struct DownloadSave { pub staged_path: String, pub block_number: u32, pub allow_during_reorg: bool, + pub allow_historical: bool, pub db: Db, pub verification_service: Arc, pub map: Arc>, @@ -244,9 +245,7 @@ impl Torrent { for expected_piece in 1..=actual_piece_count { if cursor.len() < 17 { // 1 byte index + 16 bytes hash - return Err(tokio::io::Error::other( - "Insufficient data for pieces", - )); + return Err(tokio::io::Error::other("Insufficient data for pieces")); } let index = cursor[0]; let expected_piece = u8::try_from(expected_piece).map_err(|_| { diff --git a/src/torrent/torrenting_system/download_pieces.rs b/src/torrent/torrenting_system/download_pieces.rs index b9d8fda..bd052d3 100644 --- a/src/torrent/torrenting_system/download_pieces.rs +++ b/src/torrent/torrenting_system/download_pieces.rs @@ -1,22 +1,25 @@ use crate::common::check_genesis::genesis_checkup; use crate::common::skein::skein_128_hash_bytes; +use crate::decode; +use crate::log::{error, warn}; use crate::records::block_height::get_block_height::get_height; use crate::records::memory::torrentmap::{PieceReservation, TorrentMap}; +use crate::sleep; use crate::torrent::structs::PieceStatus; -use crate::torrent::structs::{PieceDownloadJob, DownloadedPieceJob, DownloadSave, RequestPiece}; +use crate::torrent::structs::{DownloadSave, DownloadedPieceJob, PieceDownloadJob, RequestPiece}; use crate::torrent::torrenting_system::get_nodes::get_nodes_from_memory; use crate::torrent::torrenting_system::request_piece::request_piece_from_node; use crate::torrent::torrenting_system::temp_database_storage::remove_block_pieces_from_db; use crate::torrent::torrenting_system::temp_database_storage::save_piece_to_db; use crate::torrent::torrenting_system::torrent_map::file_download_status; -use crate::log::{error, warn}; use crate::Arc; use crate::Duration; -use crate::decode; use crate::Mutex; -use crate::sleep; -fn expected_piece_hash(torrent: &crate::torrent::structs::Torrent, piece: u8) -> Result { +fn expected_piece_hash( + torrent: &crate::torrent::structs::Torrent, + piece: u8, +) -> Result { // Torrent piece hashes are stored as one-entry maps, indexed by the piece number. let piece_map = torrent .info @@ -30,16 +33,60 @@ fn expected_piece_hash(torrent: &crate::torrent::structs::Torrent, piece: u8) -> .ok_or_else(|| format!("No hash found for piece {piece}")) } -async fn mark_piece_failed( - torrent_map: Arc>, - piece: u8, - ip: &str, -) { +async fn mark_piece_failed(torrent_map: Arc>, piece: u8, ip: &str) { // Marking the peer as failed prevents the scheduler from retrying this piece on the same IP. let mut torrent_map = torrent_map.lock().await; let _ = torrent_map.mark_piece_failed(piece, ip); } +async fn cached_piece_data(params: &DownloadSave, piece: u8) -> Result>, String> { + // A prior preflight may already have staged the exact block/hash/piece. + // Reuse it so rollback replay does not need to request the same bytes twice. + let tree = params + .db + .open_tree("block_pieces") + .map_err(|e| format!("Failed to open block_pieces tree: {e}"))?; + let key = format!( + "{}-{}-{}", + params.block_number, params.torrent.info.info_hash, piece + ); + tree.get(key.as_bytes()) + .map(|piece| piece.map(|data| data.to_vec())) + .map_err(|e| format!("Failed to read cached block piece {key}: {e}")) +} + +async fn remove_cached_piece(params: &DownloadSave, piece: u8) -> Result<(), String> { + let tree = params + .db + .open_tree("block_pieces") + .map_err(|e| format!("Failed to open block_pieces tree: {e}"))?; + let key = format!( + "{}-{}-{}", + params.block_number, params.torrent.info.info_hash, piece + ); + tree.remove(key.as_bytes()) + .map_err(|e| format!("Failed to remove cached block piece {key}: {e}"))?; + Ok(()) +} + +async fn hydrate_cached_pieces(params: &DownloadSave, pieces: &[u8]) -> Result<(), String> { + for piece in pieces { + let Some(data) = cached_piece_data(params, *piece).await? else { + continue; + }; + let expected_hash = expected_piece_hash(¶ms.torrent, *piece)?; + if skein_128_hash_bytes(&data) == expected_hash { + let mut torrent_map = params.torrent_map.lock().await; + let _ = torrent_map.mark_piece_complete(*piece); + } else { + // A stale or corrupt cached row must not be allowed into final + // combine_pieces, because combine_pieces trusts the staged bytes. + remove_cached_piece(params, *piece).await?; + } + } + Ok(()) +} + async fn handle_downloaded_piece(job: DownloadedPieceJob, data: Vec) { // Validate the returned piece against the torrent metadata before marking it // complete and writing it into temporary storage. @@ -70,9 +117,7 @@ async fn handle_downloaded_piece(job: DownloadedPieceJob, data: Vec) { { error!( "[download] failed to stage piece data: block_number={} piece={} err={}", - job.block_number, - job.piece, - err + job.block_number, job.piece, err ); } } else { @@ -94,7 +139,6 @@ fn spawn_piece_download(job: PieceDownloadJob) { tokio::spawn(async move { let piece = job.request.piece_number; let ip = job.request.ip.clone(); - let block_number = job.request.block_number; let torrent_map = job.downloaded_piece.torrent_map.clone(); let downloaded_piece = job.downloaded_piece; @@ -102,12 +146,9 @@ fn spawn_piece_download(job: PieceDownloadJob) { Ok(data) => { handle_downloaded_piece(downloaded_piece, data).await; } - Err(err) => { + Err(_) => { // Failed requests return the piece to the retry pool so another peer can be tried. mark_piece_failed(torrent_map, piece, &ip).await; - warn!( - "[download] piece request failed: block_number={block_number} piece={piece} peer={ip} err={err}" - ); } } }); @@ -122,6 +163,8 @@ pub async fn download_block_pieces(params: DownloadSave) -> Result<(), String> { let pieces: Vec = (1..=piece_count) .map(|i| u8::try_from(i).map_err(|_| "Torrent piece index exceeds u8 limit".to_string())) .collect::, _>>()?; + hydrate_cached_pieces(¶ms, &pieces).await?; + // Piece requests send the 128-bit info hash as little-endian bytes. let info_hash_bytes = decode(¶ms.torrent.info.info_hash) .map_err(|e| format!("Invalid torrent info_hash hex: {e}"))?; @@ -142,7 +185,7 @@ pub async fn download_block_pieces(params: DownloadSave) -> Result<(), String> { current_height }; - if params.block_number < current_height { + if params.block_number < current_height && !params.allow_historical { warn!( "[download] aborting obsolete download: block_number={} current_height={} expected_height={}", params.block_number, diff --git a/src/torrent/torrenting_system/save_block.rs b/src/torrent/torrenting_system/save_block.rs index 243250c..ed35f8d 100644 --- a/src/torrent/torrenting_system/save_block.rs +++ b/src/torrent/torrenting_system/save_block.rs @@ -1,5 +1,6 @@ use crate::common::check_genesis::genesis_checkup; use crate::common::skein::skein_128_hash_bytes; +use crate::log::{error, warn}; use crate::miner::flag::is_reorganizing_mode; use crate::records::block_height::get_block_height::get_height; use crate::records::record_chain::save::save_block; @@ -9,7 +10,6 @@ use crate::torrent::structs::DownloadSave; use crate::torrent::torrenting_system::create_file::combine_pieces; use crate::torrent::torrenting_system::save_torrent::promote_staged_torrent; use crate::torrent::torrenting_system::temp_database_storage::remove_block_pieces_from_db; -use crate::log::{error, warn}; use crate::Path; async fn cleanup_download_pieces(params: &DownloadSave) { @@ -125,12 +125,19 @@ pub async fn verify_and_save_block(params: DownloadSave) -> Result<(), String> { } // Once the block is saved successfully, move the staged torrent into its canonical path. - if let Err(err) = promote_staged_torrent(Path::new(¶ms.staged_path), params.block_number) { - error!( - "[download] failed to promote staged torrent: block_number={} err={}", - params.block_number, - err - ); + match promote_staged_torrent(Path::new(¶ms.staged_path), params.block_number) { + Ok(_) => { + // The saved block and canonical torrent can now serve future + // piece requests, so this node no longer needs its temporary + // piece rows for the accepted candidate. + cleanup_download_pieces(¶ms).await; + } + Err(err) => { + error!( + "[download] failed to promote staged torrent: block_number={} err={}", + params.block_number, err + ); + } } Ok(()) diff --git a/src/torrent/torrenting_system/save_torrent.rs b/src/torrent/torrenting_system/save_torrent.rs index 79c175e..c6fa492 100644 --- a/src/torrent/torrenting_system/save_torrent.rs +++ b/src/torrent/torrenting_system/save_torrent.rs @@ -1,5 +1,7 @@ -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::{create_dir_all, fs, read, read_dir, remove_file, AsyncWriteExt, File, Path}; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::torrent::structs::Torrent; +use crate::{create_dir_all, fs, read, read_dir, remove_file, AsyncWriteExt, Path}; +use std::io::ErrorKind; fn staged_torrent_dir() -> Result { // Keep staged torrents under a dedicated subdirectory so incoming @@ -19,12 +21,12 @@ fn staged_torrent_dir() -> Result { Ok(staging_dir) } -fn staged_torrent_path(height: u32, suffix: u32) -> std::path::PathBuf { - // Multiple torrents may temporarily exist for the same height during - // sync and orphan handling, so each staged file gets a numeric suffix. - let ( - _network_name, - _padded_base_coin, +fn staged_torrent_path(height: u32, info_hash: &str) -> std::path::PathBuf { + // Each candidate gets its own deterministic staging path, so competing + // torrents at the same height cannot race over a shared suffix. + let ( + _network_name, + _padded_base_coin, _block_ext, out_path, _wallet_path, @@ -32,11 +34,11 @@ fn staged_torrent_path(height: u32, suffix: u32) -> std::path::PathBuf { _db_path, _balance_path, _log_path, - ) = block_extension_and_paths(); - Path::new(&out_path) - .join("staging") - .join(format!("{height}.torrent-{suffix}")) -} + ) = block_extension_and_paths(); + Path::new(&out_path) + .join("staging") + .join(format!("{height}.{info_hash}.torrent")) +} fn canonical_torrent_path(height: u32) -> std::path::PathBuf { let ( @@ -64,18 +66,28 @@ async fn torrent_bytes_match(path: &Path, torrent_bytes: &[u8]) -> Result Option<(u32, u32)> { - // Staged torrent names encode both the height and the local suffix - // so replay logic can sort and process them deterministically. - let (height_str, suffix_str) = file_name.split_once(".torrent-")?; - let height = height_str.parse::().ok()?; - let suffix = suffix_str.parse::().ok()?; - Some((height, suffix)) -} +fn parse_staged_torrent_file_name(file_name: &str) -> Option<(u32, String)> { + // New staged names encode both height and candidate hash: + // `..torrent`. + if let Some(stem) = file_name.strip_suffix(".torrent") { + let (height_str, info_hash) = stem.split_once('.')?; + let height = height_str.parse::().ok()?; + if !info_hash.is_empty() { + return Some((height, info_hash.to_string())); + } + } + + // Older staging files used `.torrent-`. Keep parsing + // them so cleanup/replay can drain stale staging directories after upgrade. + let (height_str, suffix_str) = file_name.split_once(".torrent-")?; + let height = height_str.parse::().ok()?; + let suffix = suffix_str.parse::().ok()?; + Some((height, format!("legacy-{suffix:010}"))) +} pub async fn list_staged_torrents() -> Result, String> { - // Enumerate staged torrents in height/suffix order so replay and - // cleanup logic handle them in a stable sequence. + // Enumerate staged torrents in height/candidate order so replay and + // cleanup logic handle them in a stable sequence. let staging_dir = staged_torrent_dir()?; if !staging_dir.exists() { @@ -96,10 +108,10 @@ pub async fn list_staged_torrents() -> Result, St let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { continue; }; - if let Some((height, suffix)) = parse_staged_torrent_file_name(file_name) { - staged.push((height, suffix, path)); - } - } + if let Some((height, candidate_key)) = parse_staged_torrent_file_name(file_name) { + staged.push((height, candidate_key, path)); + } + } staged.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); Ok(staged @@ -124,58 +136,64 @@ pub async fn list_staged_torrents_for_height( .collect()) } -pub async fn save_staged_torrent(height: u32, torrent_bytes: &[u8]) -> Result { - // Find the next available suffix for this height before writing the - // incoming torrent into the staging directory. Exact duplicates are - // suppressed if the same torrent is already staged or canonical. - let staging_path = staged_torrent_dir()?; - - create_dir_all(&staging_path) - .await - .map_err(|e| format!("Failed to create staging directory: {e}"))?; - - let canonical_path = canonical_torrent_path(height); - if torrent_bytes_match(&canonical_path, torrent_bytes).await? { - return Ok(canonical_path.to_string_lossy().to_string()); - } - - let mut next_suffix = 1_u32; - let mut entries = read_dir(&staging_path) - .await - .map_err(|e| format!("Failed to read staging directory: {e}"))?; - - while let Some(entry) = entries - .next_entry() - .await - .map_err(|e| format!("Failed to iterate staging directory: {e}"))? - { - let path = entry.path(); - let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { - continue; - }; - if let Some((entry_height, suffix)) = parse_staged_torrent_file_name(file_name) { - if entry_height == height { - if torrent_bytes_match(&path, torrent_bytes).await? { - return Ok(path.to_string_lossy().to_string()); - } - if suffix >= next_suffix { - next_suffix = suffix.saturating_add(1); - } - } - } - } - - let torrent_file_path = staged_torrent_path(height, next_suffix); - let mut torrent_file = File::create(&torrent_file_path) - .await - .map_err(|e| format!("Failed to create staged torrent file: {e}"))?; +pub async fn save_staged_torrent(height: u32, torrent_bytes: &[u8]) -> Result { + // Store this candidate by its advertised block info hash. That makes + // staging idempotent per candidate and prevents same-height races from + // overwriting a different torrent file. + let staging_path = staged_torrent_dir()?; + + create_dir_all(&staging_path) + .await + .map_err(|e| format!("Failed to create staging directory: {e}"))?; + + let torrent = Torrent::from_bytes(torrent_bytes) + .await + .map_err(|e| format!("Failed to parse staged torrent: {e}"))?; + let info_hash = torrent.info.info_hash; + + let canonical_path = canonical_torrent_path(height); + if torrent_bytes_match(&canonical_path, torrent_bytes).await? { + return Ok(canonical_path.to_string_lossy().to_string()); + } + + let torrent_file_path = staged_torrent_path(height, &info_hash); + if torrent_file_path.exists() { + if torrent_bytes_match(&torrent_file_path, torrent_bytes).await? { + return Ok(torrent_file_path.to_string_lossy().to_string()); + } + return Err(format!( + "Staged torrent collision for height {height} info_hash {info_hash}" + )); + } + + let mut torrent_file = match crate::tokio::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&torrent_file_path) + .await + { + Ok(file) => file, + Err(err) if err.kind() == ErrorKind::AlreadyExists => { + if torrent_bytes_match(&torrent_file_path, torrent_bytes).await? { + return Ok(torrent_file_path.to_string_lossy().to_string()); + } + return Err(format!( + "Staged torrent collision for height {height} info_hash {info_hash}" + )); + } + Err(err) => return Err(format!("Failed to create staged torrent file: {err}")), + }; // Keep torrent staging separate from the canonical torrent file so // validation and orphan logic can decide when promotion is safe. - torrent_file - .write_all(torrent_bytes) - .await - .map_err(|e| format!("Failed to write staged torrent file: {e}"))?; + torrent_file + .write_all(torrent_bytes) + .await + .map_err(|e| format!("Failed to write staged torrent file: {e}"))?; + torrent_file + .flush() + .await + .map_err(|e| format!("Failed to flush staged torrent file: {e}"))?; Ok(torrent_file_path.to_string_lossy().to_string()) } @@ -225,16 +243,20 @@ pub fn promote_staged_torrent(staged_path: &Path, height: u32) -> Result>, - local_height: u32, - hashmap_key: Byte3, - connections_key: String, -) -> io::Result<()> { - // Ask the remote node for the torrent metadata for the requested - // block height using the shared response hashmap key. - let request_torrent: u8 = RPC_TORRENT_BY_HEIGHT; - let request_torrent_binary = request_torrent.to_le_bytes(); - let get_height_binary = local_height.to_le_bytes(); - let mut message = Vec::new(); - message.extend(request_torrent_binary); - message.extend(hashmap_key); - message.extend(get_height_binary); - RpcResponse::send_raw(&stream, Some(&connections_key), &message).await; - Ok(()) -} - -pub async fn handle_response_and_save_torrent( - height: u32, - db: &Db, - torrent: Torrent, - wallet_key: &str, - map: Arc>, - allow_during_reorg: bool, -) -> Result<(), String> { - let Some((torrent, staged_path)) = - stage_and_verify_torrent(height, db, torrent, wallet_key, true).await? - else { - return Ok(()); - }; - let torrent_bytes = torrent.clone().to_bytes().await; - - setup_download_for_torrent( - height, - torrent, - staged_path, - allow_during_reorg, - db.clone(), - map.clone(), - ) - .await?; - - // A requested torrent is only forwarded after this node has the - // complete validated block available for piece requests. - broadcast_new_torrent_to_peers(height, &torrent_bytes, map).await; - Ok(()) -} - -#[derive(Clone)] -pub struct ProcessTorrentResponse { - pub height: u32, - pub db: Db, - pub torrent: Torrent, - pub wallet_key: String, - pub map: Arc>, - pub allow_during_reorg: bool, - pub process_now: bool, -} - -pub async fn process_torrent_response(params: ProcessTorrentResponse) -> Result<(), String> { - let Some((torrent, staged_path)) = stage_and_verify_torrent( - params.height, - ¶ms.db, - params.torrent, - ¶ms.wallet_key, - params.process_now, - ) - .await? - else { - return Ok(()); - }; - let torrent_bytes = torrent.clone().to_bytes().await; - - setup_download_for_torrent( - params.height, - torrent, - staged_path, - params.allow_during_reorg, - params.db, - params.map.clone(), - ) - .await?; - - // Successful replay/download means this node can now seed the block - // behind this torrent, so rebroadcasting is safe. - broadcast_new_torrent_to_peers(params.height, &torrent_bytes, params.map).await; - Ok(()) -} - -pub async fn stage_and_verify_torrent( - height: u32, - db: &Db, - torrent: Torrent, - wallet_key: &str, - process_now: bool, -) -> Result, String> { - // Stage the torrent first so a parseable candidate is never lost just - // because the current chain state is in the middle of syncing or - // orphan correction. Immediate validation/download only happens when - // this torrent is actionable right now. - let torrent_bytes = torrent.clone().to_bytes().await; - let staged_path = save_staged_torrent(height, &torrent_bytes) - .await - .map_err(|err| format!("Failed to save staged torrent: {err}"))?; - set_torrent_status( - height, - &torrent.info.info_hash, - TorrentStatus::Pending, - ) - .await; - - if !process_now { - return Ok(None); +use crate::io; +use crate::log::warn; +use crate::records::memory::response_channels::{Byte3, Command}; +use crate::records::memory::torrent_status::{set_torrent_status, TorrentStatus}; +use crate::rpc::command_maps::RPC_TORRENT_BY_HEIGHT; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::torrent::create_metadata::broadcast_new_torrent_to_peers; +use crate::torrent::structs::Torrent; +use crate::torrent::torrenting_system::save_torrent::save_staged_torrent; +use crate::torrent::torrenting_system::setup_block_download::setup_download; +use crate::verifications::verification_service::global_verification_service; +use crate::Arc; +use crate::Mutex; +use crate::TcpStream; + +pub async fn send_request_torrent_message( + stream: Arc>, + local_height: u32, + hashmap_key: Byte3, + connections_key: String, +) -> io::Result<()> { + // Ask the remote node for the torrent metadata for the requested + // block height using the shared response hashmap key. + let request_torrent: u8 = RPC_TORRENT_BY_HEIGHT; + let request_torrent_binary = request_torrent.to_le_bytes(); + let get_height_binary = local_height.to_le_bytes(); + let mut message = Vec::new(); + message.extend(request_torrent_binary); + message.extend(hashmap_key); + message.extend(get_height_binary); + RpcResponse::send_raw(&stream, Some(&connections_key), &message).await; + Ok(()) +} + +pub async fn handle_response_and_save_torrent( + height: u32, + db: &Db, + torrent: Torrent, + wallet_key: &str, + map: Arc>, + allow_during_reorg: bool, +) -> Result<(), String> { + let Some((torrent, staged_path)) = + stage_and_verify_torrent(height, db, torrent, wallet_key, true).await? + else { + return Ok(()); + }; + let torrent_bytes = torrent.clone().to_bytes().await; + + setup_download_for_torrent( + height, + torrent, + staged_path, + allow_during_reorg, + db.clone(), + map.clone(), + ) + .await?; + + // A requested torrent is only forwarded after this node has the + // complete validated block available for piece requests. + broadcast_new_torrent_to_peers(height, &torrent_bytes, map).await; + Ok(()) +} + +#[derive(Clone)] +pub struct ProcessTorrentResponse { + pub height: u32, + pub db: Db, + pub torrent: Torrent, + pub wallet_key: String, + pub map: Arc>, + pub allow_during_reorg: bool, + pub process_now: bool, +} + +pub async fn process_torrent_response(params: ProcessTorrentResponse) -> Result<(), String> { + let Some((torrent, staged_path)) = stage_and_verify_torrent( + params.height, + ¶ms.db, + params.torrent, + ¶ms.wallet_key, + params.process_now, + ) + .await? + else { + return Ok(()); + }; + let torrent_bytes = torrent.clone().to_bytes().await; + + setup_download_for_torrent( + params.height, + torrent, + staged_path, + params.allow_during_reorg, + params.db, + params.map.clone(), + ) + .await?; + + // Successful replay/download means this node can now seed the block + // behind this torrent, so rebroadcasting is safe. + broadcast_new_torrent_to_peers(params.height, &torrent_bytes, params.map).await; + Ok(()) +} + +pub async fn stage_and_verify_torrent( + height: u32, + db: &Db, + torrent: Torrent, + wallet_key: &str, + process_now: bool, +) -> Result, String> { + // Stage the torrent first so a parseable candidate is never lost just + // because the current chain state is in the middle of syncing or + // orphan correction. Immediate validation/download only happens when + // this torrent is actionable right now. + let torrent_bytes = torrent.clone().to_bytes().await; + let staged_path = save_staged_torrent(height, &torrent_bytes) + .await + .map_err(|err| format!("Failed to save staged torrent: {err}"))?; + set_torrent_status(height, &torrent.info.info_hash, TorrentStatus::Pending).await; + + if !process_now { + return Ok(None); } if let Err(error) = torrent.verify(height, db, wallet_key).await { warn!("[torrent] validation failed: height={height} err={error}"); return Err(error); } - - Ok(Some((torrent, staged_path))) -} - -pub async fn setup_download_for_torrent( - height: u32, - torrent: Torrent, - staged_path: String, - allow_during_reorg: bool, - db: Db, - map: Arc>, -) -> Result<(), String> { - let verification_service = global_verification_service() - .ok_or_else(|| "Verification service not initialized".to_string())?; - - // Hand the staged torrent off to the download pipeline so the - // full block can be assembled, verified, and saved. - setup_download( - height, - torrent, - staged_path, - allow_during_reorg, - db, - Arc::new(verification_service), - map, - ) - .await -} + + Ok(Some((torrent, staged_path))) +} + +pub async fn setup_download_for_torrent( + height: u32, + torrent: Torrent, + staged_path: String, + allow_during_reorg: bool, + db: Db, + map: Arc>, +) -> Result<(), String> { + let verification_service = global_verification_service() + .ok_or_else(|| "Verification service not initialized".to_string())?; + + // Hand the staged torrent off to the download pipeline so the + // full block can be assembled, verified, and saved. + setup_download( + height, + torrent, + staged_path, + allow_during_reorg, + db, + Arc::new(verification_service), + map, + ) + .await +} diff --git a/src/torrent/unpack_remote_torrent.rs b/src/torrent/unpack_remote_torrent.rs index 204b255..8132b6d 100644 --- a/src/torrent/unpack_remote_torrent.rs +++ b/src/torrent/unpack_remote_torrent.rs @@ -30,14 +30,9 @@ async fn send_torrent_request( RpcResponse::send_raw(&stream, Some(connections_key), &message).await; let mut rx = torrent_checkup_rx.lock().await; - if let Some(torrent_contents) = - timeout(Duration::from_secs(30), rx.recv()) - .await - .map_err(|_| { - format!( - "Timed out waiting for torrent response at height {local_height}" - ) - })? + if let Some(torrent_contents) = timeout(Duration::from_secs(30), rx.recv()) + .await + .map_err(|_| format!("Timed out waiting for torrent response at height {local_height}"))? { // Torrent errors can come back as text instead of binary metadata. if let Ok(torrent_str) = String::from_utf8(torrent_contents.clone()) { diff --git a/src/verifications/async_funcs/checks/block_balance.rs b/src/verifications/async_funcs/checks/block_balance.rs index 4c7a0fd..497a2ec 100644 --- a/src/verifications/async_funcs/checks/block_balance.rs +++ b/src/verifications/async_funcs/checks/block_balance.rs @@ -1,6 +1,7 @@ use crate::blocks::loans::LoanContractTransaction; use crate::common::nft_assets::nft_asset_name; use crate::common::types::Transaction; +use crate::decode; use crate::records::balance_sheet::get_wallet_balance::get_balance_with_db; use crate::records::memory::mempool::{ get_basecoin_balance, get_coin_balance, signature_exists, BASECOIN, @@ -8,7 +9,6 @@ use crate::records::memory::mempool::{ use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; use crate::rpc::responses::RpcResponse; use crate::sled::Db; -use crate::decode; use std::collections::{HashMap, HashSet}; #[derive(Clone)] @@ -200,7 +200,12 @@ async fn transaction_balance_view( // The sender pays the transferred asset plus the base-coin fee. add_debit(&mut debits, &transfer.sender, asset, transfer.value); - add_debit(&mut debits, &transfer.sender, BASECOIN.clone(), transfer.txfee); + add_debit( + &mut debits, + &transfer.sender, + BASECOIN.clone(), + transfer.txfee, + ); Ok(TransactionBalanceView { hash: transfer.hash().await, @@ -311,7 +316,12 @@ async fn transaction_balance_view( let loan = &tx.unsigned_loan_contract; let mut debits = Vec::new(); - add_debit(&mut debits, &loan.lender, loan.loan_coin.clone(), loan.loan_amount); + add_debit( + &mut debits, + &loan.lender, + loan.loan_coin.clone(), + loan.loan_amount, + ); add_debit(&mut debits, &loan.lender, BASECOIN.clone(), loan.txfee); add_debit( &mut debits, @@ -342,7 +352,12 @@ async fn transaction_balance_view( contract.unsigned_loan_contract.loan_coin, payment_total, ); - add_debit(&mut debits, &payment.address, BASECOIN.clone(), payment.txfee); + add_debit( + &mut debits, + &payment.address, + BASECOIN.clone(), + payment.txfee, + ); Ok(TransactionBalanceView { hash: payment.hash().await, diff --git a/src/verifications/async_funcs/transactions.rs b/src/verifications/async_funcs/transactions.rs index 2edc5ce..6062f28 100644 --- a/src/verifications/async_funcs/transactions.rs +++ b/src/verifications/async_funcs/transactions.rs @@ -99,9 +99,7 @@ async fn verify_transaction( } } Err(err) => { - return Err(format!( - "Validation failed for rewards transaction: {err}" - )); + return Err(format!("Validation failed for rewards transaction: {err}")); } } } @@ -112,9 +110,7 @@ async fn verify_transaction( return Ok(value); } Err(err) => { - return Err(format!( - "Validation failed for transfer transaction: {err}" - )); + return Err(format!("Validation failed for transfer transaction: {err}")); } } } diff --git a/src/verifications/async_funcs/validate_torrent_data.rs b/src/verifications/async_funcs/validate_torrent_data.rs index 26adee1..c04f857 100644 --- a/src/verifications/async_funcs/validate_torrent_data.rs +++ b/src/verifications/async_funcs/validate_torrent_data.rs @@ -20,14 +20,28 @@ impl Torrent { let now = Utc::now().timestamp() as u32; if let Err(e) = Self::validate_piece_count(self).await { - let _ = - update_ip_score(&ip, "miner", InfractionType::BadTorrent, now, db, wallet_key).await; + let _ = update_ip_score( + &ip, + "miner", + InfractionType::BadTorrent, + now, + db, + wallet_key, + ) + .await; return Err(e); } if let Err(e) = Self::validate_mined_by(self, db).await { - let _ = - update_ip_score(&ip, "miner", InfractionType::BadTorrent, now, db, wallet_key).await; + let _ = update_ip_score( + &ip, + "miner", + InfractionType::BadTorrent, + now, + db, + wallet_key, + ) + .await; return Err(e); } diff --git a/src/verifications/async_funcs/verify_burn.rs b/src/verifications/async_funcs/verify_burn.rs index faa2be8..a39d603 100644 --- a/src/verifications/async_funcs/verify_burn.rs +++ b/src/verifications/async_funcs/verify_burn.rs @@ -71,9 +71,7 @@ impl BurnTransaction { // burn any positive integer amount. if nft_exists { if self.unsigned_burn.value != NFT_UNIT { - return Err(format!( - "NFT burns must destroy exactly {NFT_UNIT} units." - )); + return Err(format!("NFT burns must destroy exactly {NFT_UNIT} units.")); } } else if self.unsigned_burn.value == 0 { return Err("Burn value must be greater than 0.".to_string()); diff --git a/src/verifications/verification_service.rs b/src/verifications/verification_service.rs index 14eb514..66922df 100644 --- a/src/verifications/verification_service.rs +++ b/src/verifications/verification_service.rs @@ -1,8 +1,8 @@ use crate::common::types::Transaction; use crate::rayon::ThreadPool; use crate::rayon::ThreadPoolBuilder; -use crate::verifications::async_funcs::transactions::verify_transactions; use crate::verifications::async_funcs::checks::block_balance::BlockBalanceTracker; +use crate::verifications::async_funcs::transactions::verify_transactions; use crate::verifications::sync_funcs::transaction_verify_loop::COUNTER; use crate::verifications::verification_types::{ VerificationChunkJob, VerificationChunkResult, VerificationJob, diff --git a/src/wallets/wallet_bytes.rs b/src/wallets/wallet_bytes.rs index 8040d29..5297319 100644 --- a/src/wallets/wallet_bytes.rs +++ b/src/wallets/wallet_bytes.rs @@ -1,4 +1,4 @@ -use crate::common::skein::{skein_256_hash_bytes, ripemd160_hash_bytes}; +use crate::common::skein::{ripemd160_hash_bytes, skein_256_hash_bytes}; use crate::log::error; use crate::wallets::structures::Wallet; use crate::{decode, encode};