Fix staged torrent candidate race

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

View File

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

View File

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

View File

@ -11,10 +11,7 @@ use blockchain::{Local, LocalResult, NaiveDate, NaiveTime, TimeZone};
fn pad_to_width(input: &str, width: usize) -> 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:<width$}"),
);
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
result
}

View File

@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad 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:<width$}"),
);
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
result
}

View File

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

View File

@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad 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:<width$}"),
);
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
result
}

View File

@ -12,10 +12,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad 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:<width$}"),
);
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
result
}

View File

@ -10,10 +10,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad 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:<width$}"),
);
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
result
}

View File

@ -13,10 +13,7 @@ use blockchain::{create_dir_all, AsyncWriteExt};
// pad 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:<width$}"),
);
let _ = std::fmt::write(&mut result, format_args!("{input:<width$}"));
result
}

View File

@ -26,17 +26,50 @@ async fn decode_one_transaction(tx_bytes: &[u8]) -> Option<String> {
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,
}
}

View File

@ -27,17 +27,50 @@ async fn decode_mempool_transaction(response: &[u8]) -> Option<String> {
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,
}
}

View File

@ -45,10 +45,9 @@ fn decode_network_info(response: &[u8]) -> Option<String> {
// Mainnet uses CLC and testnet uses CLTC, so the prefix length is
// 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)?;

View File

@ -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}");

View File

@ -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;

View File

@ -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<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <block_number>", 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<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <block_number>", 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.");
}
}

View File

@ -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}");

View File

@ -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),

View File

@ -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:<width$}"),
);
result
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
resolve_local_input_short_address(address.trim())
}
#[tokio::main]
async fn main() {
// Get the filename from the command line arguments
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage:./sign_swap <path/to/file.json>");
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<Value, _> = 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:<width$}"));
result
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
resolve_local_input_short_address(address.trim())
}
#[tokio::main]
async fn main() {
// Get the filename from the command line arguments
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage:./sign_swap <path/to/file.json>");
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<Value, _> = 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}");
}

View File

