Compare commits
29 Commits
Contractle
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
7e99a98643 | |
|
|
9cc17ac0b5 | |
|
|
0e852e0e69 | |
|
|
b8c8e47b69 | |
|
|
f651fda2dd | |
|
|
a40ec3a06e | |
|
|
7181cc4e26 | |
|
|
a4c4518adf | |
|
|
38dabfa27f | |
|
|
0845fd3ba6 | |
|
|
64df13519a | |
|
|
f071d728a3 | |
|
|
fb5362ae3a | |
|
|
d07faa57ed | |
|
|
13ae207739 | |
|
|
04b93275fa | |
|
|
a621522170 | |
|
|
413adbf241 | |
|
|
5c4c1baf7e | |
|
|
3358e2e95a | |
|
|
0aea32a2d5 | |
|
|
6cd01d1345 | |
|
|
9f3f53ca34 | |
|
|
7cd421e142 | |
|
|
88b043cf41 | |
|
|
72b894d6cb | |
|
|
4b66a2bd54 | |
|
|
ddc4cef037 | |
|
|
c2f907a36a |
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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]`
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
let lender =
|
||||
match resolve_wallet_address_input(json["lender"].as_str().unwrap_or_default(), &wallet)
|
||||
.await
|
||||
{
|
||||
Ok(address) => address,
|
||||
Err(_) => {
|
||||
println!("Transaction is not valid. Lender address is not a valid short 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}"
|
||||
|
|
|
|||
|
|
@ -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,11 +92,13 @@ 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())
|
||||
let sender1 =
|
||||
match resolve_wallet_address_input(json["sender1"].as_str().unwrap_or_default(), &wallet)
|
||||
.await
|
||||
{
|
||||
Ok(address) => address,
|
||||
Err(_) => {
|
||||
println!("sender1 wallet invalid");
|
||||
Err(err) => {
|
||||
println!("sender1 wallet invalid: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
@ -106,11 +108,13 @@ async fn main() {
|
|||
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())
|
||||
let sender2 =
|
||||
match resolve_wallet_address_input(json["sender2"].as_str().unwrap_or_default(), &wallet)
|
||||
.await
|
||||
{
|
||||
Ok(address) => address,
|
||||
Err(_) => {
|
||||
println!("sender2 wallet invalid");
|
||||
Err(err) => {
|
||||
println!("sender2 wallet invalid: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
let mut target = anchor_target as u128 * factor.max(1) as u128;
|
||||
|
||||
if shifts >= 0 {
|
||||
if shifts >= 64 {
|
||||
return u64::MAX;
|
||||
}
|
||||
|
||||
// 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)
|
||||
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;
|
||||
}
|
||||
|
||||
// Adjust difficulty based on the latest saved block averages.
|
||||
target >>= ASERT_RADIX_BITS as u32;
|
||||
target.clamp(1, u64::MAX as u128) as u64
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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}]: ");
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)],
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +39,16 @@ pub async fn create_genesis_block(
|
|||
return;
|
||||
}
|
||||
};
|
||||
handle_response_and_save_torrent(0, &db, torrent, wallet_key, map.clone(), false)
|
||||
handle_response_and_save_torrent(
|
||||
0,
|
||||
&db,
|
||||
torrent,
|
||||
wallet,
|
||||
map.clone(),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, ¶ms.db, wallet_key).await?;
|
||||
torrent.verify(height, ¶ms.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(¶ms.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(¶ms.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
|
|||
¶ms,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(¶ms.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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
¶ms.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,
|
||||
¶ms.db,
|
||||
torrent,
|
||||
wallet_key,
|
||||
wallet.clone(),
|
||||
params.map.clone(),
|
||||
true,
|
||||
params.node_syncing,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
if get_height(¶ms.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}"
|
||||
|
|
|
|||
|
|
@ -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(¶ms.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
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
¶ms.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(¶ms, 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(¶ms.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(¶ms.db) < height_before_window_check
|
||||
{
|
||||
let height_after_window_check = get_height(¶ms.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(¶ms.db);
|
||||
match replay_staged_torrents(¶ms, 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(¶ms, 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(¶ms.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();
|
||||
}
|
||||
|
||||
if result.is_ok() {
|
||||
info!("[orphan] orphan check completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, ¶ms.db).await;
|
||||
rebuild_chain_state_cache(¶ms.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(¶ms.db).await?;
|
||||
save_new_blocks(¶ms, replay_to_height, wallet_key, true_start_height).await?;
|
||||
NodeInfo::rebuild_mined_counts_from_chain(¶ms.db).await?;
|
||||
// Counts are corrected incrementally: rollback decrements each removed
|
||||
// block above, and replay/save increments each accepted replacement.
|
||||
save_new_blocks(¶ms, replay_to_height, wallet, true_start_height).await?;
|
||||
rebuild_chain_state_cache(¶ms.db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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::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;
|
||||
|
|
@ -84,8 +85,11 @@ pub async fn undo_borrower_transaction(
|
|||
.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(decode(&tx_hash).map_err(|e| format!("Error decoding txid: {e}"))?)
|
||||
.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
|
||||
|
|
@ -93,7 +97,6 @@ pub async fn undo_borrower_transaction(
|
|||
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) {
|
||||
let _ = remove_nft_history_entry(db, &loan_coin, &tx_hash_bytes);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,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::rpc::commands::transaction_by_txid::request_transaction_by_txid;
|
||||
use crate::rpc::responses::RpcResponse;
|
||||
use crate::sled::Db;
|
||||
|
|
@ -85,13 +86,12 @@ pub async fn undo_collateral_transaction(
|
|||
// 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();
|
||||
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();
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}"))?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
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::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_key: &str,
|
||||
wallet: Arc<Wallet>,
|
||||
db: &Db,
|
||||
) -> String {
|
||||
// Penalties only matter for active known connections, so resolve the
|
||||
|
|
@ -20,7 +22,7 @@ pub async fn issue_penalty(
|
|||
return "No action taken".to_string();
|
||||
};
|
||||
|
||||
let signature = sign_ip_to_ban(ip, wallet_key).await;
|
||||
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)
|
||||
|
|
@ -29,65 +31,41 @@ pub async fn issue_penalty(
|
|||
// 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;
|
||||
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;
|
||||
block_peer(db, ip.to_string(), signature.to_string(), wallet.clone()).await;
|
||||
spawn_unban(
|
||||
db.clone(),
|
||||
ip.to_string(),
|
||||
signature.to_string(),
|
||||
wallet_key.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;
|
||||
block_peer(db, ip.to_string(), signature.to_string(), wallet.clone()).await;
|
||||
spawn_unban(
|
||||
db.clone(),
|
||||
ip.to_string(),
|
||||
signature.to_string(),
|
||||
wallet_key.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;
|
||||
block_peer(db, ip.to_string(), signature.to_string(), wallet.clone()).await;
|
||||
spawn_unban(
|
||||
db.clone(),
|
||||
ip.to_string(),
|
||||
signature.to_string(),
|
||||
wallet_key.to_string(),
|
||||
wallet.clone(),
|
||||
Duration::from_secs(1800),
|
||||
);
|
||||
return format!("IP {ip} banned for 30 minutes");
|
||||
|
|
@ -97,6 +75,9 @@ pub async fn issue_penalty(
|
|||
// 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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
use crate::records::memory::chain_state::cached_asert_genesis_anchor;
|
||||
use crate::records::unpack_block::unpack_header::load_block_header;
|
||||
|
||||
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 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,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,24 +69,14 @@ 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) {
|
||||
if !try_start_reconnect() {
|
||||
warn!("[reconnect] replacement attempt already in progress, skipping duplicate request");
|
||||
return;
|
||||
}
|
||||
|
||||
async {
|
||||
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 = {
|
||||
|
|
@ -103,6 +100,9 @@ async fn reconnect_dropped_outgoing(excluded_ip: &str) {
|
|||
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)))
|
||||
|
|
@ -118,21 +118,82 @@ async fn reconnect_dropped_outgoing(excluded_ip: &str) {
|
|||
let bootstrap_params = BootstrapParams {
|
||||
stream,
|
||||
connections_key,
|
||||
wallet_key: context.wallet_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] reconnect attempt already in progress, skipping duplicate request");
|
||||
return;
|
||||
}
|
||||
|
||||
async {
|
||||
let context = {
|
||||
let guard = RECONNECT_CONTEXT.lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
let Some(context) = context else {
|
||||
warn!("[reconnect] no reconnect context configured");
|
||||
return;
|
||||
};
|
||||
|
||||
let addr_string = format!("{ip}:{port}");
|
||||
for attempt in 1..=3 {
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
let socket_addr = match addr_string.parse() {
|
||||
Ok(addr) => addr,
|
||||
Err(err) => {
|
||||
warn!("[reconnect] invalid dropped peer address {addr_string}: {err}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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 {
|
||||
loop {
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
let still_registered = {
|
||||
let guard = CONNECTIONS.read().await;
|
||||
guard
|
||||
.as_ref()
|
||||
.map(|conn| {
|
||||
let connection_key = ConnectionKey {
|
||||
connection_type: connection_type.as_bytes(),
|
||||
ip: ip_to_binary(&ip),
|
||||
port,
|
||||
};
|
||||
conn.connection_map.contains_key(&connection_key)
|
||||
})
|
||||
let mut consecutive_failures = 0_u8;
|
||||
loop {
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
let still_monitoring_same_stream = {
|
||||
let guard = CONNECTIONS.read().await;
|
||||
guard
|
||||
.as_ref()
|
||||
.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() {
|
||||
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)))
|
||||
})
|
||||
.unwrap_or(0)
|
||||
.collect()
|
||||
})
|
||||
.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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use crate::sled::Db;
|
|||
use crate::wallets::structures::Wallet;
|
||||
use crate::HashMap;
|
||||
use crate::NoTls;
|
||||
use crate::{task, AtomicBool};
|
||||
use crate::{task, Arc, AtomicBool, RwLock};
|
||||
use anyhow::{anyhow, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::fs::File;
|
||||
|
|
@ -38,7 +38,7 @@ lazy_static! {
|
|||
static ref CLEANUP_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
pub static DB: OnceCell<Client> = OnceCell::new();
|
||||
pub static DB: OnceCell<RwLock<Arc<Client>>> = OnceCell::new();
|
||||
|
||||
pub const EPOCH_ROW_CAP: i64 = 100_000;
|
||||
const NFT_UNIT: i64 = 100_000_000;
|
||||
|
|
@ -207,14 +207,15 @@ mod selection;
|
|||
|
||||
pub use lookups::{
|
||||
get_basecoin_balance, get_coin_balance, get_pending_payments_for_contract, largest_fee,
|
||||
signature_exists, total_transactions, transaction_by_signature, transactions_by_address,
|
||||
latest_pending_txids_by_address, pending_transaction_by_txid, signature_exists,
|
||||
total_transactions, transaction_by_signature, transactions_by_address,
|
||||
};
|
||||
pub use processing::{
|
||||
delete_by_signatures, mark_processed_by_signatures, mark_selected_transactions_processed,
|
||||
restore_processed_by_signatures, restore_selected_transactions_processed,
|
||||
spawn_processed_cleanup,
|
||||
};
|
||||
pub use schema::{clear_mempool, init_db, setup_mempool};
|
||||
pub use schema::{clear_mempool, db_client, ensure_db_connection, init_db, setup_mempool};
|
||||
pub use selection::{
|
||||
apply_selected_transaction_math, clear_selected_transaction_sql, delete_selected_transactions,
|
||||
select_transactions_for_block, stream_selected_transaction_originals,
|
||||
|
|
@ -360,7 +361,8 @@ async fn unmark_by_signatures(
|
|||
async fn delete_processed_before_or_at(block_number: u32, limit: i64) -> Result<()> {
|
||||
// Periodic cleanup deletes processed mempool rows in bounded batches
|
||||
// so long-lived nodes do not accumulate infinite processed history.
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let client_handle = db_client().await?;
|
||||
let client = client_handle.as_ref();
|
||||
let bn = block_number as i32;
|
||||
|
||||
delete_processed_rows_limited(client, "transfer", bn, limit).await?;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ pub async fn mark_selected_transactions_processed(
|
|||
) -> Result<()> {
|
||||
// Mark each selected mempool row as processed under the saved block
|
||||
// number so it can be cleaned up or restored later if needed.
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let client_handle = db_client().await?;
|
||||
let client = client_handle.as_ref();
|
||||
let bn = block_number as i32;
|
||||
|
||||
// Selected batches are grouped by table, then marked with one UPDATE per
|
||||
|
|
@ -14,7 +15,7 @@ pub async fn mark_selected_transactions_processed(
|
|||
mark_rows_by_ids(client, "transfer", &ids_for_table(batch, "transfer"), bn).await?;
|
||||
mark_rows_by_ids(client, "token", &ids_for_table(batch, "token"), bn).await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
&client,
|
||||
"issue_token",
|
||||
&ids_for_table(batch, "issue_token"),
|
||||
bn,
|
||||
|
|
@ -24,7 +25,7 @@ pub async fn mark_selected_transactions_processed(
|
|||
mark_rows_by_ids(client, "nft", &ids_for_table(batch, "nft"), bn).await?;
|
||||
mark_rows_by_ids(client, "marketing", &ids_for_table(batch, "marketing"), bn).await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
&client,
|
||||
"vanity_address",
|
||||
&ids_for_table(batch, "vanity_address"),
|
||||
bn,
|
||||
|
|
@ -32,21 +33,21 @@ pub async fn mark_selected_transactions_processed(
|
|||
.await?;
|
||||
mark_rows_by_ids(client, "swap", &ids_for_table(batch, "swap"), bn).await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
&client,
|
||||
"loan_contract",
|
||||
&ids_for_table(batch, "loan_contract"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
&client,
|
||||
"loan_payment",
|
||||
&ids_for_table(batch, "loan_payment"),
|
||||
bn,
|
||||
)
|
||||
.await?;
|
||||
mark_rows_by_ids(
|
||||
client,
|
||||
&client,
|
||||
"collateral_claim",
|
||||
&ids_for_table(batch, "collateral_claim"),
|
||||
bn,
|
||||
|
|
@ -59,7 +60,8 @@ pub async fn mark_selected_transactions_processed(
|
|||
pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatch) -> Result<()> {
|
||||
// If block commit fails after selected rows were marked processed,
|
||||
// restore them before the chain height can acknowledge the block.
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let client_handle = db_client().await?;
|
||||
let client = client_handle.as_ref();
|
||||
|
||||
unmark_rows_by_ids(client, "transfer", &ids_for_table(batch, "transfer")).await?;
|
||||
unmark_rows_by_ids(client, "token", &ids_for_table(batch, "token")).await?;
|
||||
|
|
@ -68,26 +70,26 @@ pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatc
|
|||
unmark_rows_by_ids(client, "nft", &ids_for_table(batch, "nft")).await?;
|
||||
unmark_rows_by_ids(client, "marketing", &ids_for_table(batch, "marketing")).await?;
|
||||
unmark_rows_by_ids(
|
||||
client,
|
||||
&client,
|
||||
"vanity_address",
|
||||
&ids_for_table(batch, "vanity_address"),
|
||||
)
|
||||
.await?;
|
||||
unmark_rows_by_ids(client, "swap", &ids_for_table(batch, "swap")).await?;
|
||||
unmark_rows_by_ids(
|
||||
client,
|
||||
&client,
|
||||
"loan_contract",
|
||||
&ids_for_table(batch, "loan_contract"),
|
||||
)
|
||||
.await?;
|
||||
unmark_rows_by_ids(
|
||||
client,
|
||||
&client,
|
||||
"loan_payment",
|
||||
&ids_for_table(batch, "loan_payment"),
|
||||
)
|
||||
.await?;
|
||||
unmark_rows_by_ids(
|
||||
client,
|
||||
&client,
|
||||
"collateral_claim",
|
||||
&ids_for_table(batch, "collateral_claim"),
|
||||
)
|
||||
|
|
@ -103,7 +105,8 @@ pub async fn restore_processed_by_signatures(signatures: &[String]) -> Result<bo
|
|||
return Ok(false);
|
||||
}
|
||||
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let client_handle = db_client().await?;
|
||||
let client = client_handle.as_ref();
|
||||
let mut restored = 0_u64;
|
||||
|
||||
// Each table keeps its own signature columns, so rollback unmarks every
|
||||
|
|
@ -164,7 +167,8 @@ pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let client_handle = db_client().await?;
|
||||
let client = client_handle.as_ref();
|
||||
let bn = block_number as i32;
|
||||
|
||||
// Remote/synced blocks do not know local row IDs, so they mark by
|
||||
|
|
@ -258,7 +262,8 @@ pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let client = DB.get().expect("DB not initialized");
|
||||
let client_handle = db_client().await?;
|
||||
let client = client_handle.as_ref();
|
||||
|
||||
// Failed validation removes every matching pending row no matter which
|
||||
// transaction table currently owns the signature.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue