Compare commits

..

29 Commits

Author SHA1 Message Date
viraladmin 7e99a98643 standalone tools fix for command 47 2026-06-14 15:26:18 -06:00
viraladmin 9cc17ac0b5 network mapping fix 2026-06-14 09:52:52 -06:00
viraladmin 0e852e0e69 fixed network ync startup process 2026-06-13 19:36:03 -06:00
viraladmin b8c8e47b69 fixed mining io issues 2026-06-13 13:51:54 -06:00
viraladmin f651fda2dd startup sync bug fixes 2026-06-13 09:55:52 -06:00
viraladmin a40ec3a06e bug fixes 2026-06-12 19:01:49 -06:00
viraladmin 7181cc4e26 handshake process 2026-06-12 10:49:34 -06:00
viraladmin a4c4518adf added RPC explorer commands 2026-06-12 10:27:28 -06:00
viraladmin 38dabfa27f bug fixes 2026-06-11 15:43:48 -06:00
viraladmin 0845fd3ba6 bug fixes 2026-06-11 15:06:02 -06:00
viraladmin 64df13519a bug fixes 2026-06-11 02:26:21 -06:00
viraladmin f071d728a3 bug fixes 2026-06-11 02:03:09 -06:00
viraladmin fb5362ae3a comment correction 2026-06-07 09:23:52 -06:00
viraladmin d07faa57ed Updated docs 2026-06-06 13:07:55 -06:00
viraladmin 13ae207739 sybil detection fixes 2026-06-05 21:33:37 -06:00
viraladmin 04b93275fa fixed syncing issues 2026-06-05 09:36:06 -06:00
viraladmin a621522170 adjusting difficulty algorithm 2026-06-04 21:11:23 -06:00
viraladmin 413adbf241 node startup bug fixes 2026-06-04 20:45:43 -06:00
viraladmin 5c4c1baf7e node startup bug fixes 2026-06-04 15:22:07 -06:00
viraladmin 3358e2e95a node sstartup bug fixes 2026-06-04 11:51:07 -06:00
viraladmin 0aea32a2d5 removed long addresses 2026-06-04 10:18:55 -06:00
viraladmin 6cd01d1345 sybil fix, vanity fix, wallet selection fix 2026-06-04 09:06:51 -06:00
viraladmin 9f3f53ca34 binary wallet functions fix 2026-06-02 18:09:46 -06:00
viraladmin 7cd421e142 error handling fixes 2026-06-02 17:05:48 -06:00
viraladmin 88b043cf41 difficulty adjectment 2026-06-01 13:51:23 -06:00
viraladmin 72b894d6cb fixed windows service bug 2026-06-01 09:19:47 -06:00
viraladmin 4b66a2bd54 fixed issues with orphan correction and mining 2026-06-01 08:29:11 -06:00
viraladmin ddc4cef037 fixed prphan correction status bug 2026-05-27 10:19:59 -06:00
viraladmin c2f907a36a fixed debugging 2026-05-26 17:33:03 -06:00
214 changed files with 5945 additions and 2746 deletions

View File

@ -2,7 +2,7 @@
This file lists the command-line tools built from `src/bin`. On Windows, add `.exe` to each tool name. Most network lookup tools prompt for the wallet decryption key because the RPC handshake is authenticated before the request is sent.
Transaction creator tools produce signed transaction JSON. After creating a transaction, broadcast it with `broadcast_transaction`.
Transaction creator tools produce signed transaction JSON and save it under `./transactions/` using the transaction hash as the filename. Creating a transaction does not submit it to the network; broadcast the saved JSON with `broadcast_transaction`.
## Table of Contents
@ -18,7 +18,7 @@ Transaction creator tools produce signed transaction JSON. After creating a tran
### create_new_wallet
Creates or loads an encrypted wallet file at the requested path.
Creates a new encrypted wallet file at the requested path.
Requires:
@ -35,15 +35,13 @@ create_new_wallet <wallet_path> <wallet_filename>
Expected reply:
```text
Long Address: <long wallet address>
Short Address: <short wallet address>
Vanity Address: <optional vanity address>
Public Key: <public key>
```
### register_wallet
Registers the configured wallet short-address to long-address mapping with a peer.
Registers the wallet with the network. Registration is required before an address can be used.
Requires:
@ -63,15 +61,9 @@ Expected reply:
Wallet registered: <short address>
```
or:
```text
Wallet registration failed.
```
### recreate_wallet
Recreates an encrypted wallet file from a private key.
Recreates an encrypted wallet file from a private key. The private key must be obtained with `private_key_from_image`. You must also know the encryption/decryption key used when the wallet was originally created.
Requires:
@ -93,7 +85,7 @@ Expected reply:
### recreate_wallet_from_image
Recreates an encrypted wallet file from a private-key image.
Recreates an encrypted wallet file from a private-key image. The image can be a base64 image file or a PNG image file. A base64 image must be stored in a standalone file, not inside the wallet file. You must also know the encryption/decryption key used when the wallet was originally created.
Requires:
@ -137,7 +129,7 @@ Private key image saved to <path>
### private_key_from_image
Reads a private key back from an image file.
Reads a private key back from a wallet file or base64 image file. This tool does not read private keys from PNG saved images.
Requires:
@ -195,7 +187,7 @@ Service state: <state>
## Transaction Creation Tools
These tools prompt for the fields needed by each transaction type, sign the transaction with the configured wallet and print the signed JSON. The same JSON is also written to the transaction output folder when the tool creates the output file successfully.
These tools prompt for the fields needed by each transaction type, sign the transaction with the selected wallet, print the signed JSON and write the same JSON to `./transactions/<transaction_hash>.json`. The transaction is not sent to the network until the saved JSON is submitted with `broadcast_transaction`.
### create_transfer_tx
@ -243,7 +235,7 @@ transaction: <signed create-token transaction json>
### create_issue_token_tx
Creates an issue-token transaction for an existing token.
Issues more units of an existing token. This can only be used by the token creator, and only when the token was originally created without a hard cap that prevents further issuance.
Requires:
@ -377,6 +369,21 @@ transaction: <partially signed swap transaction json>
Creates the first side of a loan contract transaction. The second party signs the transaction with `verify_sign_loan_tx`.
The lender creates this transaction. The tool asks for:
- Loan coin/token: the asset being lent.
- Loan amount: how much of that asset is lent.
- Payment period: `daily`, `weekly` or `monthly`; this sets the payment cadence.
- Payment number: how many payments are required before the loan is considered paid.
- Payment amount: how much the borrower must pay each period.
- Grace period: how many payments may be missed before collateral can be claimed.
- Max late value: how far behind the borrower may be, in payment value, before collateral can be claimed.
- Collateral coin/token/NFT: the asset the borrower puts up as collateral.
- Collateral amount: how much collateral is required; enter `1` for NFT collateral.
- Start date: the date the loan begins, entered as `YYYY-MM-DD`.
- Borrower wallet: the borrower's short address or vanity address.
- Transaction fee: the base-currency fee paid by the lender.
Requires:
- Running node: No
@ -399,6 +406,13 @@ transaction: <partially signed loan transaction json>
Creates a loan payment transaction for an existing loan contract.
The borrower uses this after the loan contract is active. The tool asks for:
- Loan contract hash: the hash of the loan contract being repaid.
- Payment amount: the amount being paid toward the loan.
- Miner tip: a tip in the loan asset; it must be at least 1% of the payment amount.
- Transaction fee: the base-currency fee paid by the borrower.
Requires:
- Running node: No
@ -421,6 +435,11 @@ transaction: <signed loan-payment transaction json>
Creates a collateral claim transaction for an eligible loan contract.
The lender uses this when the loan rules allow collateral to be claimed. The tool asks for:
- Loan contract hash: the hash of the loan contract whose collateral is being claimed.
- Transaction fee: the base-currency fee paid by the claimant.
Requires:
- Running node: No
@ -462,12 +481,6 @@ successful_broadcast: true
<transaction hash>
```
or:
```text
<error returned by peer>
```
## Transaction Signing Tools
### verify_sign_swap_tx
@ -1228,7 +1241,7 @@ On Windows, the tool also prints `[Postgres-Testnet]`.
### sign_message
Signs a plain text message with the configured wallet.
Signs a plain text message with the selected wallet.
Requires:

View File

@ -100,11 +100,21 @@ Valid common values:
The logger also supports more advanced module-level filters through `flexi_logger`, but normal nodes should use one of the simple values above.
### `IP`
### `PUBLIC_IP`
Reachable IP address or domain name announced by this node during handshakes.
Reachable public IP address announced by this node during handshakes and network-map broadcasts.
Use `127.0.0.1` only for local testing. A public node should use a public IP address or a domain name that resolves to the node and reaches the configured RPC port.
Use `127.0.0.1` only for local testing. A public node should use the public IP address that outside peers use to reach the configured RPC port.
This value is protocol identity. It is validated against forbidden private or unroutable ranges before the node is accepted by peers.
### `LISTEN_IP`
Local bind address used by the RPC server.
Use `0.0.0.0` to listen on all local interfaces, which is usually the correct value for nodes behind NAT or router port forwarding.
This value is never announced to peers. It may be private, loopback or wildcard because it only controls where the local process listens.
### `RPC_PORT`
@ -132,6 +142,22 @@ Maximum number of outgoing peer connections the node tries to discover and maint
This should be greater than `0`. A recommended value is `10`.
### `VALIDATOR`
Disables local mining while keeping the node connected to peers, synced and able to validate and rebroadcast network activity.
Use this for bootstrap or validator-only nodes:
```ini
VALIDATOR = "true"
```
Mining nodes should use:
```ini
VALIDATOR = "false"
```
### `THREADS`
Number of async worker tasks used during each mining nonce round.
@ -152,13 +178,15 @@ Piggyback entries are startup peers. The node tries these peers when it first st
```ini
[Piggyback]
PIGGYBACK_1 = "1.2.3.4:50053"
PIGGYBACK_1 = "contractless.dev"
PIGGYBACK_2 = "5.6.7.8:50053"
```
The node only needs one live piggyback peer to begin discovery. Additional entries are backups if earlier entries are offline or unreachable.
Piggyback entries should be public, routable peers. Private, loopback and invalid addresses are filtered before startup connections begin.
Piggyback entries may use a public IP endpoint or a hostname. If no port is included, the active network RPC port is used. Hostnames are resolved only for these startup entries; nodes must still announce public IP endpoints in handshakes and network mapping.
Piggyback entries should resolve to public, routable peers. Private, loopback and invalid addresses are filtered before startup connections begin.
## `[Postgres-Testnet]`

View File

@ -6,12 +6,12 @@ TORRENT_PATH = "./torrents"
DB_PATH = "./state"
BALANCE_SHEET = "./balance_sheet"
LOG_PATH = "./logs"
WALLET_PATH = "/home/viraladmin/chatgpt/wallets"
WALLET_PATH = "./wallets"
WALLET_NAME = "contractless.wallet"
[Settings]
LOG_LEVEL = "disabled"
LOG_LEVEL = "info"
PUBLIC_IP = "your_public_ip_address"
LISTEN_IP = "0.0.0.0"
RPC_PORT = "50055"

View File