@ -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<Self> {
// 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];

View File

@ -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<Self> {
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?;

View File

@ -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<Self> {
// 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?;

View File

@ -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.

View File

@ -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<Self> {
// 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.

View File

@ -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<Self> {
// 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?;

View File

@ -21,9 +21,9 @@ pub struct UnsignedLoanContractTransaction {
pub payment_period: String, // 1 byte d, w, or m for days, weeks, or months
pub payment_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<Self> {
// 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];

View File

@ -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<Self> {
// 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];

View File

@ -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<Self> {
// 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?;

View File

@ -65,8 +65,7 @@ impl RewardsTransaction {
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// 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.

View File

@ -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<Self> {
// 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?;

View File

@ -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<Self> {
// 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];

View File

@ -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<Self> {
// 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?;

View File

@ -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?;

View File

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

View File

@ -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.

View File

@ -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

View File

@ -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(

View File

@ -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,

View File

@ -1,15 +1,24 @@
use crate::log::info;
use crate::common::skein::skein_128_hash_bytes;
use crate::log::{info, warn};
use crate::miner::flag::begin_reorg_lock;
use crate::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<Torrent> {
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<Torrent> {
let mut preferred: Option<Torrent> = None;
) -> Vec<Torrent> {
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, &params.db, wallet_key).await?;
let verification_service = global_verification_service()
.ok_or_else(|| "Verification service not initialized".to_string())?;
let torrent_map = create_torrent_map(torrent).await?;
let download_save_params = DownloadSave {
torrent_map,
torrent: torrent.clone(),
staged_path: String::new(),
block_number: height,
allow_during_reorg: true,
allow_historical: true,
db: params.db.clone(),
verification_service: std::sync::Arc::new(verification_service),
map: params.map.clone(),
};
download_block_pieces(download_save_params).await?;
let result = combine_pieces(&params.db, height, &torrent.info.info_hash).await?;
if result.len() != torrent.info.length as usize {
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Downloaded candidate length does not match torrent metadata.".to_string());
}
if skein_128_hash_bytes(&result) != torrent.info.info_hash {
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Hash validation failed for complete block".to_string());
}
let loaded_block = load_block_from_binary(&result)
.await
.map_err(|err| format!("Failed to load block from binary: {err}"))?;
let header_hash = loaded_block.vrf_block.hash().await;
if header_hash != torrent.info.block_hash {
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Candidate header hash does not match torrent metadata.".to_string());
}
if height > 0 {
let parent_height = height - 1;
let parent_header = load_block_header(parent_height).await?;
let parent_hash = parent_header.hash().await;
if loaded_block.vrf_block.unmined_block.previous_hash != parent_hash {
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Incorrect previous_block_hash.".to_string());
}
}
preferred
Ok(())
}
pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), String> {
@ -89,41 +162,69 @@ pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), Stri
let local_torrent = load_torrent(&params.db, height).await?;
let 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(
&params,
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;
}
}
}
}

View File

@ -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")
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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(&params, 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(&params.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(&params.db) < height_before_window_check
{
replay_waiting = true;
}
error!("[orphan] orphan window check error: {err}");
}
}
let height_before_replay = get_height(&params.db);
match replay_staged_torrents(&params, wallet_key).await {
Ok(()) => {}
Err(err) => error!("[orphan] staged torrent replay error: {err}"),
Err(err) => {
replay_waiting |= should_retry_staged_candidate(&err);
error!("[orphan] staged torrent replay error: {err}");
}
}
if get_height(&params.db) > height_before_replay {
replay_waiting = false;
}
if replay_waiting && !params.node_syncing {
return Err("orphan replay is waiting for block data".to_string());
}
if !params.node_syncing {
end_reorg_lock();

View File

@ -39,10 +39,17 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
for transaction in transactions.into_iter().rev() {
match transaction {
Transaction::Rewards(rewards_tx) => {
undo_rewards_transaction(rewards_tx, &mining_receiver, &params.db, current_height).await
undo_rewards_transaction(
rewards_tx,
&mining_receiver,
&params.db,
current_height,
)
.await
}
Transaction::Transfer(transfer_tx) => {
undo_transfer_transaction(transfer_tx.clone(), &mining_receiver, &params.db).await;
undo_transfer_transaction(transfer_tx.clone(), &mining_receiver, &params.db)
.await;
rolled_back_transactions.push(Transaction::Transfer(transfer_tx));
}
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, &params.db)
.await;
undo_create_token_transaction(
create_token_tx.clone(),
&mining_receiver,
&params.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, &params.db).await;
undo_issue_token_transaction(
issue_token_tx.clone(),
&mining_receiver,
&params.db,
)
.await;
rolled_back_transactions.push(Transaction::IssueToken(issue_token_tx));
}
Transaction::Nft(create_nft_tx) => {
undo_create_nft_transaction(create_nft_tx.clone(), &mining_receiver, &params.db).await;
undo_create_nft_transaction(
create_nft_tx.clone(),
&mining_receiver,
&params.db,
)
.await;
rolled_back_transactions.push(Transaction::Nft(create_nft_tx));
}
Transaction::Marketing(marketing_tx) => {
undo_marketing_transaction(marketing_tx.clone(), &mining_receiver, &params.db).await;
undo_marketing_transaction(marketing_tx.clone(), &mining_receiver, &params.db)
.await;
rolled_back_transactions.push(Transaction::Marketing(marketing_tx));
}
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, &params.db).await;
undo_loan_creation_transaction(loan_tx.clone(), &mining_receiver, &params.db)
.await;
rolled_back_transactions.push(Transaction::Lender(loan_tx));
}
Transaction::Borrower(borrower_tx) => {
undo_borrower_transaction(borrower_tx.clone(), &mining_receiver, &params.db).await?;
undo_borrower_transaction(borrower_tx.clone(), &mining_receiver, &params.db)
.await?;
rolled_back_transactions.push(Transaction::Borrower(borrower_tx));
}
Transaction::Collateral(collateral_tx) => {
undo_collateral_transaction(collateral_tx.clone(), &mining_receiver, &params.db).await?;
undo_collateral_transaction(
collateral_tx.clone(),
&mining_receiver,
&params.db,
)
.await?;
rolled_back_transactions.push(Transaction::Collateral(collateral_tx));
}
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, &params.db).await?;
undo_vanity_transaction(vanity_tx.clone(), &mining_receiver, &params.db)
.await?;
rolled_back_transactions.push(Transaction::Vanity(vanity_tx));
}
}

View File

@ -11,12 +11,12 @@ use crate::blocks::transfer::TransferTransaction;
use crate::blocks::vanity::VanityAddressTransaction;
use crate::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<F, Fut>(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 {

View File

@ -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(),
);
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -1,93 +1,92 @@
use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::loans::LoanContractTransaction;
use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::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);
}
}

View File

@ -1,49 +1,48 @@
use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::marketing::MarketingTransaction;
use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::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();
}

View File

@ -1,8 +1,10 @@
use crate::blocks::rewards::RewardsTransaction;
use crate::blocks::rewards::RewardsTransaction;
use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::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();
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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)

View File

@ -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::<u16>().ok()?;
Some((ip, port))
}
use crate::records::memory::structs::{ConnectionInfo, ConnectionKey};
#[derive(Clone)]
struct ReconnectContext {
db: Db,
wallet_key: String,
map: Arc<Mutex<Command>>,
}
lazy_static! {
static ref RECONNECT_CONTEXT: Mutex<Option<ReconnectContext>> = 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<Mutex<Command>>,
) {
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::<u16>().ok()?;
Some((ip, port))
}
use crate::records::memory::structs::{ConnectionInfo, ConnectionKey};
#[derive(Clone)]
struct ReconnectContext {
db: Db,
wallet_key: String,
map: Arc<Mutex<Command>>,
}
lazy_static! {
static ref RECONNECT_CONTEXT: Mutex<Option<ReconnectContext>> = 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<Mutex<Command>>) {
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<ConnectionInfo> {
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<Mutex<TcpStream>>,
connection_type: ConnectionType,
ip: String,
port: u16,
command_map: Arc<Mutex<Command>>,
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<ConnectionInfo> {
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<Mutex<TcpStream>>,
connection_type: ConnectionType,
ip: String,
port: u16,
command_map: Arc<Mutex<Command>>,
) {
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<u8> = 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<u8> = 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<Arc<Mutex<TcpStream>>> {
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<Arc<Mutex<TcpStream>>> {
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<Arc<Mutex<TcpStream>>> {
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<Arc<Mutex<TcpStream>>> {
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<Arc<Mutex<TcpStream>>> {
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<Arc<Mutex<TcpStream>>> {
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<u16> {
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<u8>, 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<RwLock<Option<Connection>>> = 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<u16> {
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<u8>, 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<RwLock<Option<Connection>>> = 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<ClientType> {
// 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
}

View File

@ -1,89 +1,89 @@
use super::*;
use super::*;
pub async fn signature_exists(signature: &str, hash: &str) -> Result<bool> {
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<u8> = 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<u8> = 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<u8> = 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<u8> = 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<i64> = 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<i64> = 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<u64, Box<dyn std::error::Error + Send + Sync>> {
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<u64, Box<dyn std::error::Error + Send + Sync>> {
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<u64, Box<dyn std::error::Error + Send + Sync>> {
@ -393,63 +392,62 @@ pub async fn get_pending_payments_for_contract(
// Loan verification uses this to prevent pending payments from exceeding
// 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<i64> = 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<i64> = row.get(0);
let result = (total.unwrap_or(0) as u32).to_le_bytes().to_vec();
RpcResponse::Binary(result)
}

View File

@ -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<Client> = OnceCell::new();
pub const EPOCH_ROW_CAP: i64 = 100_000;
const NFT_UNIT: i64 = 100_000_000;
const CLEANUP_DEPTH: u32 = 10;
const CLEANUP_BATCH_LIMIT: i64 = 1000;
#[derive(Clone)]
enum SelectedMempoolTransaction {
// These variants hold the minimal fields needed to score, mark, and
// later apply selected mempool transactions into a saved block.
Transfer {
id: i64,
fee: i64,
sender: String,
value: i64,
coin: String,
nft_series: i32,
receiver: String,
hash: String,
},
Token {
id: i64,
fee: i64,
creator: String,
number: i64,
ticker: String,
hash: String,
},
IssueToken {
id: i64,
fee: i64,
creator: String,
number: i64,
ticker: String,
hash: String,
},
Burn {
id: i64,
fee: i64,
address: String,
coin: String,
nft_series: i32,
value: i64,
hash: String,
},
Nft {
id: i64,
fee: i64,
creator: String,
nft_name: String,
series: i16,
count: i64,
hash: String,
},
Marketing {
id: i64,
fee: i64,
advertiser: String,
hash: String,
},
Vanity {
id: i64,
fee: i64,
address: String,
vanity_address: String,
hash: String,
},
Swap {
id: i64,
fee1: i64,
fee2: i64,
ticker1: String,
nft_series1: i32,
ticker2: String,
nft_series2: i32,
value1: i64,
value2: i64,
sender1: String,
tip1: i64,
tip2: i64,
sender2: String,
hash: String,
},
Lender {
id: i64,
fee: i64,
loan_coin: String,
loan_amount: i64,
lender: String,
collateral: String,
collateral_amount: i64,
borrower: String,
txid: String,
hash: String,
},
Borrower {
id: i64,
fee: i64,
payback_amount: i64,
contract_hash: String,
address: String,
tip: i64,
hash: String,
},
Collateral {
id: i64,
fee: i64,
address: String,
contract_hash: String,
hash: String,
},
}
#[derive(Clone, Default)]
pub struct SelectedMempoolBatch {
// The selected transaction view is kept separate from the original
// serialized bytes so save paths can stream the original payloads.
transactions: Vec<SelectedMempoolTransaction>,
originals: Vec<Vec<u8>>,
}
impl SelectedMempoolTransaction {
fn table_name(&self) -> &'static str {
match self {
SelectedMempoolTransaction::Transfer { .. } => "transfer",
SelectedMempoolTransaction::Token { .. } => "token",
SelectedMempoolTransaction::IssueToken { .. } => "issue_token",
SelectedMempoolTransaction::Burn { .. } => "burn",
SelectedMempoolTransaction::Nft { .. } => "nft",
SelectedMempoolTransaction::Marketing { .. } => "marketing",
SelectedMempoolTransaction::Vanity { .. } => "vanity_address",
SelectedMempoolTransaction::Swap { .. } => "swap",
SelectedMempoolTransaction::Lender { .. } => "loan_contract",
SelectedMempoolTransaction::Borrower { .. } => "loan_payment",
SelectedMempoolTransaction::Collateral { .. } => "collateral_claim",
}
}
fn id(&self) -> i64 {
match self {
SelectedMempoolTransaction::Transfer { id, .. }
| SelectedMempoolTransaction::Token { id, .. }
| SelectedMempoolTransaction::IssueToken { id, .. }
| SelectedMempoolTransaction::Burn { id, .. }
| SelectedMempoolTransaction::Nft { id, .. }
| SelectedMempoolTransaction::Marketing { id, .. }
| SelectedMempoolTransaction::Vanity { id, .. }
| SelectedMempoolTransaction::Swap { id, .. }
| SelectedMempoolTransaction::Lender { id, .. }
| SelectedMempoolTransaction::Borrower { id, .. }
| SelectedMempoolTransaction::Collateral { id, .. } => *id,
}
}
}
impl SelectedMempoolBatch {
pub fn is_empty(&self) -> bool {
self.transactions.is_empty()
}
}
mod lookups;
mod processing;
mod schema;
mod selection;
pub use lookups::{
get_basecoin_balance, get_coin_balance, get_pending_payments_for_contract, largest_fee,
signature_exists, total_transactions, transaction_by_signature, transactions_by_address,
};
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::HashMap;
use crate::NoTls;
use crate::{task, AtomicBool};
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use std::fs::File;
use std::io::Write;
use tokio_postgres::Client;
lazy_static! {
pub static ref BASECOIN: String = {
let (
_network_name,
base_coin,
_suffix,
_torrentpath,
_wallet_path,
_blockpath,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
format!("{base_coin:<15}")
};
static ref CLEANUP_RUNNING: AtomicBool = AtomicBool::new(false);
}
pub static DB: OnceCell<Client> = OnceCell::new();
pub const EPOCH_ROW_CAP: i64 = 100_000;
const NFT_UNIT: i64 = 100_000_000;
const CLEANUP_DEPTH: u32 = 10;
const CLEANUP_BATCH_LIMIT: i64 = 1000;
#[derive(Clone)]
enum SelectedMempoolTransaction {
// These variants hold the minimal fields needed to score, mark, and
// later apply selected mempool transactions into a saved block.
Transfer {
id: i64,
fee: i64,
sender: String,
value: i64,
coin: String,
nft_series: i32,
receiver: String,
hash: String,
},
Token {
id: i64,
fee: i64,
creator: String,
number: i64,
ticker: String,
hash: String,
},
IssueToken {
id: i64,
fee: i64,
creator: String,
number: i64,
ticker: String,
hash: String,
},
Burn {
id: i64,
fee: i64,
address: String,
coin: String,
nft_series: i32,
value: i64,
hash: String,
},
Nft {
id: i64,
fee: i64,
creator: String,
nft_name: String,
series: i16,
count: i64,
hash: String,
},
Marketing {
id: i64,
fee: i64,
advertiser: String,
hash: String,
},
Vanity {
id: i64,
fee: i64,
address: String,
vanity_address: String,
hash: String,
},
Swap {
id: i64,
fee1: i64,
fee2: i64,
ticker1: String,
nft_series1: i32,
ticker2: String,
nft_series2: i32,
value1: i64,
value2: i64,
sender1: String,
tip1: i64,
tip2: i64,
sender2: String,
hash: String,
},
Lender {
id: i64,
fee: i64,
loan_coin: String,
loan_amount: i64,
lender: String,
collateral: String,
collateral_amount: i64,
borrower: String,
txid: String,
hash: String,
},
Borrower {
id: i64,
fee: i64,
payback_amount: i64,
contract_hash: String,
address: String,
tip: i64,
hash: String,
},
Collateral {
id: i64,
fee: i64,
address: String,
contract_hash: String,
hash: String,
},
}
#[derive(Clone, Default)]
pub struct SelectedMempoolBatch {
// The selected transaction view is kept separate from the original
// serialized bytes so save paths can stream the original payloads.
transactions: Vec<SelectedMempoolTransaction>,
originals: Vec<Vec<u8>>,
}
impl SelectedMempoolTransaction {
fn table_name(&self) -> &'static str {
match self {
SelectedMempoolTransaction::Transfer { .. } => "transfer",
SelectedMempoolTransaction::Token { .. } => "token",
SelectedMempoolTransaction::IssueToken { .. } => "issue_token",
SelectedMempoolTransaction::Burn { .. } => "burn",
SelectedMempoolTransaction::Nft { .. } => "nft",
SelectedMempoolTransaction::Marketing { .. } => "marketing",
SelectedMempoolTransaction::Vanity { .. } => "vanity_address",
SelectedMempoolTransaction::Swap { .. } => "swap",
SelectedMempoolTransaction::Lender { .. } => "loan_contract",
SelectedMempoolTransaction::Borrower { .. } => "loan_payment",
SelectedMempoolTransaction::Collateral { .. } => "collateral_claim",
}
}
fn id(&self) -> i64 {
match self {
SelectedMempoolTransaction::Transfer { id, .. }
| SelectedMempoolTransaction::Token { id, .. }
| SelectedMempoolTransaction::IssueToken { id, .. }
| SelectedMempoolTransaction::Burn { id, .. }
| SelectedMempoolTransaction::Nft { id, .. }
| SelectedMempoolTransaction::Marketing { id, .. }
| SelectedMempoolTransaction::Vanity { id, .. }
| SelectedMempoolTransaction::Swap { id, .. }
| SelectedMempoolTransaction::Lender { id, .. }
| SelectedMempoolTransaction::Borrower { id, .. }
| SelectedMempoolTransaction::Collateral { id, .. } => *id,
}
}
}
impl SelectedMempoolBatch {
pub fn is_empty(&self) -> bool {
self.transactions.is_empty()
}
}
mod lookups;
mod processing;
mod schema;
mod selection;
pub use lookups::{
get_basecoin_balance, get_coin_balance, get_pending_payments_for_contract, largest_fee,
signature_exists, total_transactions, transaction_by_signature, transactions_by_address,
};
pub use processing::{
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<String> {
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<String> {
row.try_get::<_, Option<String>>(column)?
.ok_or_else(|| anyhow!("Missing required column {column}"))
}
fn add_balance_change(
db: &Db,
balance_changes: &mut HashMap<BalanceKey, i64>,
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<BalanceKey, i64>,
address: Vec<u8>,
coin: Vec<u8>,
delta: i64,
) {
*balance_changes
.entry(BalanceKey { address, coin })
.or_insert(0) += delta;
}
}
fn add_balance_change(
db: &Db,
balance_changes: &mut HashMap<BalanceKey, i64>,
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<BalanceKey, i64>,
address: Vec<u8>,
coin: Vec<u8>,
delta: i64,
) {
*balance_changes
.entry(BalanceKey { address, coin })
.or_insert(0) += delta;
}
fn address_key_bytes(db: &Db, address: &str) -> Vec<u8> {
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<String> {
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<u8>, Vec<u8>)> {
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<u8>, 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<i64> {
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<String> {
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<u8>, Vec<u8>)> {
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<u8>, 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<i64> {
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<u64> {
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<u64> {
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<u64> {
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<u64> {
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?)
}

View File

@ -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<bool> {
// 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(())
}

View File

@ -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(())
}

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -1,30 +1,30 @@
use super::*;
impl NodeInfo {
pub async fn broadcast_node(
map: Arc<Mutex<Command>>,
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<Mutex<Command>>,
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<Vec<Arc<Mutex<TcpStream>>>> = 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<u8> = 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())
}
}
}

View File

@ -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())
}
}

View File

@ -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<String, u8> = 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<String, u8> = 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(())
}
}

View File

@ -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<HashMap<String, NodeInfo>> = Mutex::new(HashMap::new());
static ref PING_MONITORS: Mutex<HashMap<String, String>> = 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<HashMap<String, NodeInfo>> = Mutex::new(HashMap::new());
static ref PING_MONITORS: Mutex<HashMap<String, String>> = 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;

View File

@ -11,24 +11,23 @@ impl NodeInfo {
false
}
pub async fn find_address_by_ip(ip: &str) -> Option<String> {
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<String> {
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<String> {
let map = ADDRESS_MAP.lock().await;
map.get(address).map(|node_info| node_info.ip.clone())
}
pub async fn active_node_ips() -> Vec<String> {
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<u8> {
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<u8> = 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<u8> = 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
}
}

View File

@ -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()
}

View File

@ -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)

View File

@ -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

View File

@ -373,12 +373,8 @@ fn apply_effect(db: &Db, effect: &PendingEffect) -> Result<AppliedEffect, String
contract_id,
payment,
} => {
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<AppliedEffect, String> {
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}"))?;

View File

@ -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;

View File

@ -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(),

View File

@ -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};

View File

@ -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<dyn std::error::Error>> {

View File

@ -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<Vec<u8>> {
// Client handshakes are assembled from the signed message, wallet identity, timestamp,

View File

@ -1,40 +1,46 @@
use crate::common::binary_conversions::binary_to_ip_port;
use crate::common::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;
}

View File

@ -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<Mutex<TcpStream>>,

View File

@ -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<Mutex<TcpStream>>,

View File

@ -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<Mutex<TcpStream>>,

View File

@ -1,10 +1,10 @@
use crate::records::memory::response_channels::Command;
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::network_mapping::structs::{AddAddressParams, SignedNodeEdit};
use crate::records::memory::network_mapping::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 =

View File

@ -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.

View File

@ -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

View File

@ -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()),
}
}

View File

@ -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

View File

@ -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

View File

@ -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<CollateralClaimTransaction> = 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<u8>, 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());
};

View File

@ -1,10 +1,10 @@
use crate::records::memory::response_channels::Command;
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::network_mapping::structs::{DeleteAddressParams, SignedNodeEdit};
use crate::records::memory::network_mapping::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 =

View File

@ -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;

View File

@ -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

View File

@ -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<Vec<u8>> {
// 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());
}

View File

@ -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.

View File

@ -1,9 +1,11 @@
use crate::records::memory::enums::ClientType;
use crate::records::memory::response_channels::{delete_entry, get_entry, is_retired_entry, Command};
use crate::rpc::commands::bad_rpc_call;
use crate::rpc::command_maps::MAX_RPC_REPLY_BYTES;
use crate::rpc::read_bytes_from_stream;
use crate::log::warn;
use crate::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;

View File

@ -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<u64> {
// 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());

View File

@ -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);
}
}

View File

@ -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
// `<height>.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
// `<height>.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)
}
}
}

View File

@ -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<u8>) -> 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<u8>) -> 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<u8>) -> 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<u8> {
// 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<Vec<u8>> {
// 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<u8>) -> 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<u8>) -> 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<u8>) -> 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<u8> {
// 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<Vec<u8>> {
// 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)
}

View File

@ -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<u8>) {
// Broadcast newly accepted mempool transactions only to miner peers,

View File

@ -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,

View File

@ -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;

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