@ -1,94 +1,94 @@
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::env;
use blockchain::fs;
use blockchain::Duration;
use blockchain::Error;
fn calculate_average_time_between_timestamps(
start_block: u32,
stop_block: u32,
directory: &str,
) -> Result<Option<Duration>, Box<dyn Error>> {
let mut timestamps: Vec<u32> = Vec::new();
// Iterate over all files in the directory
for entry in fs::read_dir(directory)? {
let entry = entry?;
let path = entry.path();
let (
_network_name,
_padded_base_coin,
extension,
_torrentpath,
_wallet_path,
_blockpath,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
if path.is_file() && path.extension().is_some_and(|ext| *ext == *extension) {
if let Some(file_name) = path.file_stem() {
if let Some(file_name_str) = file_name.to_str() {
if let Ok(block_number) = file_name_str.parse::<u32>() {
if block_number >= start_block && block_number <= stop_block {
let file_content = fs::read(&path)?;
if file_content.len() >= 4 {
let timestamp_bytes = &file_content[0..4];
let timestamp = u32::from_le_bytes(timestamp_bytes.try_into()?);
timestamps.push(timestamp);
}
}
}
}
}
}
}
// Sort the timestamps
timestamps.sort();
// Calculate the differences between consecutive timestamps
let mut time_differences: Vec<Duration> = Vec::new();
for i in 1..timestamps.len() {
let time_difference = timestamps[i] - timestamps[i - 1];
time_differences.push(Duration::from_secs(time_difference as u64));
}
// Calculate the average time difference
if !time_differences.is_empty() {
let total_duration: Duration = time_differences.iter().sum();
let average_time = total_duration / (time_differences.len() as u32);
Ok(Some(average_time))
} else {
Ok(None)
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Collect command-line arguments
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
println!("Usage: {} <start_block> <stop_block> <directory>", args[0]);
return Ok(());
}
let start_block: u32 = args[1].parse()?;
let stop_block: u32 = args[2].parse()?;
let directory = &args[3];
match calculate_average_time_between_timestamps(start_block, stop_block, directory)? {
Some(average_time) => {
println!(
"The average time between timestamps is {:.2} seconds.",
average_time.as_secs_f64()
);
}
None => {
println!("Not enough data to calculate an average time difference.");
}
}
Ok(())
}
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::env;
use blockchain::fs;
use blockchain::Duration;
use blockchain::Error;
fn calculate_average_time_between_timestamps(
start_block: u32,
stop_block: u32,
directory: &str,
) -> Result<Option<Duration>, Box<dyn Error>> {
let mut timestamps: Vec<u32> = Vec::new();
// Iterate over all files in the directory
for entry in fs::read_dir(directory)? {
let entry = entry?;
let path = entry.path();
let (
_network_name,
_padded_base_coin,
extension,
_torrentpath,
_wallet_path,
_blockpath,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
if path.is_file() && path.extension().is_some_and(|ext| *ext == *extension) {
if let Some(file_name) = path.file_stem() {
if let Some(file_name_str) = file_name.to_str() {
if let Ok(block_number) = file_name_str.parse::<u32>() {
if block_number >= start_block && block_number <= stop_block {
let file_content = fs::read(&path)?;
if file_content.len() >= 4 {
let timestamp_bytes = &file_content[0..4];
let timestamp = u32::from_le_bytes(timestamp_bytes.try_into()?);
timestamps.push(timestamp);
}
}
}
}
}
}
}
// Sort the timestamps
timestamps.sort();
// Calculate the differences between consecutive timestamps
let mut time_differences: Vec<Duration> = Vec::new();
for i in 1..timestamps.len() {
let time_difference = timestamps[i] - timestamps[i - 1];
time_differences.push(Duration::from_secs(time_difference as u64));
}
// Calculate the average time difference
if !time_differences.is_empty() {
let total_duration: Duration = time_differences.iter().sum();
let average_time = total_duration / (time_differences.len() as u32);
Ok(Some(average_time))
} else {
Ok(None)
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Collect command-line arguments
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
println!("Usage: {} <start_block> <stop_block> <directory>", args[0]);
return Ok(());
}
let start_block: u32 = args[1].parse()?;
let stop_block: u32 = args[2].parse()?;
let directory = &args[3];
match calculate_average_time_between_timestamps(start_block, stop_block, directory)? {
Some(average_time) => {
println!(
"The average time between timestamps is {:.2} seconds.",
average_time.as_secs_f64()
);
}
None => {
println!("Not enough data to calculate an average time difference.");
}
}
Ok(())
}

View File

@ -1,11 +1,11 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::from_slice;
use blockchain::read;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::wallets::structures::{SavedWallet, Wallet};
use blockchain::wallets::structures::SavedWallet;
use blockchain::Path;
use serde_json::Value;
@ -19,7 +19,7 @@ fn display_vanity_address(short_address: &str) -> Option<String> {
Some(format!("{trimmed_payload}.{network_suffix}"))
}
async fn persist_local_vanity_address(encryption_key: &str, tx_json: &str) {
async fn persist_local_vanity_address(encryption_key: &str, tx_json: &str, wallet_path: &str) {
// Only successful vanity transactions update the local saved wallet display field.
let Ok(value) = serde_json::from_str::<Value>(tx_json) else {
return;
@ -41,7 +41,6 @@ async fn persist_local_vanity_address(encryption_key: &str, tx_json: &str) {
};
// Read the saved wallet and only update it if it matches the vanity transaction sender.
let wallet_path = Wallet::get_wallet_path().await;
let wallet_bytes = match read(Path::new(&wallet_path)).await {
Ok(bytes) => bytes,
Err(_) => return,
@ -63,7 +62,7 @@ async fn persist_local_vanity_address(encryption_key: &str, tx_json: &str) {
};
saved_wallet.vanity_address = Some(display_vanity);
saved_wallet.save_the_wallet(Path::new(&wallet_path)).await;
saved_wallet.save_the_wallet(Path::new(wallet_path)).await;
}
#[tokio::main]
@ -81,6 +80,7 @@ async fn main() {
}
};
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
@ -118,7 +118,10 @@ async fn main() {
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;
@ -130,7 +133,7 @@ async fn main() {
println!("{trimmed}");
// When a vanity tx broadcasts successfully, keep the local wallet display in sync.
if trimmed == "successful_broadcast: true" {
persist_local_vanity_address(&encryption_key, &json).await;
persist_local_vanity_address(&encryption_key, &json, &wallet_path).await;
if let Some(hash) = &txid {
println!("{hash}");
}

View File

@ -1,6 +1,7 @@
use blockchain::blocks::burn::{BurnTransaction, UnsignedBurnTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::types::BURN_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
@ -63,6 +64,7 @@ async fn main() {
.parse()
.expect("Please enter a valid fee.");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -71,7 +73,7 @@ async fn main() {
.await;
// Load the wallet that owns the asset being burned.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");

View File

@ -1,8 +1,9 @@
use blockchain::blocks::collateral::{
CollateralClaimTransaction, UnsignedCollateralClaimTransaction,
};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::COLLATERAL_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
@ -35,6 +36,7 @@ async fn main() {
.parse()
.expect("Please enter a valid fee");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -43,7 +45,7 @@ async fn main() {
.await;
// Load the wallet so the transaction can use the saved short address and private key.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");

View File

@ -1,6 +1,7 @@
use blockchain::blocks::issue_token::{IssueTokenTransaction, UnsignedIssueTokenTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::ISSUE_TOKEN_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
@ -49,6 +50,7 @@ async fn main() {
.parse()
.expect("Please enter a valid fee.");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -57,7 +59,7 @@ async fn main() {
.await;
// Load the creator wallet that signs the additional token issuance.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");

View File

@ -1,8 +1,9 @@
use blockchain::blocks::loan_payment::{
ContractPaymentTransaction, UnsignedContractPaymentTransaction,
};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::BORROWER_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
@ -53,6 +54,7 @@ async fn main() {
.parse()
.expect("Please enter a valid fee");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -61,7 +63,7 @@ async fn main() {
.await;
// Load the wallet so the transaction can use the saved short address and private key.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");

View File

@ -1,8 +1,9 @@
use blockchain::blocks::loans::UnsignedLoanContractTransaction;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::LENDER_FEE;
use blockchain::json;
use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::{create_dir_all, AsyncWriteExt};
@ -20,11 +21,6 @@ fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
// Accept local vanity/short input and resolve it into the real short address.
resolve_local_input_short_address(address.trim())
}
fn parse_start_date(input: &str) -> Result<u32, String> {
// Loan start dates are entered as calendar dates and stored as local midnight timestamps.
let date = NaiveDate::parse_from_str(input.trim(), "%Y-%m-%d")
@ -43,6 +39,23 @@ fn parse_start_date(input: &str) -> Result<u32, String> {
Ok(timestamp as u32)
}
async fn load_signing_wallet() -> Option<Wallet> {
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => Some(wallet),
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
None
}
}
}
#[tokio::main]
async fn main() {
// Loan contracts use transaction type 7 and are first signed by the lender.
@ -130,12 +143,18 @@ async fn main() {
}
};
// Load the lender wallet before resolving vanity inputs; remote vanity lookup needs a signed handshake.
let wallet = match load_signing_wallet().await {
Some(wallet) => wallet,
None => return,
};
// Resolve the borrower input before writing it into the loan offer.
let borrower = prompt_visible("What is the wallet address of the borrower? ").await;
let borrower = match normalize_short_address_input(borrower.trim()) {
let borrower = match resolve_wallet_address_input(borrower.trim(), &wallet).await {
Ok(address) => address,
Err(_) => {
println!("borrower wallet invalid");
Err(err) => {
println!("borrower wallet invalid: {err}");
return;
}
};
@ -151,20 +170,6 @@ async fn main() {
.expect("Please enter a valid fee");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
// Load the lender wallet that creates the first loan-contract signature.
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 lender = &wallet.saved.short_address;

View File

@ -1,6 +1,7 @@
use blockchain::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::MARKETING_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
@ -75,6 +76,7 @@ async fn main() {
.parse()
.expect("Please enter a valid value.");
let txfee = ((txfee_f32 as f64) * (100000000_f64)).round() as u64;
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -82,7 +84,7 @@ async fn main() {
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");

View File

@ -1,6 +1,7 @@
use blockchain::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::CREATE_NFT_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
@ -60,6 +61,7 @@ async fn main() {
.parse()
.expect("Please enter a valid value.");
let txfee = ((txfee_f32 as f64) * (100000000_f64)).round() as u64;
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -67,7 +69,7 @@ async fn main() {
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");

View File

@ -1,8 +1,9 @@
use blockchain::blocks::swap::UnsignedSwapTransaction;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::SWAP_FEE;
use blockchain::json;
use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
use blockchain::Duration;
use blockchain::File;
@ -20,10 +21,6 @@ fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
resolve_local_input_short_address(address.trim())
}
#[tokio::main]
async fn main() {
// set type and timestampe
@ -75,14 +72,30 @@ async fn main() {
.expect("Please enter a valid age");
let value2 = (value2_f64 * (100000000_f64)).round() as u64;
let wallet_path = prompt_wallet_path().await;
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, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
// get user input swapper address
let sender2 =
prompt_visible("Please enter the wallet address of the account you are swapping with: ")
.await;
let sender2 = match normalize_short_address_input(&sender2) {
let sender2 = match resolve_wallet_address_input(&sender2, &wallet).await {
Ok(address) => address,
Err(_) => {
println!("reciver wallet is not valid");
Err(err) => {
println!("receiver wallet is not valid: {err}");
return;
}
};
@ -135,20 +148,6 @@ async fn main() {
let expiration_time: u32 = expiration_duration.as_secs() as u32;
let expires = timestamp + expiration_time;
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;

View File

@ -1,6 +1,7 @@
use blockchain::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::CREATE_TOKEN_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
@ -61,6 +62,7 @@ async fn main() {
.parse()
.expect("Please enter a valid value.");
let txfee = ((txfee_f32 as f64) * (100000000_f64)).round() as u64;
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -68,7 +70,7 @@ async fn main() {
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");

View File

@ -1,10 +1,11 @@
use blockchain::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::types::{NON_BASE_TRANSFER_MIN_FEE, TRANSFER_FEE};
use blockchain::env;
use blockchain::json;
use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
@ -21,10 +22,6 @@ fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
resolve_local_input_short_address(address.trim())
}
#[tokio::main]
async fn main() {
let minimum_transfer_fee_percent = TRANSFER_FEE * 100.0;
@ -60,15 +57,70 @@ async fn main() {
.expect("Please enter a valid amount.");
let value = ((value_f32 as f64) * 100_000_000.0).round() as u64;
let wallet_path = prompt_wallet_path().await;
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, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let receiver_input = if args.len() > 3 {
args[3].clone()
} else {
prompt_visible("Please enter the receiver wallet address: ").await
};
let receiver = match normalize_short_address_input(&receiver_input) {
let receiver = match resolve_wallet_address_input(&receiver_input, &wallet).await {
Ok(address) => address,
Err(_) => {
println!("reciver wallet is not valid");
Err(err) => {
let trimmed_receiver = receiver_input.trim();
let escaped_receiver = receiver_input
.chars()
.flat_map(char::escape_default)
.collect::<String>();
let escaped_trimmed_receiver = trimmed_receiver
.chars()
.flat_map(char::escape_default)
.collect::<String>();
let dot_count = trimmed_receiver.matches('.').count();
let (payload_chars, payload_bytes, suffix, payload_is_hex, payload_is_ascii) =
match trimmed_receiver.rsplit_once('.') {
Some((payload, suffix)) => (
payload.chars().count(),
payload.len(),
suffix,
payload.chars().all(|ch| ch.is_ascii_hexdigit()),
payload.is_ascii(),
),
None => (0, 0, "<missing>", false, trimmed_receiver.is_ascii()),
};
println!("receiver wallet is not valid: {err}");
println!(
"receiver diagnostics: raw_chars={} raw_bytes={} trimmed_chars={} trimmed_bytes={} dot_count={} suffix={} payload_chars={} payload_bytes={} payload_hex={} payload_ascii={} raw_ascii={} raw_control_chars={}",
receiver_input.chars().count(),
receiver_input.len(),
trimmed_receiver.chars().count(),
trimmed_receiver.len(),
dot_count,
suffix,
payload_chars,
payload_bytes,
payload_is_hex,
payload_is_ascii,
receiver_input.is_ascii(),
receiver_input.chars().any(char::is_control)
);
println!("receiver escaped raw: {escaped_receiver}");
println!("receiver escaped trimmed: {escaped_trimmed_receiver}");
return;
}
};
@ -107,20 +159,6 @@ async fn main() {
.parse()
.expect("Please enter a valid NFT series number.");
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 short_address = &wallet.saved.short_address;

View File

@ -1,6 +1,7 @@
use blockchain::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::{VANITY_ADDRESS_FEE, VANITY_ADDRESS_TYPE};
use blockchain::env;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
@ -70,6 +71,7 @@ async fn main() {
.parse()
.expect("Please enter a valid fee.");
let txfee = ((txfee_f32 as f64) * 100_000_000.0).round() as u64;
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -78,7 +80,7 @@ async fn main() {
.await;
// Load the wallet that will own and sign the vanity registration.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -24,6 +24,7 @@ async fn main() {
println!("Please enter a valid 64-character block hash");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -44,7 +45,10 @@ async fn main() {
socket_address,
block_hash.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -26,6 +26,7 @@ async fn main() {
return;
}
};
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -48,7 +49,10 @@ async fn main() {
socket_address,
payload.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,9 +1,11 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::encode;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
#[tokio::main]
async fn main() {
@ -25,12 +27,26 @@ async fn main() {
return;
}
};
let wallet_path = prompt_wallet_path().await;
let encryption_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(encryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let wallet_address = match resolve_wallet_address_input(&wallet_address, &wallet).await {
Ok(address) => address,
Err(err) => {
eprintln!("wallet address is not valid: {err}");
return;
}
};
// Try each configured peer until one returns a response.
let connections = get_connections().await;
@ -46,7 +62,10 @@ async fn main() {
socket_address,
wallet_address.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletParts {
public_key: wallet.saved.public_key.clone(),
private_key: wallet.saved.private_key.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::encode;
use blockchain::env;
@ -25,6 +25,7 @@ async fn main() {
return;
}
};
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -46,7 +47,10 @@ async fn main() {
socket_address,
contract_hash.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -10,6 +10,7 @@ async fn main() {
let hashmap_key = generate_uid();
let _args: Vec<String> = env::args().collect();
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
@ -32,7 +33,10 @@ async fn main() {
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -16,6 +16,7 @@ async fn main() {
println!("Usage: ./request_height");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -40,7 +41,10 @@ async fn main() {
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -22,6 +22,7 @@ async fn main() {
println!("Usage: ./large_tx_fee");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -46,7 +47,10 @@ async fn main() {
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -9,7 +9,8 @@ use blockchain::blocks::swap::SwapTransaction;
use blockchain::blocks::token::CreateTokenTransaction;
use blockchain::blocks::transfer::TransferTransaction;
use blockchain::blocks::vanity::VanityAddressTransaction;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE,
@ -19,7 +20,9 @@ use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::rpc::command_maps;
use blockchain::standalone_tools::connections::handshake;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::to_string_pretty;
use blockchain::wallets::structures::Wallet;
async fn decode_one_transaction(tx_bytes: &[u8]) -> Option<String> {
let txtype = *tx_bytes.first()?;
@ -106,12 +109,26 @@ async fn main() {
return;
}
let address = args[1].trim().to_string();
let wallet_path = prompt_wallet_path().await;
let encryption_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(encryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let address = match resolve_wallet_address_input(&address, &wallet).await {
Ok(address) => address,
Err(err) => {
eprintln!("wallet address is not valid: {err}");
return;
}
};
let connections = get_connections().await;
let mut connected = false;
@ -126,7 +143,10 @@ async fn main() {
socket_address,
address.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletParts {
public_key: wallet.saved.public_key.clone(),
private_key: wallet.saved.private_key.clone(),
},
generate_uid(),
)
.await;

View File

@ -9,7 +9,8 @@ use blockchain::blocks::swap::SwapTransaction;
use blockchain::blocks::token::CreateTokenTransaction;
use blockchain::blocks::transfer::TransferTransaction;
use blockchain::blocks::vanity::VanityAddressTransaction;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE,
@ -87,6 +88,7 @@ async fn main() {
return;
}
let signature = args[1].trim().to_string();
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -107,7 +109,10 @@ async fn main() {
socket_address,
signature.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
generate_uid(),
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -15,6 +15,7 @@ async fn main() {
println!("Usage: ./lookup_mempool_tx_count");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -36,7 +37,10 @@ async fn main() {
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
generate_uid(),
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
@ -86,6 +86,7 @@ async fn main() {
println!("Usage: ./lookup_network_info");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -108,7 +109,10 @@ async fn main() {
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,5 +1,6 @@
use blockchain::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::encode;
use blockchain::env;
@ -181,6 +182,7 @@ async fn main() {
let nft_name = args[1].clone();
let item_number = args[2].clone();
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
@ -203,7 +205,10 @@ async fn main() {
socket_address,
payload.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,5 +1,6 @@
use blockchain::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
@ -65,6 +66,7 @@ async fn main() {
println!("Usage: ./nft_list");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -85,7 +87,10 @@ async fn main() {
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -16,6 +16,7 @@ async fn main() {
println!("Usage: ./lookup_node_time");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -37,7 +38,10 @@ async fn main() {
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
generate_uid(),
)
.await;

View File

@ -1,11 +1,13 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::from_str;
use blockchain::read_to_string;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::tilde;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
@ -31,13 +33,12 @@ fn extract_address(contents: &str) -> Result<String, String> {
}
if trimmed.starts_with('{') {
// Prefer short_address for balance lookups, but accept long_address too.
let value: Value =
from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?;
let address = value
.get("short_address")
.and_then(|v| v.as_str())
.or_else(|| value.get("long_address").and_then(|v| v.as_str()))
.or_else(|| value.get("vanity_address").and_then(|v| v.as_str()))
.ok_or_else(|| "Wallet JSON does not contain a usable address field".to_string())?;
return Ok(address.trim().to_string());
}
@ -110,12 +111,26 @@ async fn main() {
return;
}
};
let wallet_path = prompt_wallet_path().await;
let encryption_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(encryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let wallet_address = match resolve_wallet_address_input(&wallet_address, &wallet).await {
Ok(address) => address,
Err(err) => {
eprintln!("wallet address is not valid: {err}");
return;
}
};
let json = wallet_address;
// Try each configured peer until one returns a parsable balance response or text error.
@ -131,7 +146,10 @@ async fn main() {
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletParts {
public_key: wallet.saved.public_key.clone(),
private_key: wallet.saved.private_key.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,5 +1,6 @@
use blockchain::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
@ -36,6 +37,7 @@ async fn main() {
}
let token_name = args[1].clone();
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
@ -56,7 +58,10 @@ async fn main() {
socket_address,
token_name.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,5 +1,6 @@
use blockchain::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
@ -55,6 +56,7 @@ async fn main() {
println!("Usage: ./token_list");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -75,7 +77,10 @@ async fn main() {
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -32,6 +32,7 @@ async fn main() {
};
// Extract the encryption key from the command-line arguments
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
@ -52,7 +53,10 @@ async fn main() {
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE,
@ -20,6 +20,7 @@ async fn main() {
println!("Usage: ./total_transactions");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -46,7 +47,10 @@ async fn main() {
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -9,7 +9,8 @@ use blockchain::blocks::swap::SwapTransaction;
use blockchain::blocks::token::CreateTokenTransaction;
use blockchain::blocks::transfer::TransferTransaction;
use blockchain::blocks::vanity::VanityAddressTransaction;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
BORROWER_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, GENESIS_TYPE, LENDER_TYPE,
@ -46,6 +47,7 @@ async fn main() {
};
// Extract the encryption ley from the command-line argument
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
@ -68,7 +70,10 @@ async fn main() {
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -36,7 +36,7 @@ fn display_vanity_address(short_address: &str) -> Option<String> {
async fn lookup_registered_vanity_address(
short_address: &str,
long_address: &str,
public_key: &str,
private_key: &str,
) -> Option<String> {
let hashmap_key = generate_uid();
@ -53,7 +53,7 @@ async fn lookup_registered_vanity_address(
short_address.to_string(),
rpc_command,
handshake::HandshakeWallet::WalletParts {
long_address: long_address.to_string(),
public_key: public_key.to_string(),
private_key: private_key.to_string(),
},
hashmap_key,
@ -209,17 +209,14 @@ async fn main() {
match Wallet::regenerate_public_key(&private_key) {
Ok(public_key_bytes) => {
let public_key = blockchain::encode(public_key_bytes.clone());
let long_address = Wallet::generate_address(&public_key);
let long_address_bytes = Wallet::long_address_to_bytes(long_address.clone());
let short_address_bytes =
Wallet::long_address_bytes_to_short_address_bytes(&long_address_bytes)
Wallet::public_key_bytes_to_short_address_bytes(&public_key_bytes)
.expect("Failed to derive short address bytes");
let short_address = Wallet::bytes_to_short_address(&short_address_bytes)
.expect("Failed to encode short address");
let vanity_address =
lookup_registered_vanity_address(&short_address, &long_address, &private_key).await;
lookup_registered_vanity_address(&short_address, &public_key, &private_key).await;
let saved_wallet = SavedWallet {
long_address,
short_address,
vanity_address,
public_key,

View File

@ -34,7 +34,7 @@ fn display_vanity_address(short_address: &str) -> Option<String> {
async fn lookup_registered_vanity_address(
short_address: &str,
long_address: &str,
public_key: &str,
private_key: &str,
) -> Option<String> {
let hashmap_key = generate_uid();
@ -51,7 +51,7 @@ async fn lookup_registered_vanity_address(
short_address.to_string(),
rpc_command,
handshake::HandshakeWallet::WalletParts {
long_address: long_address.to_string(),
public_key: public_key.to_string(),
private_key: private_key.to_string(),
},
hashmap_key,
@ -246,17 +246,14 @@ async fn main() {
match Wallet::regenerate_public_key(&private_key) {
Ok(public_key_bytes) => {
let public_key = blockchain::encode(public_key_bytes.clone());
let long_address = Wallet::generate_address(&public_key);
let long_address_bytes = Wallet::long_address_to_bytes(long_address.clone());
let short_address_bytes =
Wallet::long_address_bytes_to_short_address_bytes(&long_address_bytes)
Wallet::public_key_bytes_to_short_address_bytes(&public_key_bytes)
.expect("Failed to derive short address bytes");
let short_address = Wallet::bytes_to_short_address(&short_address_bytes)
.expect("Failed to encode short address");
let vanity_address =
lookup_registered_vanity_address(&short_address, &long_address, &private_key).await;
lookup_registered_vanity_address(&short_address, &public_key, &private_key).await;
let saved_wallet = SavedWallet {
long_address,
short_address,
vanity_address,
public_key,

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::common::skein::skein_256_hash_bytes;
use blockchain::env;
@ -18,6 +18,7 @@ async fn main() {
println!("Usage: ./register_wallet");
return;
}
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -26,7 +27,7 @@ async fn main() {
.await;
// Load the wallet so both address forms and the signing key come from the same saved file.
let wallet = match Wallet::try_obtain_wallet(decryption_key.clone(), None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key.clone(), Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
@ -34,9 +35,8 @@ async fn main() {
}
};
// The peer receives both addresses, but the signature proves they belong together.
// The peer receives the short address and raw public key; the signature proves they belong together.
let short_address = wallet.saved.short_address.clone();
let long_address = wallet.saved.long_address.clone();
let short_address_bytes = match Wallet::short_address_to_bytes(&short_address) {
Some(bytes) => bytes,
None => {
@ -44,20 +44,28 @@ async fn main() {
return;
}
};
let long_address_bytes = Wallet::long_address_to_bytes(long_address.clone());
let public_key_bytes = match Wallet::normalize_saved_public_key_bytes(&wallet.saved.public_key)
{
Some(bytes) => bytes,
None => {
eprintln!("Failed to decode public key from the wallet.");
return;
}
};
// The signed payload mirrors the binary request body: command byte, short address, long address.
// The signed payload mirrors the binary request body: command byte, short address, public key.
let mut signed_payload =
Vec::with_capacity(1 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::ADDRESS_BYTES_LENGTH);
Vec::with_capacity(1 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::PUBLIC_KEY_LENGTH);
signed_payload.push(RPC_REGISTER_WALLET);
signed_payload.extend_from_slice(&short_address_bytes);
signed_payload.extend_from_slice(&long_address_bytes);
signed_payload.extend_from_slice(&public_key_bytes);
let payload_hash = skein_256_hash_bytes(&signed_payload);
let signature = Wallet::sign_transaction(&payload_hash, &wallet.saved.private_key).await;
// sending_request encodes command 38 from this pipe-delimited payload.
let json = format!("{short_address}|{long_address}|{signature}");
let public_key_hex = blockchain::encode(&public_key_bytes);
let json = format!("{short_address}|{public_key_hex}|{signature}");
let rpc_command = 38;
let connections = get_connections().await;
@ -73,7 +81,10 @@ async fn main() {
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(decryption_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: decryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -17,6 +17,7 @@ async fn main() {
return;
}
let ip = args[1].trim().to_string();
let wallet_path = prompt_wallet_path().await;
let wallet_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -25,7 +26,7 @@ async fn main() {
.await;
// Server-side verification expects a signature over the exact IP string.
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
@ -48,7 +49,10 @@ async fn main() {
socket_address,
payload.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(wallet_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: wallet_key.clone(),
wallet_path: wallet_path.clone(),
},
generate_uid(),
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
@ -17,6 +17,7 @@ async fn main() {
return;
}
let ip = args[1].trim().to_string();
let wallet_path = prompt_wallet_path().await;
let wallet_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -25,7 +26,7 @@ async fn main() {
.await;
// Server-side verification expects a signature over the exact IP string.
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
@ -48,7 +49,10 @@ async fn main() {
socket_address,
payload.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(wallet_key.clone()),
handshake::HandshakeWallet::WalletKey {
encryption_key: wallet_key.clone(),
wallet_path: wallet_path.clone(),
},
generate_uid(),
)
.await;

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::skein::skein_256_hash_data;
use blockchain::env;
use blockchain::wallets::structures::Wallet;
@ -20,6 +20,7 @@ async fn main() {
return;
}
};
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
@ -28,7 +29,7 @@ async fn main() {
.await;
// Load the wallet whose private key will create the detached signature.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");

View File

@ -1,56 +1,56 @@
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::env;
use blockchain::exit;
use blockchain::io;
use blockchain::to_string_pretty;
use blockchain::torrent::structs::Torrent;
use blockchain::AsyncReadExt;
use blockchain::File;
use blockchain::Path;
#[tokio::main]
async fn main() -> io::Result<()> {
// This utility loads one local torrent file by block number and prints the decoded struct.
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <block_number>", args[0]);
exit(1);
}
let block_number = &args[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();
// Torrent files are scoped by the active network path returned from settings.
let torrent_filename = format!("{torrent_path}/{block_number}.torrent");
if !Path::new(&torrent_filename).exists() {
eprintln!("Torrent file not found: {torrent_filename}");
return Ok(());
}
let mut torrent_file = File::open(&torrent_filename).await?;
let mut torrent_contents = Vec::new();
torrent_file.read_to_end(&mut torrent_contents).await?;
// The torrent parser validates the binary layout before the result is printed as JSON.
match Torrent::from_bytes(&torrent_contents).await {
Ok(torrent) => {
let json_pretty = to_string_pretty(&torrent)?;
println!("{json_pretty}");
}
Err(e) => {
eprintln!("Failed to parse torrent: {e}");
}
}
Ok(())
}
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::env;
use blockchain::exit;
use blockchain::io;
use blockchain::to_string_pretty;
use blockchain::torrent::structs::Torrent;
use blockchain::AsyncReadExt;
use blockchain::File;
use blockchain::Path;
#[tokio::main]
async fn main() -> io::Result<()> {
// This utility loads one local torrent file by block number and prints the decoded struct.
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <block_number>", args[0]);
exit(1);
}
let block_number = &args[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();
// Torrent files are scoped by the active network path returned from settings.
let torrent_filename = format!("{torrent_path}/{block_number}.torrent");
if !Path::new(&torrent_filename).exists() {
eprintln!("Torrent file not found: {torrent_filename}");
return Ok(());
}
let mut torrent_file = File::open(&torrent_filename).await?;
let mut torrent_contents = Vec::new();
torrent_file.read_to_end(&mut torrent_contents).await?;
// The torrent parser validates the binary layout before the result is printed as JSON.
match Torrent::from_bytes(&torrent_contents).await {
Ok(torrent) => {
let json_pretty = to_string_pretty(&torrent)?;
println!("{json_pretty}");
}
Err(e) => {
eprintln!("Failed to parse torrent: {e}");
}
}
Ok(())
}

View File

@ -30,13 +30,12 @@ fn extract_address(contents: &str) -> Result<String, String> {
}
if trimmed.starts_with('{') {
// Prefer the short address when present, but accept long_address for wallet validation too.
// Wallet JSON address validation uses the canonical short address.
let value: Value =
from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?;
let address = value
.get("short_address")
.and_then(|v| v.as_str())
.or_else(|| value.get("long_address").and_then(|v| v.as_str()))
.ok_or_else(|| "Wallet JSON does not contain a usable address field".to_string())?;
return Ok(address.trim().to_string());
}
@ -100,9 +99,7 @@ async fn main() {
}
};
// Verify either a long wallet address or a current-network short address.
let message_hash = Wallet::wallet_validation(address.trim()).await
|| Wallet::short_address_validation(address.trim());
let message_hash = Wallet::short_address_validation(address.trim());
println!("{message_hash}");
}

View File

@ -1,8 +1,11 @@
use blockchain::common::cli_prompts::prompt_visible;
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::skein::skein_256_hash_data;
use blockchain::env;
use blockchain::from_str;
use blockchain::read_to_string;
use blockchain::records::wallet_registry::resolve_pubkey_from_short_address;
use blockchain::sled;
use blockchain::tilde;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
@ -41,13 +44,12 @@ fn extract_address(contents: &str) -> Result<String, String> {
}
if trimmed.starts_with('{') {
// Signature verification prefers the long address but can also verify with a short address.
// Signature verification uses the canonical short address.
let value: Value =
from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?;
let address = value
.get("long_address")
.get("short_address")
.and_then(|v| v.as_str())
.or_else(|| value.get("short_address").and_then(|v| v.as_str()))
.ok_or_else(|| "Wallet JSON does not contain a usable address field".to_string())?;
return Ok(address.trim().to_string());
}
@ -140,7 +142,31 @@ async fn main() {
// Hash the message exactly as sign_message does before verifying the wallet signature.
let message_hash = skein_256_hash_data(message.as_str());
let signature = Wallet::verify_transaction(&message_hash, &signature, address.trim()).await;
let (
_network_name,
_padded_base_coin,
_suffix,
_torrent_path,
_wallet_path,
_block_path,
db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
let db = match sled::open(&db_path) {
Ok(db) => db,
Err(err) => {
eprintln!("Failed to open wallet registry database: {err}");
return;
}
};
let signature = match resolve_pubkey_from_short_address(&db, address.trim()) {
Ok(Some(pubkey)) => {
Wallet::verify_transaction_with_public_key_bytes(&message_hash, &signature, &pubkey)
.await
}
_ => false,
};
if signature {
println!("valid signature");

View File

@ -1,11 +1,14 @@
use blockchain::blocks::loans::UnsignedLoanContractTransaction;
use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty};
use blockchain::common::cli_prompts::{
ask_yes_no_question, prompt_hidden_nonempty, prompt_wallet_path,
};
use blockchain::env;
use blockchain::from_str;
use blockchain::fs;
use blockchain::json;
use blockchain::read_to_string;
use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::to_string_pretty;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
@ -34,11 +37,6 @@ fn display_start_date(timestamp: u32) -> String {
}
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
// Accept local vanity/short input and resolve it into the real short address.
resolve_local_input_short_address(address.trim())
}
#[tokio::main]
async fn main() {
// Borrowers use this tool to review a lender-signed loan and add signature2.
@ -67,27 +65,48 @@ async fn main() {
}
};
// Load the borrower wallet before resolving vanity inputs; remote vanity lookup needs a signed handshake.
let wallet_path = prompt_wallet_path().await;
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, Some(&wallet_path)).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;
// Extract every field that participates in the hash so the borrower signs the same bytes.
let txtype = 7;
let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32;
let loan_coin = json["loan_coin"].as_str().unwrap_or_default().to_string();
let loan_amount = json["loan_amount"].as_u64().unwrap_or_default();
let lender = match normalize_short_address_input(json["lender"].as_str().unwrap_or_default()) {
Ok(address) => address,
Err(_) => {
println!("Transaction is not valid. Lender address is not a valid short address.");
return;
}
};
let lender =
match resolve_wallet_address_input(json["lender"].as_str().unwrap_or_default(), &wallet)
.await
{
Ok(address) => address,
Err(err) => {
println!("Transaction is not valid. Lender address is not valid: {err}");
return;
}
};
let collateral = json["collateral"].as_str().unwrap_or_default().to_string();
let collateral_amount = json["collateral_amount"].as_u64().unwrap_or_default();
let borrower =
match normalize_short_address_input(json["borrower"].as_str().unwrap_or_default()) {
match resolve_wallet_address_input(json["borrower"].as_str().unwrap_or_default(), &wallet)
.await
{
Ok(address) => address,
Err(_) => {
println!(
"Transaction is not valid. Borrower address is not a valid short address."
);
Err(err) => {
println!("Transaction is not valid. Borrower address is not valid: {err}");
return;
}
};
@ -176,23 +195,6 @@ async fn main() {
return;
}
// Load the borrower wallet and ensure it matches the borrower address in the offer.
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;
if borrower.trim() != address.trim() {
println!(
"Transaction is not valid for your wallet address. Expected {borrower} found {address}"

View File

@ -1,11 +1,14 @@
use blockchain::blocks::swap::UnsignedSwapTransaction;
use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty};
use blockchain::common::cli_prompts::{
ask_yes_no_question, prompt_hidden_nonempty, prompt_wallet_path,
};
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::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
@ -16,10 +19,6 @@ fn pad_to_width(input: &str, width: usize) -> String {
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
@ -29,13 +28,14 @@ async fn main() {
return;
}
let filename = &args[1];
let wallet_path = prompt_wallet_path().await;
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 {
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
@ -92,28 +92,32 @@ async fn main() {
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 sender1 =
match resolve_wallet_address_input(json["sender1"].as_str().unwrap_or_default(), &wallet)
.await
{
Ok(address) => address,
Err(err) => {
println!("sender1 wallet invalid: {err}");
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;
}
};
let sender2 =
match resolve_wallet_address_input(json["sender2"].as_str().unwrap_or_default(), &wallet)
.await
{
Ok(address) => address,
Err(err) => {
println!("sender2 wallet invalid: {err}");
return;
}
};
// ensure wallet and sender2 match
if sender2 != address.trim() {

View File

@ -1,16 +1,21 @@
use crate::common::skein::{skein_256_hash_data, skein_512_hash_data};
use crate::common::types::Transaction;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::averages::{calculate_averages, update_block_data};
use crate::records::memory::averages::asert_genesis_anchor;
use crate::records::memory::chain_state::cached_chain_height;
use crate::sled::Db;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Duration;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
const TARGET_BLOCK_SECONDS: i128 = 15;
const ASERT_HALF_LIFE_SECONDS: i128 = 300;
const ASERT_RADIX_BITS: i128 = 16;
const ASERT_FIXED_ONE: i128 = 1 << ASERT_RADIX_BITS;
pub const TIMESTAMP_OFFSET: usize = 0;
pub const MINER_OFFSET: usize = TIMESTAMP_OFFSET + 4;
pub const PREVIOUS_HASH_OFFSET: usize = MINER_OFFSET + Wallet::SHORT_ADDRESS_BYTES_LENGTH;
@ -99,15 +104,11 @@ impl UnminedBlock {
a ^ b ^ c ^ d
}
pub async fn vrf_generate(self, wallet_key: String) -> VrfBlock {
pub async fn vrf_generate(self, private_key: &str) -> VrfBlock {
// Sign the unmined header hash with the miner wallet and derive
// the VRF number from that signature.
let hash = self.hash().await;
let wallet = Wallet::try_obtain_wallet(wallet_key, None)
.await
.unwrap_or_else(|err| panic!("Wallet decryption failed: {err}"));
let privkey = &wallet.saved.private_key;
let proof = Wallet::sign_transaction(&hash, privkey).await;
let proof = Wallet::sign_transaction(&hash, private_key).await;
let vrf = Self::generate_random_number(&proof).await;
VrfBlock {
unmined_block: self,
@ -122,52 +123,82 @@ impl UnminedBlock {
skein_256_hash_data(&serialized)
}
// Calculate the next difficulty using the rolling average and target block time.
fn calculate_new_difficulty(
current_difficulty: u64,
difficulty_average: u64,
average_duration: Duration,
) -> u64 {
let lower_bound = Duration::from_secs(14);
let upper_bound = Duration::from_secs(16);
fn asert_target(anchor_target: u64, height_delta: u32, time_delta: i128) -> u64 {
// Deterministic fixed-point ASERT calculation. The polynomial
// approximates 2^x without platform-dependent floats.
let expected_time = height_delta as i128 * TARGET_BLOCK_SECONDS;
let time_error = time_delta - expected_time;
let exponent = (time_error << ASERT_RADIX_BITS) / ASERT_HALF_LIFE_SECONDS;
let shifts = exponent >> ASERT_RADIX_BITS;
let frac = exponent - (shifts << ASERT_RADIX_BITS);
// When the rolling average is already within the target window,
// use the cached mean difficulty exactly.
if difficulty_average > 0
&& average_duration >= lower_bound
&& average_duration <= upper_bound
{
return difficulty_average;
}
let factor = ASERT_FIXED_ONE
+ ((195_766_423_245_049_i128 * frac
+ 971_821_376_i128 * frac * frac
+ 5_127_i128 * frac * frac * frac
+ (1_i128 << 47))
>> 48);
// Outside the target window, apply the capped 30% adjustment
// with integer math to keep the result stable.
let adjustment = current_difficulty.saturating_mul(30).saturating_div(100);
if average_duration > upper_bound {
current_difficulty.saturating_add(adjustment)
} else if average_duration < lower_bound {
current_difficulty.saturating_sub(adjustment)
let mut target = anchor_target as u128 * factor.max(1) as u128;
if shifts >= 0 {
if shifts >= 64 {
return u64::MAX;
}
target = target.checked_shl(shifts as u32).unwrap_or(u128::MAX);
} else {
current_difficulty
let right_shift = (-shifts) as u32;
if right_shift >= 128 {
return 1;
}
target >>= right_shift;
}
target >>= ASERT_RADIX_BITS as u32;
target.clamp(1, u64::MAX as u128) as u64
}
// Adjust difficulty based on the latest saved block averages.
fn clamp_per_block(raw_target: u64, current_difficulty: u64) -> u64 {
// ASERT provides the direction and scale, while this guard keeps any
// single block from swinging the threshold too far.
let lower_bound = current_difficulty
.saturating_mul(85)
.saturating_div(100)
.max(1);
let upper_bound = current_difficulty
.saturating_mul(115)
.saturating_div(100)
.max(lower_bound);
raw_target.clamp(lower_bound, upper_bound)
}
// Adjust difficulty based on ASERT drift from the genesis anchor.
pub async fn adjust_difficulty(
current_timestamp: u32,
db: &Db,
current_difficulty: u64,
) -> u64 {
let block_number = get_height(db);
let block_number = cached_chain_height()
.await
.unwrap_or_else(|| get_height(db));
let candidate_height = block_number + 1;
// Refresh rolling block data before reading averages.
update_block_data(block_number).await;
let Some((anchor_height, anchor_timestamp, anchor_difficulty)) =
asert_genesis_anchor().await
else {
return current_difficulty;
};
// Get the current rolling difficulty and duration averages.
let (difficulty_average, average_duration) = calculate_averages(current_timestamp).await;
if anchor_height >= candidate_height {
return current_difficulty;
}
// Apply the bounded difficulty adjustment.
Self::calculate_new_difficulty(current_difficulty, difficulty_average, average_duration)
let height_delta = candidate_height - anchor_height;
let time_delta = current_timestamp as i128 - anchor_timestamp as i128;
let raw_target = Self::asert_target(anchor_difficulty, height_delta, time_delta);
Self::clamp_per_block(raw_target, current_difficulty)
}
}

View File

@ -1,6 +1,6 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -181,7 +181,8 @@ impl BurnTransaction {
let hash = &self.unsigned_burn.hash().await;
let signature = &self.signature;
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,5 +1,5 @@
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -176,7 +176,8 @@ impl CollateralClaimTransaction {
let signature = &self.signature;
// Collateral-claim transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,6 +1,6 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -182,7 +182,8 @@ impl IssueTokenTransaction {
let signature = &self.signature;
// Issue-token transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,5 +1,5 @@
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -208,7 +208,8 @@ impl ContractPaymentTransaction {
let signature = &self.signature;
// Loan-payment transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,6 +1,6 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -326,7 +326,8 @@ impl LoanContractTransaction {
let signature2 = &self.signature2;
// Loan contracts remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,6 +1,6 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -253,7 +253,8 @@ impl MarketingTransaction {
let signature = &self.signature;
// Marketing transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,6 +1,6 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -230,7 +230,8 @@ impl CreateNftTransaction {
let signature = &self.signature;
// NFT-creation transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,6 +1,6 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -327,7 +327,8 @@ impl SwapTransaction {
let signature2 = &self.signature2;
// Swap transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,6 +1,6 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -204,7 +204,8 @@ impl CreateTokenTransaction {
let signature = &self.signature;
// Token-creation transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,6 +1,6 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -226,7 +226,8 @@ impl TransferTransaction {
let signature = &self.signature;
// Transfer transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,5 +1,5 @@
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::records::memory::mempool::db_client;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
@ -182,10 +182,8 @@ impl VanityAddressTransaction {
// Vanity transactions are written to the vanity mempool table
// until the transaction is mined or removed.
let client = DB.get().ok_or_else(|| {
Box::new(std::io::Error::other("DB not initialized"))
as Box<dyn std::error::Error + Send + Sync>
})?;
let client_handle = db_client().await?;
let client = client_handle.as_ref();
client
.execute(

View File

@ -1,8 +1,52 @@
use crate::read_password;
use crate::stdout;
use crate::AsyncWriteExt;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
use rustyline_derive::Completer;
use rustyline_derive::Helper as RustyHelper;
use rustyline_derive::Highlighter as RustyHighlighter;
use rustyline_derive::Hinter as RustyHinter;
use rustyline_derive::Validator as RustyValidator;
use tokio::io::{stdin, AsyncBufReadExt, BufReader};
#[derive(RustyHelper, Completer, RustyHighlighter, RustyHinter, RustyValidator)]
struct PathHelper {
#[rustyline(Completer)]
completer: FilenameCompleter,
}
pub fn prompt_path(prompt: &str) -> Result<String, String> {
// Rustyline gives CLI path prompts filesystem completion, including tab completion.
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(value) => Ok(value.trim().to_string()),
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(err.to_string()),
}
}
pub async fn prompt_wallet_path() -> String {
loop {
match prompt_path("Please enter the path to your wallet file: ") {
Ok(path) if !path.trim().is_empty() => return path,
Ok(_) => println!("Wallet path cannot be empty. Please try again."),
Err(err) => println!("Failed to read wallet path: {err}"),
}
}
}
pub async fn prompt_visible_with_default(prompt: &str, default: &str) -> String {
// Show the default in brackets so pressing enter keeps the existing value.
let full_prompt = format!("{prompt} [{default}]: ");

View File

@ -2,13 +2,14 @@ use blockchain::exit;
use blockchain::log::{error, logger};
use blockchain::startup::daemonize::daemonize_after_wallet_prompt;
use blockchain::startup::daemonize::handle_control_command;
use blockchain::startup::initialize_startup::obtain_startup_wallet_key;
use blockchain::startup::initialize_startup::obtain_startup_wallet;
use blockchain::startup::initialize_startup::prepare_pre_wallet_startup;
use blockchain::startup::node_runtime::initialize_node_logging;
use blockchain::startup::node_runtime::install_panic_cleanup;
use blockchain::startup::node_runtime::run_unlocked_node;
use blockchain::startup::windows_service::handle_windows_service_command;
use blockchain::startup::windows_service::try_run_as_windows_service;
use blockchain::Arc;
use blockchain::Runtime;
use tokio::runtime::Builder;
@ -52,7 +53,7 @@ fn main() {
.build()
.expect("Failed to create startup runtime");
startup_runtime.block_on(prepare_pre_wallet_startup());
let wallet_key = startup_runtime.block_on(obtain_startup_wallet_key());
let wallet = startup_runtime.block_on(obtain_startup_wallet());
drop(startup_runtime);
// Linux detaches after the wallet prompt unless --foreground is supplied.
@ -74,7 +75,7 @@ fn main() {
}
});
install_panic_cleanup();
if let Err(e) = runtime.block_on(run_unlocked_node(wallet_key, true)) {
if let Err(e) = runtime.block_on(run_unlocked_node(Arc::new(wallet), true)) {
error!("Failed to start unlocked node runtime: {e}");
logger().flush();
eprintln!("Failed to start unlocked node runtime: {e}");

View File

@ -1,8 +1,8 @@
use crate::blocks::rewards::{RewardsTransaction, UnsignedRewardsTransaction};
use crate::common::types::BLOCKS_PER_HALVING;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::network_mapping::NodeInfo;
use crate::sled::Db;
const REWARD_MATURITY_BLOCKS: u8 = 100;
pub async fn calculate_block_reward(block_height: u32) -> u64 {
// Apply the fixed halving schedule based on the block
@ -23,26 +23,22 @@ pub async fn calculate_block_reward(block_height: u32) -> u64 {
reward
}
pub async fn create_rewards_transaction(
short_address: &str,
timestamp: u32,
db: &Db,
) -> RewardsTransaction {
pub async fn reward_value_for_miner_at_height(short_address: &str, block_height: u32) -> u64 {
// New miners must first prove participation before receiving the
// block subsidy. The mined count is maintained in the network map and
// rebuilt from headers only during startup/recovery/reorg correction.
if NodeInfo::get_mined_count(short_address).await < REWARD_MATURITY_BLOCKS {
return 0_u64;
}
calculate_block_reward(block_height).await
}
pub async fn create_rewards_transaction(timestamp: u32, value: u64) -> RewardsTransaction {
// Rewards are created as the first transaction in every
// mined block using the current reward schedule.
let txtype = 1;
// The reward belongs to the block being created, not the current tip.
let block_height = get_height(db) + 1;
// New miners must first prove participation before receiving
// the block subsidy, so early mined blocks pay a zero reward.
let value = if NodeInfo::get_mined_count(short_address).await < 100 {
0_u64
} else {
calculate_block_reward(block_height).await
};
// Reward transactions are unsigned because they are created by
// consensus rules rather than by a wallet spending funds.
let unsigned_rewards = UnsignedRewardsTransaction::new(txtype, timestamp, value).await;

View File

@ -1,6 +1,7 @@
use crate::log::info;
use crate::miner::flag::{is_mining_stop_requested, is_normal_mode, set_mining_state, MiningState};
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::chain_state::cached_header;
use crate::records::unpack_block::unpack_header::load_block_header;
use crate::sled::Db;
use crate::sleep;
@ -21,8 +22,12 @@ pub async fn fairness_difficulty(block_height: u32, miner_wallet: &str) -> bool
// Walk backward through the recent headers and count how many
// consecutive blocks were mined by this same miner.
for i in (start_block..=block_height).rev() {
// Load the saved header for this height.
let block = load_block_header(i).await.unwrap();
// Use the in-memory recent-header cache during mining, falling
// back to disk only if the cache is not populated yet.
let block = match cached_header(i).await {
Some(block) => block,
None => load_block_header(i).await.unwrap(),
};
// The header stores the miner short address directly.
let mined_by_miner = block.unmined_block.miner;

View File

@ -25,19 +25,10 @@ use crate::Utc;
pub async fn create_genesis_transaction(
db: &Db,
verification_service: Arc<VerificationService>,
wallet_key: String,
wallet: Arc<Wallet>,
map: Arc<Mutex<Command>>,
) {
// Load the local wallet so the genesis block records the miner's
// current short address in the header.
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
Ok(wallet) => wallet,
Err(err) => {
error!("Wallet decryption failed: {err}");
return;
}
};
let miner = wallet.saved.short_address;
let miner = wallet.saved.short_address.clone();
// The genesis transaction carries the fixed launch message and
// uses transaction type zero.
@ -52,7 +43,7 @@ pub async fn create_genesis_transaction(
let _ = create_genesis_block(
genesis_transaction,
&miner,
wallet_key,
wallet,
db,
verification_service,
map,
@ -63,7 +54,7 @@ pub async fn create_genesis_transaction(
async fn create_genesis_block(
signed_genesis_transaction: GenesisTransaction,
miner: &str,
wallet_key: String,
wallet: Arc<Wallet>,
db: &Db,
verification_service: Arc<VerificationService>,
map: Arc<Mutex<Command>>,
@ -123,7 +114,7 @@ async fn create_genesis_block(
// Genesis uses the fixed parent hash and launch difficulty.
let timestamp = Utc::now().timestamp() as u32;
let next_block_difficulty = 3000000000000000_u64;
let next_block_difficulty = 2000000000000000_u64;
let block_struct = UnminedBlock::new(
timestamp,
miner,
@ -134,7 +125,7 @@ async fn create_genesis_block(
.await;
// The VRF binds the candidate header to the mining wallet.
let vrf_block = UnminedBlock::vrf_generate(block_struct, wallet_key.clone()).await;
let vrf_block = UnminedBlock::vrf_generate(block_struct, &wallet.saved.private_key).await;
let header_hash = vrf_block.hash().await;

View File

@ -1,13 +1,14 @@
use crate::blocks::block::{Block, UnminedBlock};
use crate::common::types::Transaction;
use crate::log::{error, info};
use crate::miner::block_rewards::create_rewards_transaction;
use crate::miner::block_rewards::{create_rewards_transaction, reward_value_for_miner_at_height};
use crate::miner::fairness::wait_for_fairness_gate;
use crate::miner::flag::{is_mining_stop_requested, is_normal_mode, set_mining_state, MiningState};
use crate::miner::nonce::run_nonce_round;
use crate::miner::structs::MiningAttemptContext;
use crate::miner::winner::{handle_mining_winner, verify_and_save_block};
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::chain_state::{cached_chain_height, cached_tip_header};
use crate::records::memory::connections::peer_connection_count;
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::response_channels::Command;
@ -26,7 +27,7 @@ use crate::Utc;
pub async fn mine_block(
db: &Db,
verification_service: Arc<VerificationService>,
wallet_key: String,
wallet: Arc<Wallet>,
map: Arc<Mutex<Command>>,
) -> Result<(), Box<dyn Error>> {
if Settings::load()
@ -42,17 +43,11 @@ pub async fn mine_block(
// Mining runs continuously, rebuilding its context from the
// latest saved tip before each one-second nonce round.
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
Ok(wallet) => wallet,
Err(err) => {
return Err(std::io::Error::other(format!("Wallet decryption failed: {err}")).into());
}
};
let miner_short = wallet.saved.short_address;
let miner_short = wallet.saved.short_address.clone();
// Track the height this miner expects to produce next so nonce workers
// can stop quickly when another peer advances the chain.
let mut expected_block_height = get_height(db) + 1;
let mut expected_block_height = current_chain_height(db).await + 1;
let mut fairness_paused_height: Option<u32> = None;
let mut registration_paused_height: Option<u32> = None;
let mut was_stopped = true;
@ -64,7 +59,7 @@ pub async fn mine_block(
// Re-read height each round because peers may have saved a block
// while this miner was paused or waiting for the next second.
let current_block_number = get_height(db) + 1;
let current_block_number = current_chain_height(db).await + 1;
if current_block_number != expected_block_height {
expected_block_height = current_block_number;
}
@ -109,7 +104,7 @@ pub async fn mine_block(
let attempt_context = match prepare_attempt_context(
db,
miner_short.clone(),
wallet_key.clone(),
wallet.clone(),
current_block_number,
verification_service.clone(),
)
@ -157,7 +152,7 @@ async fn wait_until_mining_allowed(mut was_stopped: bool) -> bool {
async fn prepare_attempt_context(
db: &Db,
miner_short: String,
wallet_key: String,
wallet: Arc<Wallet>,
current_block_number: u32,
verification_service: Arc<VerificationService>,
) -> Option<MiningAttemptContext> {
@ -171,7 +166,7 @@ async fn prepare_attempt_context(
match build_attempt_context(
db,
miner_short,
wallet_key,
wallet,
current_block_number,
verification_service,
)
@ -197,14 +192,14 @@ async fn wait_for_next_second_or_chain_change(
// space unless the chain tip or node mode changes first.
if !(is_normal_mode()
&& !is_mining_stop_requested()
&& get_height(db) + 1 == expected_block_height)
&& current_chain_height(db).await + 1 == expected_block_height)
{
return false;
}
while is_normal_mode()
&& !is_mining_stop_requested()
&& get_height(db) + 1 == expected_block_height
&& current_chain_height(db).await + 1 == expected_block_height
{
let now_second = Utc::now().timestamp() as u32;
if now_second != round_second {
@ -218,7 +213,7 @@ async fn wait_for_next_second_or_chain_change(
async fn build_attempt_context(
db: &Db,
miner_short: String,
wallet_key: String,
wallet: Arc<Wallet>,
current_block_number: u32,
verification_service: Arc<VerificationService>,
) -> Result<MiningAttemptContext, Box<dyn Error>> {
@ -228,21 +223,32 @@ async fn build_attempt_context(
return Err("Mining paused before loading previous block header".into());
}
let previous_block_height = current_block_number - 1;
let previous_block = load_block_header(previous_block_height).await?;
let previous_block = match cached_tip_header(previous_block_height).await {
Some(header) => header,
None => load_block_header(previous_block_height).await?,
};
let previous_hash = previous_block.hash().await;
let previous_difficulty = previous_block.unmined_block.next_block_difficulty;
let reward_value = reward_value_for_miner_at_height(&miner_short, current_block_number).await;
Ok(MiningAttemptContext {
db: db.clone(),
miner_short,
wallet_key,
wallet,
current_block_number,
previous_hash,
previous_difficulty,
reward_value,
verification_service,
})
}
async fn current_chain_height(db: &Db) -> u32 {
cached_chain_height()
.await
.unwrap_or_else(|| get_height(db))
}
pub async fn mine_block_internal(
ctx: &MiningAttemptContext,
nonce: u8,
@ -271,12 +277,11 @@ pub async fn mine_block_internal(
.await;
// Add the wallet VRF proof before hashing and verifying the candidate.
let vrf_block = UnminedBlock::vrf_generate(unmined_block, ctx.wallet_key.clone()).await;
let vrf_block = UnminedBlock::vrf_generate(unmined_block, &ctx.wallet.saved.private_key).await;
let block_hash = vrf_block.hash().await;
// Every mined block begins with a consensus-created reward transaction.
let rewards_transaction =
create_rewards_transaction(&ctx.miner_short, timestamp, &ctx.db).await;
let rewards_transaction = create_rewards_transaction(timestamp, ctx.reward_value).await;
let new_block = Block {
vrf_block,
transactions: vec![Transaction::Rewards(rewards_transaction)],

View File

@ -5,6 +5,7 @@ use crate::miner::flag::{is_mining_stop_requested, is_normal_mode};
use crate::miner::mining::mine_block_internal;
use crate::miner::structs::MiningAttemptContext;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::chain_state::cached_chain_height;
use crate::task;
use crate::Arc;
use crate::AtomicBool;
@ -92,7 +93,10 @@ async fn nonce_range(
}
// If the chain tip changed, this round is stale for every worker.
if get_height(&ctx.db) + 1 != ctx.current_block_number {
let current_height = cached_chain_height()
.await
.unwrap_or_else(|| get_height(&ctx.db));
if current_height + 1 != ctx.current_block_number {
stop_flag.store(true, AtomicOrdering::SeqCst);
return Ok(());
}

View File

@ -1,5 +1,6 @@
use crate::sled::Db;
use crate::verifications::verification_service::VerificationService;
use crate::wallets::structures::Wallet;
use crate::Arc;
// MiningAttemptContext captures one consistent chain tip for a nonce round.
@ -8,9 +9,10 @@ use crate::Arc;
pub struct MiningAttemptContext {
pub db: Db,
pub miner_short: String,
pub wallet_key: String,
pub wallet: Arc<Wallet>,
pub current_block_number: u32,
pub previous_hash: String,
pub previous_difficulty: u64,
pub reward_value: u64,
pub verification_service: Arc<VerificationService>,
}

View File

@ -6,6 +6,7 @@ use crate::torrent::structs::Torrent;
use crate::torrent::torrenting_system::torrent_requests::{
handle_response_and_save_torrent, send_request_torrent_message,
};
use crate::wallets::structures::Wallet;
use crate::Arc;
use crate::Duration;
use crate::Mutex;
@ -16,7 +17,7 @@ pub async fn create_genesis_block(
map: Arc<Mutex<Command>>,
stream: Arc<Mutex<TcpStream>>,
db: Db,
wallet_key: &str,
wallet: Arc<Wallet>,
connections_key: String,
) {
// if no local genesis exists, request the remote genesis
@ -38,9 +39,18 @@ pub async fn create_genesis_block(
return;
}
};
handle_response_and_save_torrent(0, &db, torrent, wallet_key, map.clone(), false)
.await
.ok();
handle_response_and_save_torrent(
0,
&db,
torrent,
wallet,
map.clone(),
false,
true,
false,
)
.await
.ok();
}
}
}

View File

@ -0,0 +1,51 @@
use crate::{AtomicBool, AtomicOrdering};
use std::sync::atomic::AtomicU32;
static ORPHAN_CHECK_RUNNING: AtomicBool = AtomicBool::new(false);
static ORPHAN_RECHECK_REQUESTED: AtomicBool = AtomicBool::new(false);
static ORPHAN_RECHECK_FROM_HEIGHT: AtomicU32 = AtomicU32::new(0);
fn store_earliest_recheck_height(incoming_height: u32) {
let mut current = ORPHAN_RECHECK_FROM_HEIGHT.load(AtomicOrdering::SeqCst);
while current == 0 || incoming_height < current {
match ORPHAN_RECHECK_FROM_HEIGHT.compare_exchange(
current,
incoming_height,
AtomicOrdering::SeqCst,
AtomicOrdering::SeqCst,
) {
Ok(_) => return,
Err(next_current) => current = next_current,
}
}
}
pub fn try_begin_orphan_check() -> bool {
if ORPHAN_CHECK_RUNNING
.compare_exchange(false, true, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst)
.is_err()
{
return false;
}
ORPHAN_RECHECK_REQUESTED.store(false, AtomicOrdering::SeqCst);
ORPHAN_RECHECK_FROM_HEIGHT.store(0, AtomicOrdering::SeqCst);
true
}
pub fn request_orphan_recheck(incoming_height: u32) {
store_earliest_recheck_height(incoming_height);
ORPHAN_RECHECK_REQUESTED.store(true, AtomicOrdering::SeqCst);
}
pub fn take_orphan_recheck_height() -> Option<u32> {
if !ORPHAN_RECHECK_REQUESTED.swap(false, AtomicOrdering::SeqCst) {
return None;
}
Some(ORPHAN_RECHECK_FROM_HEIGHT.swap(0, AtomicOrdering::SeqCst))
}
pub fn finish_orphan_check() {
ORPHAN_CHECK_RUNNING.store(false, AtomicOrdering::SeqCst);
}

View File

@ -4,8 +4,10 @@ use crate::orphans::structs::{CheckUp, UndoTransactions};
use crate::orphans::undo_block_transactions::undo_transactions;
use crate::torrent::unpack_local_torrent::load_torrent;
use crate::torrent::unpack_remote_torrent::request_torrent;
use crate::wallets::structures::Wallet;
use crate::Arc;
pub async fn deep_sync_rollback(mut params: CheckUp, wallet_key: &str) {
pub async fn deep_sync_rollback(mut params: CheckUp, wallet: Arc<Wallet>) {
if params.local_height < params.remote_height {
// This pass only handles deeper sync gaps. Near-tip disagreements
// are left for the orphan-window check.
@ -40,7 +42,7 @@ pub async fn deep_sync_rollback(mut params: CheckUp, wallet_key: &str) {
node_syncing: params.node_syncing,
connections_key: params.connections_key.clone(),
};
undo_transactions(undo_transactions_params, wallet_key)
undo_transactions(undo_transactions_params, wallet.clone())
.await
.ok();
params.local_height -= 1;

View File

@ -1,4 +1,5 @@
pub mod add_genesis;
pub mod checkup_state;
pub mod deep_sync_rollback;
pub mod get_path_names;
pub mod orphan_checkup;

View File

@ -1,7 +1,7 @@
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::replay_errors::staged_candidate_status_for_error;
use crate::orphans::structs::{OrphanCheckup, UndoTransactions};
use crate::orphans::undo_block_transactions::undo_transactions;
use crate::records::memory::torrent_status::{
@ -9,6 +9,7 @@ use crate::records::memory::torrent_status::{
};
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::rpc::client::block_hash_vote::request_block_hash_at_height;
use crate::torrent::structs::{DownloadSave, Torrent};
use crate::torrent::torrenting_system::create_file::combine_pieces;
use crate::torrent::torrenting_system::download_locks::acquire_candidate_download;
@ -20,6 +21,8 @@ use crate::torrent::torrenting_system::temp_database_storage::remove_block_piece
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;
use crate::wallets::structures::Wallet;
use crate::Arc;
async fn staged_candidates_for_height(height: u32) -> Vec<Torrent> {
let mut candidates = Vec::new();
@ -97,11 +100,24 @@ async fn candidate_attaches_before_rollback(
params: &OrphanCheckup,
height: u32,
torrent: &Torrent,
wallet_key: &str,
wallet: Arc<Wallet>,
) -> 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?;
torrent.verify(height, &params.db, wallet).await?;
let peer_canonical_hash = request_block_hash_at_height(
params.stream.clone(),
params.map.clone(),
params.connections_key.clone(),
height,
)
.await?;
if peer_canonical_hash != torrent.info.block_hash {
return Err(format!(
"Staged candidate is not peer canonical at height {height}."
));
}
let _download_guard = acquire_candidate_download(height, &torrent.info.info_hash, true).await?;
let verification_service = global_verification_service()
@ -114,6 +130,7 @@ async fn candidate_attaches_before_rollback(
block_number: height,
allow_during_reorg: true,
allow_historical: true,
allow_startup_peers: params.node_syncing,
db: params.db.clone(),
verification_service: std::sync::Arc::new(verification_service),
map: params.map.clone(),
@ -140,6 +157,12 @@ async fn candidate_attaches_before_rollback(
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Candidate header hash does not match torrent metadata.".to_string());
}
if !torrent.info.previous_hash.is_empty()
&& loaded_block.vrf_block.unmined_block.previous_hash != torrent.info.previous_hash
{
cleanup_candidate_pieces(&params.db, height, torrent).await;
return Err("Candidate previous hash does not match torrent metadata.".to_string());
}
if height > 0 {
let parent_height = height - 1;
@ -154,7 +177,7 @@ async fn candidate_attaches_before_rollback(
Ok(())
}
pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), String> {
pub async fn checkup(params: OrphanCheckup, wallet: Arc<Wallet>) -> Result<(), String> {
// The orphan window check only reasons over local canonical/staged evidence inside the
// orphan window. If we do not yet have a competing staged torrent,
// there is nothing to compare and the local chain remains current.
@ -173,9 +196,11 @@ pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), Stri
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.
// competitor with the same parent has also lost to the local block.
for staged_torrent in &staged_candidates {
if staged_torrent.info.info_hash != local_torrent.info.info_hash {
if staged_torrent.info.info_hash != local_torrent.info.info_hash
&& staged_torrent.info.previous_hash == local_torrent.info.previous_hash
{
set_torrent_status(
height,
&staged_torrent.info.info_hash,
@ -191,7 +216,7 @@ pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), Stri
&params,
height,
&competing_torrent,
wallet_key,
wallet.clone(),
)
.await
{
@ -211,23 +236,15 @@ pub async fn checkup(params: OrphanCheckup, wallet_key: &str) -> Result<(), Stri
begin_reorg_lock().await;
}
info!("[orphan] adopting proven staged chain from height {height}");
undo_transactions(undo_transactions_params, wallet_key).await?;
undo_transactions(undo_transactions_params, wallet.clone()).await?;
return Ok(());
}
Err(err) => {
let status = if should_retry_staged_candidate(&err) {
TorrentStatus::Pending
} else {
TorrentStatus::Invalid
};
let status = staged_candidate_status_for_error(&err);
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

@ -1,7 +1,9 @@
use crate::orphans::orphan_checkup::checkup;
use crate::orphans::structs::{CheckUp, OrphanCheckup};
use crate::wallets::structures::Wallet;
use crate::Arc;
pub async fn orphan_window_check(params: CheckUp, wallet_key: &str) -> Result<(), String> {
pub async fn orphan_window_check(params: CheckUp, wallet: Arc<Wallet>) -> Result<(), String> {
// orphan window check handles near-tip comparisons where the local and
// remote chains are within the orphan correction window
let height_diff = match params.local_height.cmp(&params.remote_height) {
@ -10,66 +12,76 @@ pub async fn orphan_window_check(params: CheckUp, wallet_key: &str) -> Result<()
std::cmp::Ordering::Less => params.remote_height - params.local_height,
};
let include_recheck_floor = |stop_check: u32| {
if let Some(recheck_from_height) = params.recheck_from_height {
stop_check.min(recheck_from_height)
} else {
stop_check
}
};
let shared_tip = params.local_height.min(params.remote_height);
let shared_window_floor = include_recheck_floor(shared_tip.saturating_sub(10));
if height_diff == 0 {
// same height means compare the last ten blocks directly
let start_check = params.local_height;
let original_start_check = params.local_height;
let stop_check = params.local_height.saturating_sub(10);
let start_check = shared_tip;
let original_start_check = shared_tip;
let stop_check = shared_window_floor;
let orphan_checkup_params = OrphanCheckup {
start_check,
stop_check,
original_start_check,
local_height: params.local_height,
remote_height: params.remote_height,
recheck_from_height: params.recheck_from_height,
stream: params.stream,
db: params.db,
map: params.map.clone(),
node_syncing: params.node_syncing,
connections_key: params.connections_key,
};
checkup(orphan_checkup_params, wallet_key).await?;
checkup(orphan_checkup_params, wallet.clone()).await?;
} else if height_diff <= 10 && params.local_height > params.remote_height {
// if the local chain is slightly ahead, begin comparison from
// the remote height and only search within the overlap window
let start_check = params.remote_height;
let original_start_check = params.remote_height;
// The farther apart the tips are, the less backward overlap remains
// inside the ten-block correction window.
let stop_check = params.remote_height.saturating_sub(10 - height_diff);
let start_check = shared_tip;
let original_start_check = shared_tip;
let stop_check = shared_window_floor;
let orphan_checkup_params = OrphanCheckup {
start_check,
stop_check,
original_start_check,
local_height: params.local_height,
remote_height: params.remote_height,
recheck_from_height: params.recheck_from_height,
stream: params.stream,
db: params.db,
map: params.map.clone(),
node_syncing: params.node_syncing,
connections_key: params.connections_key,
};
checkup(orphan_checkup_params, wallet_key).await?;
checkup(orphan_checkup_params, wallet.clone()).await?;
} else if height_diff <= 10 && params.local_height < params.remote_height {
// if the remote chain is slightly ahead, start at the local tip
// and search backward only within the valid orphan range
let start_check = params.local_height;
let original_start_check = params.local_height;
// Search only the portion of local history that could still be
// replaced by staged remote candidates.
let stop_check = params.local_height.saturating_sub(10 - height_diff);
let start_check = shared_tip;
let original_start_check = shared_tip;
let stop_check = shared_window_floor;
let orphan_checkup_params = OrphanCheckup {
start_check,
stop_check,
original_start_check,
local_height: params.local_height,
remote_height: params.remote_height,
recheck_from_height: params.recheck_from_height,
stream: params.stream,
db: params.db,
map: params.map.clone(),
node_syncing: params.node_syncing,
connections_key: params.connections_key,
};
checkup(orphan_checkup_params, wallet_key).await?;
checkup(orphan_checkup_params, wallet.clone()).await?;
}
Ok(())
}

View File

@ -1,17 +1,79 @@
use crate::records::memory::torrent_status::TorrentStatus;
pub fn staged_candidate_status_for_error(error: &str) -> TorrentStatus {
if error.contains("Incorrect previous_block_hash.")
|| error.contains("Candidate parent is not current chain parent.")
{
return TorrentStatus::MissingParent;
}
if should_retry_staged_candidate(error) {
TorrentStatus::Pending
} else {
TorrentStatus::Invalid
}
}
pub fn should_retry_staged_candidate(error: &str) -> bool {
// These errors mean the torrent metadata may still describe the winning
// block, but this node could not fetch enough block data to prove it yet.
error.contains("No available peer could provide remaining pieces")
// Explicit "not found" responses mean connected peers cannot seed this
// candidate anymore. Keep retry behavior for local timing/concurrency
// conditions only.
if error.contains("Incoming block is no longer the next expected height.") {
return true;
}
if error.contains("Incorrect previous_block_hash.")
|| error.contains("Candidate parent is not current chain parent.")
{
return true;
}
if error.contains("No available peer could provide remaining pieces")
|| 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("Downloaded candidate length does not match torrent metadata")
{
return false;
}
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")
|| error.contains("Candidate download already active")
|| error.contains("Timed out waiting for active candidate download")
|| error.contains("Downloaded candidate length does not match torrent metadata")
}
#[cfg(test)]
mod tests {
use super::{should_retry_staged_candidate, staged_candidate_status_for_error};
use crate::records::memory::torrent_status::TorrentStatus;
#[test]
fn next_expected_height_race_keeps_candidate_eligible() {
assert!(should_retry_staged_candidate(
"Incoming block is no longer the next expected height."
));
}
#[test]
fn parent_mismatch_waits_for_missing_parent() {
assert_eq!(
staged_candidate_status_for_error("Incorrect previous_block_hash."),
TorrentStatus::MissingParent
);
}
#[test]
fn difficulty_mismatch_rejects_candidate() {
assert_eq!(
staged_candidate_status_for_error(
"error: Difficulty mismatch with the blockchain data."
),
TorrentStatus::Invalid
);
}
}

View File

@ -1,11 +1,12 @@
use crate::orphans::replay_errors::should_retry_staged_candidate;
use crate::orphans::replay_errors::staged_candidate_status_for_error;
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::response_channels::reserve_entry_with_context;
use crate::records::memory::torrent_status::{
get_torrent_status, mark_other_torrent_statuses_invalid, set_torrent_status, TorrentStatus,
get_torrent_status, set_torrent_status, TorrentStatus,
};
use crate::rpc::command_maps::RPC_TORRENT_BY_HEIGHT;
use crate::torrent::structs::Torrent;
use crate::torrent::torrenting_system::save_torrent::{
list_staged_torrents_for_height, read_staged_torrent,
@ -13,13 +14,15 @@ use crate::torrent::torrenting_system::save_torrent::{
use crate::torrent::torrenting_system::torrent_requests::{
handle_response_and_save_torrent, send_request_torrent_message,
};
use crate::wallets::structures::Wallet;
use crate::Arc;
use crate::{timeout, Duration};
use std::collections::HashSet;
pub async fn save_new_blocks(
params: &UndoTransactions,
replay_to_height: u32,
wallet_key: &str,
wallet: Arc<Wallet>,
mut true_start_height: u32,
) -> Result<(), String> {
// After rollback, save replacement blocks only up to the height
@ -70,9 +73,11 @@ pub async fn save_new_blocks(
true_start_height,
&params.db,
torrent.clone(),
wallet_key,
wallet.clone(),
params.map.clone(),
true,
params.node_syncing,
true,
)
.await
{
@ -84,8 +89,6 @@ pub async fn save_new_blocks(
TorrentStatus::Valid,
)
.await;
mark_other_torrent_statuses_invalid(true_start_height, &torrent_info_hash)
.await;
resolved_from_staging = true;
break;
} else {
@ -98,13 +101,7 @@ pub async fn save_new_blocks(
}
}
Err(err) => {
let status = if should_retry_staged_candidate(&err) {
// Missing pieces mean the candidate has not been
// tested yet, so keep it eligible for a later replay.
TorrentStatus::Pending
} else {
TorrentStatus::Invalid
};
let status = staged_candidate_status_for_error(&err);
set_torrent_status(true_start_height, &torrent_info_hash, status).await;
}
}
@ -141,7 +138,12 @@ pub async fn save_new_blocks(
// 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;
let (hashmap_key, _save_tx, save_rx) = reserve_entry_with_context(
params.map.clone(),
Some(RPC_TORRENT_BY_HEIGHT),
Some(params.connections_key.clone()),
)
.await;
send_request_torrent_message(
params.stream.clone(),
@ -188,9 +190,11 @@ pub async fn save_new_blocks(
true_start_height,
&params.db,
torrent,
wallet_key,
wallet.clone(),
params.map.clone(),
true,
params.node_syncing,
true,
)
.await?;
if get_height(&params.db) <= local_height_before {
@ -205,7 +209,6 @@ pub async fn save_new_blocks(
));
}
set_torrent_status(true_start_height, &torrent_info_hash, TorrentStatus::Valid).await;
mark_other_torrent_statuses_invalid(true_start_height, &torrent_info_hash).await;
} else {
return Err(format!(
"No replacement torrent received while replaying height {true_start_height}"

View File

@ -1,11 +1,16 @@
use crate::common::binary_conversions::binary_to_string;
use crate::log::error;
use crate::miner::flag::begin_reorg_lock;
use crate::log::{error, info, warn};
use crate::miner::flag::{begin_reorg_lock, is_syncing_mode};
use crate::orphans::structs::UndoTransactions;
use crate::orphans::undo_block_transactions::undo_transactions;
use crate::records::memory::connections::live_miner_peer_streams;
use crate::records::memory::response_channels::Command;
use crate::records::unpack_block::unpack_header::load_block_header;
use crate::rpc::client::block_hash_vote::request_block_hash_at_height;
use crate::sled::Db;
use crate::torrent::unpack_remote_torrent::request_torrent;
use crate::wallets::structures::Wallet;
use crate::{tokio, Arc, Mutex};
async fn get_snapshot(db: &Db) -> Option<(u32, String)> {
// snapshots store a trusted height/hash pair used to
@ -26,7 +31,115 @@ pub async fn snapshot_height(db: &Db) -> Option<u32> {
get_snapshot(db).await.map(|(height, _)| height)
}
pub async fn update_snapshot(db: &Db, current_height: u32) -> Result<(), String> {
fn required_snapshot_votes(total_voters: usize) -> usize {
(total_voters * 2).div_ceil(3).max(2)
}
fn peer_is_syncing_vote_error(error: &str) -> bool {
error.trim() == "error: Node is syncing"
}
async fn snapshot_has_peer_quorum(
snapshot_height: u32,
local_hash: &str,
map: Arc<Mutex<Command>>,
) -> bool {
let peers = live_miner_peer_streams().await;
if peers.is_empty() {
warn!(
"[snapshot] not advancing snapshot at height {snapshot_height}: no connected miner peers"
);
return false;
}
let mut handles = Vec::with_capacity(peers.len());
for (connections_key, stream) in peers {
let map_clone = map.clone();
handles.push(tokio::spawn(async move {
let vote = request_block_hash_at_height(
stream,
map_clone,
connections_key.clone(),
snapshot_height,
)
.await;
(connections_key, vote)
}));
}
let mut matching_votes = 1usize;
let mut eligible_peer_votes = 0usize;
for handle in handles {
match handle.await {
Ok((connections_key, Ok(peer_hash))) => {
eligible_peer_votes += 1;
if peer_hash == local_hash {
matching_votes += 1;
} else {
warn!(
"[snapshot] peer hash mismatch: height={snapshot_height} peer={connections_key} local_hash={local_hash} peer_hash={peer_hash}"
);
}
}
Ok((connections_key, Err(err))) => {
if peer_is_syncing_vote_error(&err) {
info!(
"[snapshot] skipping syncing peer vote: height={snapshot_height} peer={connections_key}"
);
} else {
warn!(
"[snapshot] peer vote failed: height={snapshot_height} peer={connections_key} err={err}"
);
}
}
Err(err) => {
warn!("[snapshot] peer vote task failed at height {snapshot_height}: {err}");
}
}
}
let total_voters = eligible_peer_votes + 1;
if total_voters < 2 {
warn!(
"[snapshot] not advancing snapshot at height {snapshot_height}: no eligible non-syncing miner peers"
);
return false;
}
let required_votes = required_snapshot_votes(total_voters);
if matching_votes >= required_votes {
info!(
"[snapshot] consensus reached: height={snapshot_height} hash={local_hash} votes={matching_votes}/{total_voters} required={required_votes}"
);
true
} else {
warn!(
"[snapshot] consensus not reached: height={snapshot_height} hash={local_hash} votes={matching_votes}/{total_voters} required={required_votes}"
);
false
}
}
fn store_snapshot(db: &Db, snapshot_height: u32, hash: &str) -> Result<(), String> {
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}"))?;
Ok(())
}
pub async fn update_snapshot(
db: &Db,
current_height: u32,
map: Arc<Mutex<Command>>,
) -> Result<(), String> {
if is_syncing_mode() {
return Ok(());
}
// Genesis is always a valid snapshot, then later snapshots lag the tip
// so normal orphan correction still has room to operate.
let snapshot_height = if current_height == 0 {
@ -45,14 +158,21 @@ pub async fn update_snapshot(db: &Db, current_height: u32) -> Result<(), String>
// still loaded from disk when the snapshot is checked.
let header = load_block_header(snapshot_height).await?;
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}"))?;
if snapshot_height == 0 {
store_snapshot(db, snapshot_height, &hash)?;
return Ok(());
}
if !snapshot_has_peer_quorum(snapshot_height, &hash, map).await {
return Ok(());
}
store_snapshot(db, snapshot_height, &hash)?;
Ok(())
}
pub async fn snapshot_verified(params: UndoTransactions, wallet_key: &str) -> bool {
pub async fn snapshot_verified(params: UndoTransactions, wallet: Arc<Wallet>) -> bool {
// if the local chain disagrees with the stored snapshot,
// roll back to the snapshot point before continuing
if let Some((snap_height, snap_hash)) = get_snapshot(&params.db).await {
@ -75,7 +195,7 @@ pub async fn snapshot_verified(params: UndoTransactions, wallet_key: &str) -> bo
node_syncing: params.node_syncing,
connections_key: params.connections_key,
};
let _ = undo_transactions(undo_transactions_params, wallet_key).await;
let _ = undo_transactions(undo_transactions_params, wallet.clone()).await;
return false;
}
// also make sure the remote peer still agrees

View File

@ -12,6 +12,7 @@ pub struct OrphanCheckup {
pub original_start_check: u32,
pub local_height: u32,
pub remote_height: u32,
pub recheck_from_height: Option<u32>,
pub stream: Arc<Mutex<TcpStream>>,
pub db: Db,
pub map: Arc<Mutex<Command>>,
@ -26,6 +27,7 @@ pub struct OrphanCheckup2 {
pub db: Db,
pub local_height: u32,
pub remote_height: u32,
pub recheck_from_height: Option<u32>,
pub map: Arc<Mutex<Command>>,
pub node_syncing: bool,
pub connections_key: String,
@ -49,6 +51,7 @@ pub struct UndoTransactions {
pub struct CheckUp {
pub local_height: u32,
pub remote_height: u32,
pub recheck_from_height: Option<u32>,
pub db: Db,
pub stream: Arc<Mutex<TcpStream>>,
pub map: Arc<Mutex<Command>>,

View File

@ -2,24 +2,33 @@ use crate::common::check_genesis::genesis_checkup;
use crate::log::{error, info, warn};
use crate::miner::flag::end_reorg_lock;
use crate::orphans::add_genesis::create_genesis_block;
use crate::orphans::checkup_state::take_orphan_recheck_height;
use crate::orphans::deep_sync_rollback::deep_sync_rollback;
use crate::orphans::orphan_window_check::orphan_window_check;
use crate::orphans::replay_errors::should_retry_staged_candidate;
use crate::orphans::replay_errors::{
should_retry_staged_candidate, staged_candidate_status_for_error,
};
use crate::orphans::snapshot_check::snapshot_verified;
use crate::orphans::structs::CheckUp;
use crate::orphans::structs::OrphanCheckup2;
use crate::orphans::structs::UndoTransactions;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::torrent_status::{
get_torrent_status, mark_other_torrent_statuses_invalid, set_torrent_status, TorrentStatus,
get_torrent_status, set_torrent_status, TorrentStatus,
};
use crate::startup::remote_height::request_remote_height;
use crate::torrent::structs::Torrent;
use crate::torrent::torrenting_system::save_torrent::{
list_staged_torrents, read_staged_torrent, remove_staged_torrent,
};
use crate::torrent::torrenting_system::torrent_requests::handle_response_and_save_torrent;
use crate::wallets::structures::Wallet;
use crate::Arc;
async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Result<(), String> {
async fn replay_staged_torrents(
params: &OrphanCheckup2,
wallet: Arc<Wallet>,
) -> Result<(), String> {
// staged torrents are replayed after orphan correction so
// any valid deferred candidates can be reconsidered in order.
// Replay is height-based: all candidates for the current expected
@ -114,9 +123,11 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re
expected_height,
&params.db,
torrent,
wallet_key,
wallet.clone(),
params.map.clone(),
true,
params.node_syncing,
true,
)
.await
{
@ -131,23 +142,17 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re
};
set_torrent_status(expected_height, &torrent_info_hash, status).await;
if advanced_height {
mark_other_torrent_statuses_invalid(expected_height, &torrent_info_hash)
.await;
break;
}
}
Err(err) => {
if should_retry_staged_candidate(&err) {
let status = staged_candidate_status_for_error(&err);
if status != TorrentStatus::Invalid {
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.
set_torrent_status(
expected_height,
&torrent_info_hash,
TorrentStatus::Pending,
)
.await;
set_torrent_status(expected_height, &torrent_info_hash, status).await;
} else {
set_torrent_status(
expected_height,
@ -176,7 +181,7 @@ async fn replay_staged_torrents(params: &OrphanCheckup2, wallet_key: &str) -> Re
}
}
pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<(), String> {
async fn sync_checkup_pass(params: &OrphanCheckup2, wallet: Arc<Wallet>) -> Result<(), String> {
// bootstrap missing genesis first so the normal orphan
// correction logic can operate against a valid local chain
if params.local_height == 0 && !genesis_checkup().await {
@ -186,7 +191,7 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<()
params.map.clone(),
params.stream.clone(),
params.db.clone(),
wallet_key,
wallet.clone(),
params.connections_key.clone(),
)
.await;
@ -202,11 +207,11 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<()
};
// snapshot verification can trigger an immediate rollback
// if a trusted checkpoint no longer matches local state
if !snapshot_verified(undo_transactions_params, wallet_key).await {
if !snapshot_verified(undo_transactions_params, wallet.clone()).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 {
match replay_staged_torrents(params, wallet.clone()).await {
Ok(()) => {}
Err(err) => {
replay_waiting = should_retry_staged_candidate(&err);
@ -218,9 +223,6 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<()
"[orphan] replay is waiting for block data; leaving candidates pending for a later pass"
);
}
if !params.node_syncing {
end_reorg_lock();
}
return Ok(());
}
// run the two orphan rules in order, then replay any staged
@ -228,30 +230,37 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<()
let checkup_params = CheckUp {
local_height: params.local_height,
remote_height: params.remote_height,
recheck_from_height: params.recheck_from_height,
db: params.db.clone(),
stream: params.stream.clone(),
map: params.map.clone(),
node_syncing: params.node_syncing,
connections_key: params.connections_key.clone(),
};
deep_sync_rollback(checkup_params.clone(), wallet_key).await;
deep_sync_rollback(checkup_params.clone(), wallet.clone()).await;
let mut replay_waiting = false;
let height_before_window_check = get_height(&params.db);
match orphan_window_check(checkup_params, wallet_key).await {
match orphan_window_check(checkup_params, wallet.clone()).await {
Ok(()) => {}
Err(err) => {
if should_retry_staged_candidate(&err)
&& get_height(&params.db) < height_before_window_check
{
replay_waiting = true;
let height_after_window_check = get_height(&params.db);
let rolled_back = height_after_window_check < height_before_window_check;
if rolled_back {
if should_retry_staged_candidate(&err) {
replay_waiting = true;
} else {
return Err(format!(
"orphan window adoption failed after rollback: {err}"
));
}
}
error!("[orphan] orphan window check error: {err}");
}
}
let height_before_replay = get_height(&params.db);
match replay_staged_torrents(&params, wallet_key).await {
match replay_staged_torrents(params, wallet.clone()).await {
Ok(()) => {}
Err(err) => {
replay_waiting |= should_retry_staged_candidate(&err);
@ -266,9 +275,53 @@ pub async fn sync_checkup(params: OrphanCheckup2, wallet_key: &str) -> Result<()
"[orphan] replay is waiting for block data; leaving candidates pending for a later pass"
);
}
info!("[orphan] orphan check pass completed");
Ok(())
}
pub async fn sync_checkup(mut params: OrphanCheckup2, wallet: Arc<Wallet>) -> Result<(), String> {
let result = loop {
match sync_checkup_pass(&params, wallet.clone()).await {
Ok(()) => {}
Err(err) => break Err(err),
}
let Some(recheck_height) = take_orphan_recheck_height() else {
break Ok(());
};
let local_height = get_height(&params.db);
let remote_height = match request_remote_height(
params.stream.clone(),
params.map.clone(),
params.connections_key.clone(),
)
.await
{
Ok(height) => height,
Err(err) => {
warn!("[orphan] failed to refresh remote height before queued recheck: {err}");
params.remote_height
}
};
params.local_height = local_height;
params.remote_height = remote_height.max(recheck_height).max(local_height);
params.recheck_from_height = Some(recheck_height);
warn!(
"[orphan] running queued orphan recheck: local_height={} remote_height={} queued_height={}",
params.local_height, params.remote_height, recheck_height
);
};
if !params.node_syncing {
end_reorg_lock();
}
info!("[orphan] orphan check completed");
Ok(())
if result.is_ok() {
info!("[orphan] orphan check completed");
}
result
}

View File

@ -1,4 +1,4 @@
use crate::records::memory::response_channels::{reserve_entry, Command};
use crate::records::memory::response_channels::{reserve_entry_with_context, Command};
use crate::rpc::command_maps::RPC_TORRENT_CANDIDATES;
use crate::rpc::responses::RpcResponse;
use crate::torrent::torrenting_system::save_torrent::save_staged_torrent;
@ -11,7 +11,12 @@ pub async fn hydrate_torrent_candidates(
) -> Result<usize, String> {
// Reserve a reply slot and send a small request packet asking the peer for
// its staged/local torrent candidates.
let (hashmap_key, _tx, rx) = reserve_entry(map.clone()).await;
let (hashmap_key, _tx, rx) = reserve_entry_with_context(
map.clone(),
Some(RPC_TORRENT_CANDIDATES),
Some(connections_key.clone()),
)
.await;
let mut message = Vec::with_capacity(4);
message.push(RPC_TORRENT_CANDIDATES);
message.extend_from_slice(&hashmap_key);

View File

@ -1,5 +1,4 @@
use crate::records::block_height::decrease_block_height::decrease_height;
use crate::records::memory::averages::{load_initial_blocks, DIFFICULTY_AVERAGE_WINDOW};
use crate::remove_file;
use crate::sled::Db;
@ -21,11 +20,6 @@ pub async fn undo_block(
}
pub async fn finalize_undo_height(final_height: u32, db: &Db) {
// once rollback is complete, lower the recorded chain
// height and refresh the rolling averages cache
// once rollback is complete, lower the recorded chain height
decrease_height(final_height, db);
// Difficulty averages are cached from recent blocks, so they must be
// rebuilt after removing block files.
let start_block = final_height.saturating_sub(DIFFICULTY_AVERAGE_WINDOW.saturating_sub(1));
load_initial_blocks(start_block, final_height).await;
}

View File

@ -17,11 +17,17 @@ use crate::orphans::undo_transactions::undo_swap::undo_swap_transaction;
use crate::orphans::undo_transactions::undo_transfer::undo_transfer_transaction;
use crate::orphans::undo_transactions::undo_vanity::undo_vanity_transaction;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::chain_state::rebuild_chain_state_cache;
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::torrent_status::reset_all_torrent_statuses;
use crate::records::unpack_block::load_by_block_number::load_block;
use crate::wallets::structures::Wallet;
use crate::Arc;
pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Result<(), String> {
pub async fn undo_transactions(
params: UndoTransactions,
wallet: Arc<Wallet>,
) -> Result<(), String> {
// walk backward from the current tip to the selected
// rollback height and undo each block in reverse order
let true_start_height = params.start_height;
@ -149,6 +155,7 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
let final_height = true_start_height.saturating_sub(1);
crate::orphans::undo_block::finalize_undo_height(final_height, &params.db).await;
rebuild_chain_state_cache(&params.db).await?;
// Only now that every rolled-back block has been unwound do we test
// whether its former transactions are still spendable on the new base.
@ -160,10 +167,9 @@ pub async fn undo_transactions(params: UndoTransactions, wallet_key: &str) -> Re
// outcome must be reconsidered before replacement blocks are replayed.
reset_all_torrent_statuses().await;
// rebuild mined counts after rollback, then fetch and save the
// replacement blocks, and finally rebuild mined counts again
NodeInfo::rebuild_mined_counts_from_chain(&params.db).await?;
save_new_blocks(&params, replay_to_height, wallet_key, true_start_height).await?;
NodeInfo::rebuild_mined_counts_from_chain(&params.db).await?;
// Counts are corrected incrementally: rollback decrements each removed
// block above, and replay/save increments each accepted replacement.
save_new_blocks(&params, replay_to_height, wallet, true_start_height).await?;
rebuild_chain_state_cache(&params.db).await?;
Ok(())
}

View File

@ -3,8 +3,9 @@ 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::add_payments_db::remove_payment;
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
use crate::records::record_chain::add_payments_db::remove_payment;
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
@ -79,22 +80,24 @@ pub async fn undo_borrower_transaction(
let _ =
balance_sheet_operation_with_db(db, borrower, payback_amount, &loan_coin, operand_addition);
// Remove the payment transaction lookup from the txid tree.
let txid_tree = db
.open_tree("txid")
.map_err(|e| format!("Failed to open txid tree: {e}"))?;
let tx_hash = transaction.unsigned_contract_payment.hash().await;
txid_tree
.remove(decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?)
.map_err(|e| format!("Failed to remove borrower txid: {e}"))?;
// Remove the payment transaction lookup from the txid tree.
let txid_tree = db
.open_tree("txid")
.map_err(|e| format!("Failed to open txid tree: {e}"))?;
let tx_hash = transaction.unsigned_contract_payment.hash().await;
let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?;
let _ =
remove_wallet_transaction_index(db, &[borrower, &lender, mining_receiver], &tx_hash_bytes);
txid_tree
.remove(tx_hash_bytes.clone())
.map_err(|e| format!("Failed to remove borrower txid: {e}"))?;
// Loan payments involving NFTs also add a provenance entry for the loan
// asset, so remove it if this loan coin is tracked as an NFT.
let nft_tree = db
.open_tree("nfts")
.map_err(|e| format!("Failed to open nfts tree: {e}"))?;
let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?;
if nft_tree.contains_key(loan_coin.as_bytes()).unwrap_or(false) {
if nft_tree.contains_key(loan_coin.as_bytes()).unwrap_or(false) {
let _ = remove_nft_history_entry(db, &loan_coin, &tx_hash_bytes);
}

View File

@ -5,6 +5,7 @@ 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::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_burn_transaction(transaction: BurnTransaction, mining_receiver: &str, db: &Db) {
@ -53,6 +54,11 @@ pub async fn undo_burn_transaction(transaction: BurnTransaction, mining_receiver
);
let hash_binary = decode(&transaction.unsigned_burn.hash().await).unwrap();
let _ = remove_wallet_transaction_index(
db,
&[&transaction.unsigned_burn.address, mining_receiver],
&hash_binary,
);
// Delete the txid lookup inserted when the burn was saved.
let txid_tree = db.open_tree("txid").unwrap();

View File

@ -1,108 +1,108 @@
use crate::blocks::collateral::CollateralClaimTransaction;
use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::collateral::CollateralClaimTransaction;
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::rpc::commands::transaction_by_txid::request_transaction_by_txid;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
pub async fn undo_collateral_transaction(
transaction: CollateralClaimTransaction,
mining_receiver: &str,
db: &Db,
) -> Result<(), String> {
// restore balances and contract state for a collateral
// claim that is being removed during orphan 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();
// reload the original loan contract so the collateral
// asset and amount can be restored correctly
let contract_hash = decode(&transaction.unsigned_collateral_claim.contract_hash)
.map_err(|e| format!("Error decoding contract hash: {e}"))?;
let contract = request_transaction_by_txid(db, contract_hash.clone()).await;
let loan_txtype = 7;
let loan_tx = match contract {
RpcResponse::Binary(contract_bytes) => {
if contract_bytes.is_empty() {
return Err("Invalid loan contract: empty transaction bytes".to_string());
}
if contract_bytes[0] != loan_txtype {
return Err(
"Invalid loan contract: referenced transaction is not a loan contract"
.to_string(),
);
}
LoanContractTransaction::from_bytes(loan_txtype, &contract_bytes[1..])
.await
.map_err(|e| e.to_string())?
}
};
let collateral = loan_tx.unsigned_loan_contract.collateral;
let collateral_amount = loan_tx.unsigned_loan_contract.collateral_amount;
let collateral_holding = format!(
"collateral_{}",
transaction.unsigned_collateral_claim.contract_hash
);
let claimer = &transaction.unsigned_collateral_claim.address;
let txfee = transaction.unsigned_collateral_claim.txfee;
// reverse the fee and move the collateral back into the
// contract holding wallet until the claim exists again
let _ =
balance_sheet_operation_with_db(db, mining_receiver, txfee, &type_str, operand_subtraction);
let _ = balance_sheet_operation_with_db(db, claimer, txfee, &type_str, operand_addition);
let _ = balance_sheet_operation_with_db(
db,
claimer,
collateral_amount,
&collateral,
operand_subtraction,
);
let _ = balance_sheet_operation_with_db(
db,
&collateral_holding,
collateral_amount,
&collateral,
operand_addition,
);
// Remove the collateral-claim transaction lookup from the txid tree.
let txid_tree = db.open_tree("txid").unwrap();
let tx_hash = transaction.unsigned_collateral_claim.hash().await;
txid_tree
.remove(decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?)
.unwrap();
// NFT collateral claims write provenance for the collateral asset.
let nft_tree = db.open_tree("nfts").unwrap();
let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?;
if nft_tree
.contains_key(collateral.as_bytes())
.unwrap_or(false)
{
let _ = remove_nft_history_entry(db, &collateral, &tx_hash_bytes);
}
// Mark the loan contract active again because the collateral claim no
// longer exists after rollback.
let loan_tree = db.open_tree("loan").unwrap();
loan_tree.insert(contract_hash, "true".as_bytes()).unwrap();
use crate::records::record_chain::nft_provenance::remove_nft_history_entry;
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
pub async fn undo_collateral_transaction(
transaction: CollateralClaimTransaction,
mining_receiver: &str,
db: &Db,
) -> Result<(), String> {
// restore balances and contract state for a collateral
// claim that is being removed during orphan 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();
// reload the original loan contract so the collateral
// asset and amount can be restored correctly
let contract_hash = decode(&transaction.unsigned_collateral_claim.contract_hash)
.map_err(|e| format!("Error decoding contract hash: {e}"))?;
let contract = request_transaction_by_txid(db, contract_hash.clone()).await;
let loan_txtype = 7;
let loan_tx = match contract {
RpcResponse::Binary(contract_bytes) => {
if contract_bytes.is_empty() {
return Err("Invalid loan contract: empty transaction bytes".to_string());
}
if contract_bytes[0] != loan_txtype {
return Err(
"Invalid loan contract: referenced transaction is not a loan contract"
.to_string(),
);
}
LoanContractTransaction::from_bytes(loan_txtype, &contract_bytes[1..])
.await
.map_err(|e| e.to_string())?
}
};
let collateral = loan_tx.unsigned_loan_contract.collateral;
let collateral_amount = loan_tx.unsigned_loan_contract.collateral_amount;
let collateral_holding = format!(
"collateral_{}",
transaction.unsigned_collateral_claim.contract_hash
);
let claimer = &transaction.unsigned_collateral_claim.address;
let txfee = transaction.unsigned_collateral_claim.txfee;
// reverse the fee and move the collateral back into the
// contract holding wallet until the claim exists again
let _ =
balance_sheet_operation_with_db(db, mining_receiver, txfee, &type_str, operand_subtraction);
let _ = balance_sheet_operation_with_db(db, claimer, txfee, &type_str, operand_addition);
let _ = balance_sheet_operation_with_db(
db,
claimer,
collateral_amount,
&collateral,
operand_subtraction,
);
let _ = balance_sheet_operation_with_db(
db,
&collateral_holding,
collateral_amount,
&collateral,
operand_addition,
);
// Remove the collateral-claim transaction lookup from the txid tree.
let txid_tree = db.open_tree("txid").unwrap();
let tx_hash = transaction.unsigned_collateral_claim.hash().await;
let tx_hash_bytes = decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?;
let _ = remove_wallet_transaction_index(db, &[claimer, mining_receiver], &tx_hash_bytes);
txid_tree.remove(tx_hash_bytes.clone()).unwrap();
// NFT collateral claims write provenance for the collateral asset.
let nft_tree = db.open_tree("nfts").unwrap();
if nft_tree
.contains_key(collateral.as_bytes())
.unwrap_or(false)
{
let _ = remove_nft_history_entry(db, &collateral, &tx_hash_bytes);
}
// Mark the loan contract active again because the collateral claim no
// longer exists after rollback.
let loan_tree = db.open_tree("loan").unwrap();
loan_tree.insert(contract_hash, "true".as_bytes()).unwrap();
Ok(())
}

View File

@ -4,6 +4,7 @@ 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::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
const NFT_UNIT: u64 = 100_000_000;
@ -43,6 +44,7 @@ pub async fn undo_create_nft_transaction(
);
let _ = balance_sheet_operation_with_db(db, creator, *txfee, &type_str, operand_addition);
let hash_binary = decode(&transaction.unsigned_create_nft.hash().await).unwrap();
let _ = remove_wallet_transaction_index(db, &[creator, mining_receiver], &hash_binary);
// Remove the create-NFT transaction lookup from the txid tree.
let tree = db.open_tree("txid").unwrap();

View File

@ -3,6 +3,7 @@ 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::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_create_token_transaction(
@ -46,6 +47,7 @@ pub async fn undo_create_token_transaction(
let ticker_binary = &transaction.unsigned_create_token.ticker.as_bytes();
let hash_binary = decode(&transaction.unsigned_create_token.hash().await).unwrap();
let _ = remove_wallet_transaction_index(db, &[creator, mining_receiver], &hash_binary);
// Remove the create-token transaction lookup from the txid tree.
let tree = db.open_tree("txid").unwrap();

View File

@ -3,6 +3,7 @@ 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::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_issue_token_transaction(
@ -43,6 +44,7 @@ pub async fn undo_issue_token_transaction(
let _ = balance_sheet_operation_with_db(db, creator, *number, ticker, operand_subtraction);
let hash_binary = decode(&transaction.unsigned_issue_token.hash().await).unwrap();
let _ = remove_wallet_transaction_index(db, &[creator, mining_receiver], &hash_binary);
// Delete the issued-token transaction lookup and provenance record.
let txid_tree = db.open_tree("txid").unwrap();

View File

@ -3,6 +3,7 @@ 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::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_loan_creation_transaction(
@ -66,6 +67,7 @@ pub async fn undo_loan_creation_transaction(
);
let hash_binary = decode(hash).unwrap();
let _ = remove_wallet_transaction_index(db, &[lender, borrower, mining_receiver], &hash_binary);
// delete the txid and remove the active loan record
// so the contract no longer exists on-chain

View File

@ -2,6 +2,7 @@ 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::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_marketing_transaction(
@ -40,6 +41,7 @@ pub async fn undo_marketing_transaction(
let _ = balance_sheet_operation_with_db(db, advertiser, *txfee, &type_str, operand_addition);
let hash = decode(&transaction.unsigned_marketing.hash().await).unwrap();
let _ = remove_wallet_transaction_index(db, &[advertiser, mining_receiver], &hash);
// Remove the marketing transaction lookup from the txid tree.
let tree = db.open_tree("txid").unwrap();

View File

@ -5,6 +5,7 @@ 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::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_rewards_transaction(
@ -38,6 +39,7 @@ pub async fn undo_rewards_transaction(
}
let hash = decode(transaction.unsigned.hash().await).unwrap();
let _ = remove_wallet_transaction_index(db, &[mining_receiver], &hash);
// Remove the reward transaction lookup from the txid tree.
let tree = db.open_tree("txid").unwrap();

View File

@ -4,6 +4,7 @@ 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::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_swap_transaction(transaction: SwapTransaction, mining_receiver: &str, db: &Db) {
@ -84,6 +85,7 @@ pub async fn undo_swap_transaction(transaction: SwapTransaction, mining_receiver
// Convert the txid hash back to bytes for tree lookup/removal.
let hash = decode(&transaction.unsigned_swap.hash().await).unwrap();
let _ = remove_wallet_transaction_index(db, &[sender1, sender2, mining_receiver], &hash);
// Remove the txid lookup for the rolled-back swap.
let tree = db.open_tree("txid").unwrap();

View File

@ -4,6 +4,7 @@ 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::wallet_tx_index::remove_wallet_transaction_index;
use crate::sled::Db;
pub async fn undo_transfer_transaction(
@ -51,6 +52,7 @@ pub async fn undo_transfer_transaction(
let _ = balance_sheet_operation_with_db(db, sender, *txfee, &type_str, operand_addition);
let hash = decode(&transaction.unsigned_transfer.hash().await).unwrap();
let _ = remove_wallet_transaction_index(db, &[sender, receiver, mining_receiver], &hash);
// Remove the txid lookup so the rolled-back transfer no longer resolves as
// an on-chain transaction.

View File

@ -2,6 +2,7 @@ use crate::blocks::vanity::VanityAddressTransaction;
use crate::decode;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
use crate::records::memory::mempool::BASECOIN;
use crate::records::record_chain::wallet_tx_index::remove_wallet_transaction_index;
use crate::records::wallet_registry::{
register_or_update_vanity_address, remove_registered_vanity_for_owner,
take_previous_vanity_for_txid, VanityRegistrationResult,
@ -63,6 +64,7 @@ pub async fn undo_vanity_transaction(
.map_err(|err| format!("Could not open txid tree during vanity undo: {err}"))?;
let txkey = decode(&txhash)
.map_err(|err| format!("Could not decode vanity txhash during undo: {err}"))?;
let _ = remove_wallet_transaction_index(db, &[&owner_address, mining_receiver], &txkey);
tree.remove(txkey)
.map_err(|err| format!("Could not remove vanity txid mapping during undo: {err}"))?;

View File

@ -1,22 +1,15 @@
use crate::common::skein::skein_256_hash_data;
use crate::log::error;
use crate::records::ip_score::get_score::get_ip_score_timestamp;
use crate::rpc::commands::unblock_peer_ip::unblock_peer;
use crate::sled::Db;
use crate::sleep;
use crate::wallets::structures::Wallet;
use crate::Arc;
use crate::Duration;
pub async fn sign_ip_to_ban(ip: &str, wallet_key: &str) -> String {
pub async fn sign_ip_to_ban(ip: &str, wallet: &Wallet) -> String {
// Ban and unban operations reuse the wallet signature flow so peer actions
// can be authenticated by other nodes.
let wallet = match Wallet::try_obtain_wallet(wallet_key.to_string(), None).await {
Ok(wallet) => wallet,
Err(err) => {
error!("Wallet decryption failed while signing IP ban: {err}");
return String::new();
}
};
let privkey = &wallet.saved.private_key;
// The signature is over the IP hash, not the raw IP string.
let ip_hash = skein_256_hash_data(ip);
@ -24,12 +17,12 @@ pub async fn sign_ip_to_ban(ip: &str, wallet_key: &str) -> String {
signature
}
pub fn spawn_unban(db: Db, ip: String, signature: String, wallet_key: String, duration: Duration) {
pub fn spawn_unban(db: Db, ip: String, signature: String, wallet: Arc<Wallet>, duration: Duration) {
// Timed unbans are scheduled in the background so temporary bans can expire
// automatically without blocking the caller.
tokio::spawn(async move {
sleep(duration).await;
unblock_peer(&db, ip.to_string(), signature, wallet_key.to_string()).await;
unblock_peer(&db, ip.to_string(), signature, wallet).await;
});
}

View File

@ -1,105 +1,86 @@
use crate::log::warn;
use crate::records::ip_score::ban_management::{sign_ip_to_ban, spawn_unban};
use crate::records::memory::connections::CONNECTIONS;
use crate::records::memory::enums::ClientType;
use crate::rpc::commands::block_peer_ip::block_peer;
use crate::sled::Db;
use crate::Duration;
pub async fn issue_penalty(
score: u8,
ip: &str,
client_type: &str,
wallet_key: &str,
db: &Db,
) -> String {
use crate::log::warn;
use crate::records::ip_score::ban_management::{sign_ip_to_ban, spawn_unban};
use crate::records::memory::connections::{spawn_retry_dropped_outgoing, CONNECTIONS};
use crate::records::memory::enums::{ClientType, ConnectionType};
use crate::rpc::commands::block_peer_ip::block_peer;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::Arc;
use crate::Duration;
pub async fn issue_penalty(
score: u8,
ip: &str,
client_type: &str,
wallet: Arc<Wallet>,
db: &Db,
) -> String {
// Penalties only matter for active known connections, so resolve the
// reported client type before taking action.
let mut guard = CONNECTIONS.write().await;
let Ok(client_type) = client_type.parse::<ClientType>() else {
return "No action taken".to_string();
};
let signature = sign_ip_to_ban(ip, wallet_key).await;
if let Some(conn) = guard.as_mut() {
if let Some((connection_type, port)) =
conn.find_connection_info_by_client_type(ip, client_type)
{
let signature = sign_ip_to_ban(ip, &wallet).await;
if let Some(conn) = guard.as_mut() {
if let Some((connection_type, port)) =
conn.find_connection_info_by_client_type(ip, client_type)
{
// Higher scores escalate from a dropped connection to temporary and
// then permanent bans.
if score > 100 {
warn!("[ip_score] permanently banning ip={ip} score={score}");
block_peer(
db,
ip.to_string(),
signature.to_string(),
wallet_key.to_string(),
)
.await;
return format!("IP {ip} permanently banned");
block_peer(db, ip.to_string(), signature.to_string(), wallet.clone()).await;
return format!("IP {ip} permanently banned");
} else if score > 75 {
warn!("[ip_score] banning ip={ip} duration=24h score={score}");
block_peer(
db,
ip.to_string(),
signature.to_string(),
wallet_key.to_string(),
)
.await;
spawn_unban(
db.clone(),
ip.to_string(),
signature.to_string(),
wallet_key.to_string(),
Duration::from_secs(86400),
);
return format!("IP {ip} banned for 24 hours");
block_peer(db, ip.to_string(), signature.to_string(), wallet.clone()).await;
spawn_unban(
db.clone(),
ip.to_string(),
signature.to_string(),
wallet.clone(),
Duration::from_secs(86400),
);
return format!("IP {ip} banned for 24 hours");
} else if score > 50 {
warn!("[ip_score] banning ip={ip} duration=1h score={score}");
block_peer(
db,
ip.to_string(),
signature.to_string(),
wallet_key.to_string(),
)
.await;
spawn_unban(
db.clone(),
ip.to_string(),
signature.to_string(),
wallet_key.to_string(),
Duration::from_secs(3600),
);
return format!("IP {ip} banned for 1 hour");
block_peer(db, ip.to_string(), signature.to_string(), wallet.clone()).await;
spawn_unban(
db.clone(),
ip.to_string(),
signature.to_string(),
wallet.clone(),
Duration::from_secs(3600),
);
return format!("IP {ip} banned for 1 hour");
} else if score > 30 {
warn!("[ip_score] banning ip={ip} duration=30m score={score}");
block_peer(
db,
ip.to_string(),
signature.to_string(),
wallet_key.to_string(),
)
.await;
spawn_unban(
db.clone(),
ip.to_string(),
signature.to_string(),
wallet_key.to_string(),
Duration::from_secs(1800),
);
return format!("IP {ip} banned for 30 minutes");
block_peer(db, ip.to_string(), signature.to_string(), wallet.clone()).await;
spawn_unban(
db.clone(),
ip.to_string(),
signature.to_string(),
wallet.clone(),
Duration::from_secs(1800),
);
return format!("IP {ip} banned for 30 minutes");
} else if score > 10 {
warn!("[ip_score] dropping connection ip={ip} score={score}");
// Low-level penalties disconnect the peer but do not add a ban
// record yet.
conn.drop_connection(connection_type, ip.to_string(), port);
return format!("IP {ip} dropped due to score {score}");
}
}
}
"No action taken".to_string()
}
// Low-level penalties disconnect the peer but do not add a ban
// record yet.
conn.drop_connection(connection_type, ip.to_string(), port);
if client_type == ClientType::Miner && connection_type == ConnectionType::Outgoing {
spawn_retry_dropped_outgoing(ip.to_string(), port);
}
return format!("IP {ip} dropped due to score {score}");
}
}
}
"No action taken".to_string()
}

View File

@ -4,6 +4,8 @@ use crate::records::ip_score::enums::InfractionType;
use crate::records::ip_score::get_score::get_ip_score_timestamp;
use crate::records::ip_score::penalty::issue_penalty;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::Arc;
use crate::Duration;
fn score_subject(ip: &str, client_type: &str) -> String {
@ -22,7 +24,7 @@ pub async fn update_ip_score(
infraction_type: InfractionType,
timestamp: u32,
db: &Db,
wallet_key: &str,
wallet: Arc<Wallet>,
) -> sled::Result<()> {
// Convert the incoming event into a new score and persist the latest
// score/timestamp pair before penalty handling runs.
@ -52,7 +54,7 @@ pub async fn update_ip_score(
// Penalty handling is driven from the updated score so actions like
// temporary bans always reflect the most recent infraction state.
let action = issue_penalty(score, ip, client_type, wallet_key, db).await;
let action = issue_penalty(score, ip, client_type, wallet, db).await;
if action != "No action taken" {
warn!("[ip_score] penalty ip={ip} client_type={client_type} subject={subject} infraction={infraction_type:?} previous_score={previous_score} new_score={score} action={action}");
}

View File

@ -1,161 +1,19 @@
use crate::blocks::block::DIFFICULTY_OFFSET;
use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::lazy_static;
use crate::Duration;
use crate::HashMap;
use crate::Mutex;
use crate::PathBuf;
pub const DIFFICULTY_AVERAGE_WINDOW: u32 = 254;
lazy_static! {
static ref AVERAGE_DATA: Mutex<HashMap<u32, (u32, u64)>> = Mutex::new(HashMap::new());
}
pub async fn load_initial_blocks(start: u32, stop: u32) {
// Rebuild the rolling average cache from disk, keeping only the
// most recent rolling difficulty window needed by the algorithm.
let mut cache = AVERAGE_DATA.lock().await;
*cache = HashMap::new(); // Clear and reset the cache
let (
_network_name,
_padded_base_coin,
file_ext,
_torrent_path,
_wallet_path,
block_path,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
for block_num in start..=stop {
let file_path = PathBuf::from(&block_path).join(format!("{block_num}.{file_ext}"));
if let Ok(file_content) = tokio::fs::read(file_path).await {
let timestamp = if file_content.len() >= 4 {
u32::from_le_bytes(file_content[0..4].try_into().unwrap_or_default())
} else {
0
};
let difficulty = if file_content.len() >= DIFFICULTY_OFFSET + 8 {
u64::from_le_bytes(
file_content[DIFFICULTY_OFFSET..DIFFICULTY_OFFSET + 8]
.try_into()
.unwrap_or_default(),
)
} else {
0
};
cache.insert(block_num, (timestamp, difficulty));
// Ensure only the configured rolling window is kept if starting from a larger range.
if cache.len() > DIFFICULTY_AVERAGE_WINDOW as usize {
let oldest_block = cache.keys().min().copied();
if let Some(oldest) = oldest_block {
cache.remove(&oldest);
}
}
}
}
}
pub async fn update_block_data(block_num: u32) {
let (
_network_name,
_padded_base_coin,
file_ext,
_torrent_path,
_wallet_path,
block_path,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
// Avoid re-reading blocks that are already present in the rolling cache.
let cache = AVERAGE_DATA.lock().await;
if cache.contains_key(&block_num) {
return;
}
drop(cache);
let file_path = PathBuf::from(&block_path).join(format!("{block_num}.{file_ext}"));
if let Ok(file_content) = tokio::fs::read(file_path).await {
let timestamp = if file_content.len() >= 4 {
u32::from_le_bytes(file_content[0..4].try_into().unwrap_or_default())
} else {
0
};
let difficulty = if file_content.len() >= DIFFICULTY_OFFSET + 8 {
u64::from_le_bytes(
file_content[DIFFICULTY_OFFSET..DIFFICULTY_OFFSET + 8]
.try_into()
.unwrap_or_default(),
)
} else {
0
};
// Reinsert under the cache lock and trim back to the rolling window.
let mut cache = AVERAGE_DATA.lock().await;
cache.insert(block_num, (timestamp, difficulty));
if cache.len() > DIFFICULTY_AVERAGE_WINDOW as usize {
let oldest_block = cache.keys().min().copied();
if let Some(oldest) = oldest_block {
cache.remove(&oldest);
}
}
}
}
async fn calculate_time_differences(latest_timestamp: u32) -> Vec<Duration> {
// Build the interval list from the cached block timestamps plus
// the candidate block timestamp being evaluated right now.
let cache = AVERAGE_DATA.lock().await;
let mut timestamps: Vec<_> = cache.values().map(|&(timestamp, _)| timestamp).collect();
timestamps.push(latest_timestamp);
timestamps.sort();
timestamps
.windows(2)
.map(|w| Duration::from_secs((w[1] - w[0]) as u64))
.collect()
}
async fn calculate_mean_difficulty() -> u64 {
// Difficulty smoothing uses the rolling mean of the cached prior
// block difficulties rather than just the current tip value.
let cache = AVERAGE_DATA.lock().await;
let difficulties: Vec<_> = cache.values().map(|&(_, difficulty)| difficulty).collect();
if difficulties.is_empty() {
0
} else {
let total: u128 = difficulties
.iter()
.map(|&difficulty| difficulty as u128)
.sum();
let average = total / difficulties.len() as u128;
average.min(u64::MAX as u128) as u64
}
}
pub async fn calculate_averages(current_timestamp: u32) -> (u64, Duration) {
// Combine the rolling time intervals and rolling mean difficulty
// into the aggregate inputs used by difficulty adjustment.
let time_differences = calculate_time_differences(current_timestamp).await;
let total_duration: Duration = time_differences.iter().sum();
let average_duration = total_duration / (time_differences.len() as u32);
let mean_difficulty = calculate_mean_difficulty().await;
(mean_difficulty, average_duration)
}
use crate::records::memory::chain_state::cached_asert_genesis_anchor;
use crate::records::unpack_block::unpack_header::load_block_header;
pub async fn asert_genesis_anchor() -> Option<(u32, u32, u64)> {
// ASERT uses genesis as a fixed consensus anchor, so long-term drift
// toward the 15-second schedule cannot be forgotten by a rolling cache.
if let Some(anchor) = cached_asert_genesis_anchor().await {
return Some(anchor);
}
match load_block_header(0).await {
Ok(header) => Some((
0,
header.unmined_block.timestamp,
header.unmined_block.next_block_difficulty,
)),
Err(_) => None,
}
}

View File

@ -0,0 +1,133 @@
use crate::blocks::block::VrfBlock;
use crate::records::block_height::get_block_height::get_height;
use crate::records::unpack_block::unpack_header::load_block_header;
use crate::{lazy_static, HashMap, Mutex};
const RECENT_HEADER_CACHE_DEPTH: u32 = 32;
#[derive(Clone, Default)]
struct ChainStateCache {
height: Option<u32>,
tip_header: Option<VrfBlock>,
tip_hash: Option<String>,
genesis_anchor: Option<(u32, u32, u64)>,
recent_headers: HashMap<u32, VrfBlock>,
}
lazy_static! {
static ref CHAIN_STATE: Mutex<ChainStateCache> = Mutex::new(ChainStateCache::default());
}
fn recent_floor(height: u32) -> u32 {
height.saturating_sub(RECENT_HEADER_CACHE_DEPTH)
}
fn cache_genesis_anchor(cache: &mut ChainStateCache, height: u32, header: &VrfBlock) {
if height == 0 {
cache.genesis_anchor = Some((
0,
header.unmined_block.timestamp,
header.unmined_block.next_block_difficulty,
));
}
}
fn prune_recent_headers(cache: &mut ChainStateCache, height: u32) {
let floor = recent_floor(height);
cache
.recent_headers
.retain(|cached_height, _| *cached_height == 0 || *cached_height >= floor);
}
pub async fn rebuild_chain_state_cache(db: &crate::sled::Db) -> Result<(), String> {
let height = get_height(db);
let mut loaded_headers = HashMap::new();
let mut genesis_anchor = None;
let mut tip_header = None;
let mut tip_hash = None;
if let Ok(genesis_header) = load_block_header(0).await {
genesis_anchor = Some((
0,
genesis_header.unmined_block.timestamp,
genesis_header.unmined_block.next_block_difficulty,
));
loaded_headers.insert(0, genesis_header);
}
let floor = recent_floor(height);
for block_height in floor..=height {
if block_height == 0 && loaded_headers.contains_key(&0) {
continue;
}
if let Ok(header) = load_block_header(block_height).await {
if block_height == height {
tip_hash = Some(header.hash().await);
tip_header = Some(header.clone());
}
loaded_headers.insert(block_height, header);
}
}
if height == 0 {
if let Some(header) = loaded_headers.get(&0) {
tip_hash = Some(header.hash().await);
tip_header = Some(header.clone());
}
}
let mut cache = CHAIN_STATE.lock().await;
*cache = ChainStateCache {
height: Some(height),
tip_header,
tip_hash,
genesis_anchor,
recent_headers: loaded_headers,
};
Ok(())
}
pub async fn update_chain_state_after_save(height: u32, header: VrfBlock, header_hash: String) {
let mut cache = CHAIN_STATE.lock().await;
cache.height = Some(height);
cache.tip_header = Some(header.clone());
cache.tip_hash = Some(header_hash);
cache.recent_headers.insert(height, header.clone());
cache_genesis_anchor(&mut cache, height, &header);
prune_recent_headers(&mut cache, height);
}
pub async fn cached_chain_height() -> Option<u32> {
CHAIN_STATE.lock().await.height
}
pub async fn cached_tip_header(expected_height: u32) -> Option<VrfBlock> {
let cache = CHAIN_STATE.lock().await;
if cache.height == Some(expected_height) {
cache.tip_header.clone()
} else {
None
}
}
pub async fn cached_tip_hash(expected_height: u32) -> Option<String> {
let cache = CHAIN_STATE.lock().await;
if cache.height == Some(expected_height) {
cache.tip_hash.clone()
} else {
None
}
}
pub async fn cached_header(block_height: u32) -> Option<VrfBlock> {
CHAIN_STATE
.lock()
.await
.recent_headers
.get(&block_height)
.cloned()
}
pub async fn cached_asert_genesis_anchor() -> Option<(u32, u32, u64)> {
CHAIN_STATE.lock().await.genesis_anchor
}

View File

@ -2,9 +2,16 @@ 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::records::memory::network_mapping::monitor::{MONITOR_ACTION_ADD, MONITOR_ACTION_REMOVE};
use crate::records::memory::network_mapping::structs::{MonitorAddressParams, SignedMonitorEdit};
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::response_channels::{
delete_entry, reserve_entry_with_context, Command,
};
use crate::records::memory::structs::{Connection, StoreConnectionParams};
use crate::rpc::client::handshake::connect_and_handshake;
use crate::rpc::client::handshake_processing::{bootstrap_peer_discovery, BootstrapParams};
use crate::rpc::client::structs::Connect;
use crate::rpc::command_maps::RPC_BLOCK_HEIGHT;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
@ -40,7 +47,7 @@ use crate::records::memory::structs::{ConnectionInfo, ConnectionKey};
#[derive(Clone)]
struct ReconnectContext {
db: Db,
wallet_key: String,
wallet: Arc<Wallet>,
map: Arc<Mutex<Command>>,
}
@ -62,26 +69,74 @@ fn finish_reconnect() {
RECONNECT_IN_PROGRESS.store(false, AtomicOrdering::SeqCst);
}
pub async fn set_reconnect_context(db: Db, wallet_key: String, map: Arc<Mutex<Command>>) {
pub async fn set_reconnect_context(db: Db, wallet: Arc<Wallet>, 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,
});
*context = Some(ReconnectContext { db, wallet, map });
}
async fn reconnect_dropped_outgoing(excluded_ip: &str) {
async fn reconnect_replacement_inner(excluded_ip: &str) {
// 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;
guard.as_ref().and_then(|conn| {
conn.connection_map.iter().find_map(|(key, info)| {
if key.ip == excluded_ip_bytes {
return None;
}
if ClientType::from_bytes(&info.client_type) != Some(ClientType::Miner) {
return None;
}
if !info.ready {
return None;
}
let ip = binary_to_ip(key.ip.clone());
let connections_key = format!("{}:{}", ip, key.port);
Some((connections_key, Arc::clone(&info.stream)))
})
})
};
let Some((connections_key, stream)) = live_connection else {
warn!("[reconnect] no live stream available for bootstrap recovery");
return;
};
let bootstrap_params = BootstrapParams {
stream,
connections_key,
wallet: context.wallet,
db: context.db,
map: context.map,
first: false,
run_startup_sync: false,
};
if let Err(err) = bootstrap_peer_discovery(bootstrap_params).await {
warn!("[reconnect] bootstrap recovery failed: {err}");
}
}
async fn retry_dropped_outgoing(ip: String, port: u16) {
if !try_start_reconnect() {
warn!("[reconnect] replacement attempt already in progress, skipping duplicate request");
warn!("[reconnect] reconnect 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()
@ -92,47 +147,53 @@ async fn reconnect_dropped_outgoing(excluded_ip: &str) {
return;
};
let excluded_ip_bytes = ip_to_binary(excluded_ip);
let live_connection = {
let guard = CONNECTIONS.read().await;
guard.as_ref().and_then(|conn| {
conn.connection_map.iter().find_map(|(key, info)| {
if key.ip == excluded_ip_bytes {
return None;
}
if ClientType::from_bytes(&info.client_type) != Some(ClientType::Miner) {
return None;
}
let ip = binary_to_ip(key.ip.clone());
let connections_key = format!("{}:{}", ip, key.port);
Some((connections_key, Arc::clone(&info.stream)))
})
})
};
let addr_string = format!("{ip}:{port}");
for attempt in 1..=3 {
sleep(Duration::from_secs(30)).await;
let Some((connections_key, stream)) = live_connection else {
warn!("[reconnect] no live stream available for bootstrap recovery");
return;
};
let socket_addr = match addr_string.parse() {
Ok(addr) => addr,
Err(err) => {
warn!("[reconnect] invalid dropped peer address {addr_string}: {err}");
break;
}
};
let bootstrap_params = BootstrapParams {
stream,
connections_key,
wallet_key: context.wallet_key,
db: context.db,
map: context.map,
first: false,
};
let connect = Connect {
addr: socket_addr,
node_ip: addr_string.clone(),
wallet: context.wallet.clone(),
db: context.db.clone(),
map: context.map.clone(),
first: false,
};
if let Err(err) = bootstrap_peer_discovery(bootstrap_params).await {
warn!("[reconnect] bootstrap recovery failed: {err}");
match connect_and_handshake(connect).await {
Ok(()) => {
info!("[reconnect] reconnected dropped peer {addr_string} on attempt {attempt}");
return;
}
Err(err) => {
warn!(
"[reconnect] failed to reconnect dropped peer {addr_string} on attempt {attempt}/3: {err}"
);
}
}
}
reconnect_replacement_inner(&ip).await;
}
.await;
finish_reconnect();
}
pub fn spawn_retry_dropped_outgoing(ip: String, port: u16) {
tokio::spawn(async move {
retry_dropped_outgoing(ip, port).await;
});
}
pub fn spawn_reconnect_bootstrap(params: BootstrapParams) {
if !try_start_reconnect() {
warn!("[reconnect] bootstrap recovery already in progress, skipping duplicate request");
@ -164,8 +225,8 @@ impl Connection {
port,
stream,
client_type,
wallet_address,
command_map,
wallet_short_address,
command_map: _,
} = params;
let ip_bytes = ip_to_binary(&ip);
@ -201,8 +262,7 @@ impl Connection {
return false;
}
let address = Wallet::long_address_to_bytes(wallet_address);
if address.len() != Wallet::ADDRESS_BYTES_LENGTH {
if !Wallet::short_address_validation(&wallet_short_address) {
return false;
}
let connection_info = ConnectionInfo::new(
@ -211,13 +271,10 @@ impl Connection {
port,
stream.clone(),
client_type.as_bytes(),
address,
wallet_short_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
}
@ -236,6 +293,14 @@ impl Connection {
};
let removed = self.connection_map.remove(&connection_key);
if let Some(connection_info) = removed.as_ref() {
if ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner) {
spawn_monitor_update(
ip.clone(),
MONITOR_ACTION_REMOVE,
connection_info.wallet_short_address.clone(),
port,
);
}
let stream = Arc::clone(&connection_info.stream);
tokio::spawn(async move {
let mut stream_guard = stream.lock().await;
@ -265,30 +330,34 @@ impl Connection {
command_map: Arc<Mutex<Command>>,
) {
tokio::spawn(async move {
let connection_key = ConnectionKey {
connection_type: connection_type.as_bytes(),
ip: ip_to_binary(&ip),
port,
};
let mut consecutive_failures = 0_u8;
loop {
sleep(Duration::from_secs(30)).await;
let still_registered = {
let still_monitoring_same_stream = {
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)
})
.and_then(|conn| conn.connection_map.get(&connection_key))
.map(|connection_info| Arc::ptr_eq(&connection_info.stream, &stream))
.unwrap_or(false)
};
if !still_registered {
if !still_monitoring_same_stream {
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;
let (checkup_key, _checkup_tx, checkup_rx_mutex) = reserve_entry_with_context(
command_map.clone(),
Some(RPC_BLOCK_HEIGHT),
Some(format!("{ip}:{port}")),
)
.await;
// Send a lightweight ping message and wait for the reply
// routed back through the shared response hashmap.
@ -305,6 +374,7 @@ impl Connection {
match response_result {
Ok(Some(_reply)) => {
consecutive_failures = 0;
info!(
"[connection_manager] liveness check ok: type={} peer={}:{}",
connection_type.as_str(),
@ -313,42 +383,55 @@ impl Connection {
);
}
_ => {
let still_registered = {
let still_monitoring_same_stream = {
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)
.and_then(|conn| conn.connection_map.get(&connection_key))
.map(|connection_info| {
Arc::ptr_eq(&connection_info.stream, &stream)
})
.unwrap_or(false)
};
if !still_registered {
if !still_monitoring_same_stream {
delete_entry(command_map.clone(), checkup_key).await;
break;
}
// Timed-out or missing replies drop the connection,
// and outgoing peers trigger replacement discovery.
consecutive_failures = consecutive_failures.saturating_add(1);
warn!(
"[connection_manager] liveness check failed: type={} peer={}:{}",
"[connection_manager] liveness check failed: type={} peer={}:{} attempt={}/3",
connection_type.as_str(),
ip,
port
port,
consecutive_failures
);
delete_entry(command_map.clone(), checkup_key).await;
if consecutive_failures < 3 {
continue;
}
// Three consecutive timed-out or missing replies drop
// the connection, and outgoing peers trigger
// replacement discovery.
let mut guard = CONNECTIONS.write().await;
if let Some(conn) = guard.as_mut() {
conn.drop_connection(connection_type, ip.clone(), port);
let should_drop = conn
.connection_map
.get(&connection_key)
.map(|connection_info| {
Arc::ptr_eq(&connection_info.stream, &stream)
})
.unwrap_or(false);
if should_drop {
conn.drop_connection(connection_type, ip.clone(), port);
}
}
drop(guard);
if connection_type == ConnectionType::Outgoing {
reconnect_dropped_outgoing(&ip).await;
spawn_retry_dropped_outgoing(ip.clone(), port);
}
break;
}
@ -377,30 +460,142 @@ impl Connection {
.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 mark_wallet_registry_synced(&mut self, key: &str) -> bool {
let Some((ip, port)) = split_ip_port_key(key) else {
return false;
};
let ip_bytes = ip_to_binary(&ip);
for (connection_key, info) in self.connection_map.iter_mut() {
if connection_key.ip == ip_bytes && connection_key.port == port {
info.wallet_registry_synced = true;
return true;
}
}
false
}
pub fn mark_network_map_synced(&mut self, key: &str) -> bool {
let Some((ip, port)) = split_ip_port_key(key) else {
return false;
};
let ip_bytes = ip_to_binary(&ip);
for (connection_key, info) in self.connection_map.iter_mut() {
if connection_key.ip == ip_bytes && connection_key.port == port {
info.network_map_synced = true;
return true;
}
}
false
}
pub fn mark_operational(&mut self, key: &str, command_map: Arc<Mutex<Command>>) -> bool {
let Some((ip, port)) = split_ip_port_key(key) else {
return false;
};
let ip_bytes = ip_to_binary(&ip);
for (connection_key, info) in self.connection_map.iter_mut() {
if connection_key.ip == ip_bytes && connection_key.port == port {
if info.ready {
return true;
}
if ClientType::from_bytes(&info.client_type) != Some(ClientType::Miner)
|| !info.wallet_registry_synced
|| !info.network_map_synced
{
return false;
}
info.ready = true;
spawn_monitor_update(
ip.clone(),
MONITOR_ACTION_ADD,
info.wallet_short_address.clone(),
port,
);
Connection::client_checkup(
Arc::clone(&info.stream),
ConnectionType::from_bytes(&info.connection_type)
.unwrap_or(ConnectionType::Incoming),
ip,
port,
command_map,
);
return true;
}
}
false
}
pub fn count_ready_miner_connections(&self) -> usize {
self.connection_map
.values()
.filter(|info| {
ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) && info.ready
})
.count()
}
// Return ready peer streams so broadcast-style paths do not send
// network-wide traffic to peers still completing startup sync.
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)
&& connection_info.ready
})
.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.
// Return ready non-client peer streams so registry rebroadcasts only fan
// out through initialized chain peers.
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)
&& connection_info.ready
})
.map(|connection_info| Arc::clone(&connection_info.stream))
.collect()
}
pub fn get_all_ready_peer_streams_with_keys(&self) -> Vec<(String, Arc<Mutex<TcpStream>>)> {
self.connection_map
.iter()
.filter_map(|(key, connection_info)| {
if ClientType::from_bytes(&connection_info.client_type) != Some(ClientType::Miner)
|| !connection_info.ready
{
return None;
}
let ip = binary_to_ip(key.ip.clone());
let connections_key = format!("{}:{}", ip, key.port);
Some((connections_key, Arc::clone(&connection_info.stream)))
})
.collect()
}
pub fn get_startup_synced_peer_streams_with_keys(
&self,
) -> Vec<(String, Arc<Mutex<TcpStream>>)> {
self.connection_map
.iter()
.filter_map(|(key, connection_info)| {
if ClientType::from_bytes(&connection_info.client_type) != Some(ClientType::Miner)
|| connection_info.ready
|| !connection_info.wallet_registry_synced
|| !connection_info.network_map_synced
{
return None;
}
let ip = binary_to_ip(key.ip.clone());
let connections_key = format!("{}:{}", ip, key.port);
Some((connections_key, 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);
@ -438,6 +633,26 @@ impl Connection {
})
}
pub async fn get_wallet_for_connection_key(key: &str) -> Option<String> {
let (ip, port) = split_ip_port_key(key)?;
let lock = CONNECTIONS.read().await;
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(info.wallet_short_address.clone())
} else {
None
}
})
}
// Build the serialized connection key for a live stream when only
// the stream handle is known.
pub fn connection_key_for_stream(&self, stream: &Arc<Mutex<TcpStream>>) -> Option<String> {
@ -498,9 +713,18 @@ impl Connection {
// 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)> {
pub fn get_random_connection(
&self,
excluded_key: Option<&str>,
eligible_ips: Option<&[String]>,
) -> Option<(Vec<u8>, u16)> {
let mut rng = thread_rng();
let excluded = excluded_key.and_then(split_ip_port_key);
let is_eligible = |key_ip: &[u8]| {
eligible_ips
.map(|ips| ips.iter().any(|ip| ip_to_binary(ip) == key_ip))
.unwrap_or(true)
};
if let Some((key, _info)) = self
.connection_map
@ -508,6 +732,8 @@ impl Connection {
.filter(|(key, info)| {
ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Incoming)
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
&& info.ready
&& is_eligible(&key.ip)
&& excluded
.as_ref()
.map(|(ip, _)| key.ip != ip_to_binary(ip))
@ -524,6 +750,8 @@ impl Connection {
.filter(|(key, info)| {
ConnectionType::from_bytes(&key.connection_type) == Some(ConnectionType::Outgoing)
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
&& info.ready
&& is_eligible(&key.ip)
&& excluded
.as_ref()
.map(|(ip, _)| key.ip != ip_to_binary(ip))
@ -537,6 +765,63 @@ impl Connection {
}
}
fn spawn_monitor_update(ip: String, action: u8, monitored_address: String, port: u16) {
tokio::spawn(async move {
let context = {
let guard = RECONNECT_CONTEXT.lock().await;
guard.clone()
};
let Some(context) = context else {
return;
};
if !Wallet::short_address_validation(&monitored_address) {
return;
}
let monitoring_address = context.wallet.saved.short_address.clone();
if monitored_address == monitoring_address {
return;
}
let timestamp = crate::Utc::now().timestamp_millis() as u64;
let signature = NodeInfo::monitor_signature(
action,
&monitored_address,
&monitoring_address,
&ip,
timestamp,
&context.wallet,
)
.await;
let edit = SignedMonitorEdit {
action,
monitored_address: monitored_address.clone(),
monitoring_address,
target_ip: ip.clone(),
modified_timestamp: timestamp,
modified_signature: signature,
};
let params = MonitorAddressParams {
map: context.map.clone(),
edit,
remote_ip: String::new(),
db: context.db.clone(),
wallet: context.wallet.clone(),
connections_key: format!("{ip}:{port}"),
};
let _ = if action == MONITOR_ACTION_ADD {
NodeInfo::add_monitor(params).await
} else {
NodeInfo::remove_monitor(params).await
};
NodeInfo::broadcast_address_state(
context.map.clone(),
&monitored_address,
"",
&format!("{ip}:{port}"),
)
.await;
});
}
lazy_static! {
pub static ref CONNECTIONS: Arc<RwLock<Option<Connection>>> = Arc::new(RwLock::new(None));
}
@ -561,8 +846,75 @@ pub async fn outgoing_connection_count() -> usize {
}
pub async fn peer_connection_count() -> usize {
// Mining only needs proof that this node is connected to the live
// network; incoming and outgoing miner peers both satisfy that.
// Mining needs at least one fully initialized miner peer. A raw socket
// is not enough because block validation depends on wallet registry and
// network-map state being synced first.
CONNECTIONS
.read()
.await
.as_ref()
.map(|connection| connection.count_ready_miner_connections())
.unwrap_or(0)
}
pub async fn mark_peer_wallet_registry_synced(key: &str) -> bool {
CONNECTIONS
.write()
.await
.as_mut()
.map(|connection| connection.mark_wallet_registry_synced(key))
.unwrap_or(false)
}
pub async fn mark_peer_network_map_synced(key: &str) -> bool {
CONNECTIONS
.write()
.await
.as_mut()
.map(|connection| connection.mark_network_map_synced(key))
.unwrap_or(false)
}
pub async fn mark_peer_operational(key: &str, map: Arc<Mutex<Command>>) -> bool {
CONNECTIONS
.write()
.await
.as_mut()
.map(|connection| connection.mark_operational(key, map))
.unwrap_or(false)
}
pub async fn peer_is_operational(key: &str) -> bool {
let Some((ip, port)) = split_ip_port_key(key) else {
return false;
};
let ip_bytes = ip_to_binary(&ip);
CONNECTIONS
.read()
.await
.as_ref()
.and_then(|connection| {
connection
.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(info.ready)
} else {
None
}
})
})
.unwrap_or(false)
}
pub async fn live_miner_peer_streams() -> Vec<(String, Arc<Mutex<TcpStream>>)> {
// Snapshot consensus and recovery checks vote only across currently
// connected miner peers, regardless of incoming/outgoing direction.
CONNECTIONS
.read()
.await
@ -570,11 +922,29 @@ pub async fn peer_connection_count() -> usize {
.map(|connection| {
connection
.connection_map
.values()
.filter(|info| ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner))
.count()
.iter()
.filter_map(|(key, info)| {
if ClientType::from_bytes(&info.client_type) != Some(ClientType::Miner)
|| !info.ready
{
return None;
}
let ip = binary_to_ip(key.ip.clone());
let connections_key = format!("{}:{}", ip, key.port);
Some((connections_key, Arc::clone(&info.stream)))
})
.collect()
})
.unwrap_or(0)
.unwrap_or_default()
}
pub async fn startup_synced_peer_streams() -> Vec<(String, Arc<Mutex<TcpStream>>)> {
CONNECTIONS
.read()
.await
.as_ref()
.map(|connection| connection.get_startup_synced_peer_streams_with_keys())
.unwrap_or_default()
}
pub async fn get_client_type_from_memory(key: &str) -> Option<ClientType> {

View File

@ -1,7 +1,8 @@
use super::*;
pub async fn signature_exists(signature: &str, hash: &str) -> Result<bool> {
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
// Check every mempool table because the signature column names differ by
// transaction type, especially for two-party swaps and loans.
@ -36,7 +37,11 @@ pub async fn signature_exists(signature: &str, hash: &str) -> Result<bool> {
}
pub async fn transaction_by_signature(signature: &str) -> RpcResponse {
let client = DB.get().expect("DB not initialized");
let client_handle = match db_client().await {
Ok(client) => client,
Err(_) => return RpcResponse::Binary(Vec::new()),
};
let client = client_handle.as_ref();
// Return the original serialized transaction bytes, not a reconstructed
// row, so RPC callers receive the same payload that would enter a block.
@ -85,7 +90,11 @@ pub async fn transaction_by_signature(signature: &str) -> RpcResponse {
}
pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse {
let client = DB.get().expect("DB not initialized");
let client_handle = match db_client().await {
Ok(client) => client,
Err(_) => return RpcResponse::Binary(Vec::new()),
};
let client = client_handle.as_ref();
// Canonicalize vanity aliases before querying pending rows.
let addresses = canonical_mempool_addresses(db, address);
@ -139,8 +148,130 @@ pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse {
RpcResponse::Binary(bytes)
}
pub async fn latest_pending_txids_by_address(db: &Db, address: &str, limit: usize) -> Vec<Vec<u8>> {
if limit == 0 {
return Vec::new();
}
let client_handle = match db_client().await {
Ok(client) => client,
Err(_) => return Vec::new(),
};
let client = client_handle.as_ref();
let addresses = canonical_mempool_addresses(db, address);
let limit = i64::try_from(limit).unwrap_or(i64::MAX);
let rows = match client
.query(
r#"
SELECT hash FROM (
SELECT DISTINCT ON (hash) hash, time, source_id FROM (
SELECT hash, time, id AS source_id FROM transfer
WHERE (sender = ANY($1) OR receiver = ANY($1)) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM token
WHERE creator = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM issue_token
WHERE creator = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM burn
WHERE address = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM nft
WHERE creator = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM marketing
WHERE advertiser = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM vanity_address
WHERE address = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM swap
WHERE (sender1 = ANY($1) OR sender2 = ANY($1)) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM loan_contract
WHERE (lender = ANY($1) OR borrower = ANY($1)) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM loan_payment
WHERE address = ANY($1) AND processed = false
UNION ALL
SELECT hash, time, id AS source_id FROM collateral_claim
WHERE address = ANY($1) AND processed = false
) AS pending
ORDER BY hash, time DESC, source_id DESC
) AS deduped
ORDER BY time DESC, source_id DESC
LIMIT $2
"#,
&[&addresses, &limit],
)
.await
{
Ok(rows) => rows,
Err(_) => return Vec::new(),
};
let mut txids = rows
.into_iter()
.filter_map(|row| {
let hash: String = row.get("hash");
decode(&hash).ok().filter(|bytes| bytes.len() == 32)
})
.collect::<Vec<_>>();
txids.truncate(limit as usize);
txids
}
pub async fn pending_transaction_by_txid(txid: &[u8]) -> Option<Vec<u8>> {
if txid.len() != 32 {
return None;
}
let client_handle = db_client().await.ok()?;
let client = client_handle.as_ref();
let hash = crate::encode(txid);
let row = client
.query_opt(
r#"
SELECT original FROM (
SELECT original FROM transfer WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM token WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM issue_token WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM burn WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM nft WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM marketing WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM vanity_address WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM swap WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM loan_contract WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM loan_payment WHERE hash = $1 AND processed = false
UNION ALL
SELECT original FROM collateral_claim WHERE hash = $1 AND processed = false
) AS subquery LIMIT 1
"#,
&[&hash],
)
.await
.ok()?;
row.map(|row| row.get("original"))
}
pub async fn largest_fee() -> RpcResponse {
let client = DB.get().expect("DB not initialized");
let client_handle = match db_client().await {
Ok(client) => client,
Err(_) => return RpcResponse::Binary(0u32.to_le_bytes().to_vec()),
};
let client = client_handle.as_ref();
// Swaps have two possible fees, so both sides are included in the max.
let row = match client
@ -191,7 +322,8 @@ async fn pending_saved_loan_payment_balance(
addresses: &[String],
coin: &str,
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
let rows = client
.query(
r#"
@ -244,7 +376,8 @@ pub async fn get_coin_balance(
address: &str,
coin: &str,
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
// Pending-balance checks use canonical addresses so vanity and short
// address inputs see the same outgoing obligations.
let addresses = canonical_mempool_addresses(db, address);
@ -324,7 +457,8 @@ 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 client_handle = db_client().await?;
let client = client_handle.as_ref();
let addresses = canonical_mempool_addresses(db, address);
// Base coin projection includes direct base transfers plus all fees and
@ -387,10 +521,11 @@ pub async fn get_basecoin_balance(
pub async fn get_pending_payments_for_contract(
contract_hash: &str,
) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
let client = DB.get().expect("DB not initialized");
let client_handle = db_client().await?;
let client = client_handle.as_ref();
// Loan verification uses this to prevent pending payments from exceeding
// what the contract still owes.
// Mempool/UI callers can use this for unconfirmed payment visibility.
// Consensus validation must use confirmed contract payments only.
let row = client
.query_one(
r#"
@ -408,7 +543,11 @@ pub async fn get_pending_payments_for_contract(
}
pub async fn total_transactions() -> RpcResponse {
let client = DB.get().expect("DB not initialized");
let client_handle = match db_client().await {
Ok(client) => client,
Err(_) => return RpcResponse::Binary(vec![0; 8]),
};
let client = client_handle.as_ref();
// Count rows across all mempool tables, including processed rows that may
// still be retained briefly for orphan rollback.
let row = match client

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