From 5c68786eef83249483891cb920053c8451f5b970 Mon Sep 17 00:00:00 2001 From: viraladmin <00purple@gmail.com> Date: Sat, 11 Jul 2026 10:28:11 -0600 Subject: [PATCH] added storage transactions --- src/bin/data_storage_tx.rs | 537 ++++++++++++++++++ src/blocks/block.rs | 32 ++ src/blocks/mod.rs | 2 + src/common/types.rs | 24 + src/config.rs | 5 + src/orphans/undo_block_transactions.rs | 26 + src/orphans/undo_transactions/mod.rs | 2 + .../undo_transactions/restore_mempool.rs | 73 +++ .../undo_transactions/undo_storage_key.rs | 42 ++ .../undo_transactions/undo_storage_value.rs | 97 ++++ src/records/memory/connections.rs | 11 +- src/records/memory/mempool/lookups.rs | 30 + src/records/memory/mempool/mod.rs | 21 +- src/records/memory/mempool/processing.rs | 28 + src/records/memory/mempool/schema.rs | 49 +- src/records/memory/mempool/selection.rs | 206 +++++++ src/records/record_chain/mod.rs | 2 + .../record_chain/parse_transactions.rs | 32 ++ src/records/record_chain/pending_effects.rs | 169 ++++++ src/records/record_chain/storage_key_tx.rs | 56 ++ src/records/record_chain/storage_value_tx.rs | 141 +++++ .../unpack_block/load_by_binary_data.rs | 81 ++- .../unpack_block/load_by_block_number.rs | 515 ++++++++++------- src/rpc/command_maps.rs | 14 + src/rpc/commands/address_history.rs | 10 +- src/rpc/commands/mod.rs | 1 + src/rpc/commands/storage_lookup.rs | 372 ++++++++++++ src/rpc/commands/tx_submit.rs | 98 +++- src/rpc/server/rpc_command_loop.rs | 98 ++++ src/standalone_tools/transaction_creator.rs | 352 +++++++++++- src/startup/connections.rs | 19 +- .../async_funcs/checks/block_balance.rs | 130 +++++ src/verifications/async_funcs/mod.rs | 2 + src/verifications/async_funcs/transactions.rs | 104 ++++ .../async_funcs/verify_storage_key.rs | 77 +++ .../async_funcs/verify_storage_values.rs | 197 +++++++ 36 files changed, 3418 insertions(+), 237 deletions(-) create mode 100644 src/bin/data_storage_tx.rs create mode 100644 src/orphans/undo_transactions/undo_storage_key.rs create mode 100644 src/orphans/undo_transactions/undo_storage_value.rs create mode 100644 src/records/record_chain/storage_key_tx.rs create mode 100644 src/records/record_chain/storage_value_tx.rs create mode 100644 src/rpc/commands/storage_lookup.rs create mode 100644 src/verifications/async_funcs/verify_storage_key.rs create mode 100644 src/verifications/async_funcs/verify_storage_values.rs diff --git a/src/bin/data_storage_tx.rs b/src/bin/data_storage_tx.rs new file mode 100644 index 0000000..6bb515a --- /dev/null +++ b/src/bin/data_storage_tx.rs @@ -0,0 +1,537 @@ +use blockchain::blocks::storage_key::{StorageKey, UnsignedStorageKey}; +use blockchain::blocks::storage_values::{ + StorageBool, StorageString, StorageU128, StorageU16, StorageU32, StorageU64, StorageU8, + UnsignedStorageBool, UnsignedStorageString, UnsignedStorageU128, UnsignedStorageU16, + UnsignedStorageU32, UnsignedStorageU64, UnsignedStorageU8, STORAGE_KEY_HASH_BYTES, + STORAGE_STRING_VALUE_BYTES, STORAGE_VALUE_KEY_BYTES, +}; +use blockchain::common::cli_prompts::{ + prompt_hidden_nonempty, prompt_visible, prompt_visible_with_default, prompt_wallet_path, +}; +use blockchain::common::types::{ + STORAGE_BOOL_TYPE, STORAGE_KEY_FEE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, + STORAGE_U16_TYPE, STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, STORAGE_VALUE_FEE, +}; +use blockchain::wallets::structures::Wallet; +use blockchain::{create_dir_all, decode, env, json, AsyncWriteExt, File, Utc}; + +const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000"; + +fn usage() { + println!("Usage:"); + println!(" ./data_storage_tx create_storage [txfee]"); + println!(" ./data_storage_tx bool [txfee]"); + println!(" ./data_storage_tx u8|8bit [txfee]"); + println!(" ./data_storage_tx u16|16bit [txfee]"); + println!(" ./data_storage_tx u32|32bit [txfee]"); + println!(" ./data_storage_tx u64|64bit [txfee]"); + println!(" ./data_storage_tx u128|128bit [txfee]"); + println!( + " ./data_storage_tx string [previous_hash] [txfee]" + ); +} + +fn display_fee(value: u64) -> f64 { + value as f64 / 100_000_000.0 +} + +fn parse_fee(value: &str) -> Option { + let parsed = value.trim().parse::().ok()?; + if parsed.is_sign_negative() { + return None; + } + Some((parsed * 100_000_000.0).round() as u64) +} + +async fn prompt_fee(default_fee: u64) -> u64 { + let default_display = format!("{:.8}", display_fee(default_fee)); + loop { + let value = + prompt_visible_with_default("Please enter the transaction fee", &default_display).await; + if let Some(fee) = parse_fee(&value) { + return fee; + } + println!("Please enter a valid fee."); + } +} + +fn fixed_field_key(input: &str) -> Option { + let trimmed = input.trim(); + if trimmed.is_empty() || trimmed.as_bytes().len() > STORAGE_VALUE_KEY_BYTES { + return None; + } + Some(format!("{trimmed: Option { + if input.as_bytes().len() > STORAGE_STRING_VALUE_BYTES { + return None; + } + Some(format!("{input: bool { + matches!(decode(value.trim()), Ok(bytes) if bytes.len() == STORAGE_KEY_HASH_BYTES) +} + +fn valid_previous_hash(value: &str) -> bool { + valid_storage_key_hash(value) +} + +fn normalize_storage_type(input: &str) -> Option<&'static str> { + match input.trim().to_lowercase().as_str() { + "create_storage" | "storage" | "storage_key" | "key" => Some("create_storage"), + "bool" | "boolean" => Some("bool"), + "u8" | "8bit" | "8" => Some("u8"), + "u16" | "16bit" | "16" => Some("u16"), + "u32" | "32bit" | "32" => Some("u32"), + "u64" | "64bit" | "64" => Some("u64"), + "u128" | "128bit" | "128" => Some("u128"), + "string" | "str" | "text" => Some("string"), + _ => None, + } +} + +async fn prompt_storage_hash(args: &[String], index: usize) -> Option { + let value = if args.len() > index { + args[index].clone() + } else { + prompt_visible("Please enter the storage key hash: ").await + }; + if !valid_storage_key_hash(&value) { + println!("Storage key hash must be a 32-byte / 64-character hex string."); + return None; + } + Some(value.trim().to_string()) +} + +async fn prompt_field_key(args: &[String], index: usize) -> Option { + let value = if args.len() > index { + args[index].clone() + } else { + prompt_visible("Please enter the storage field key: ").await + }; + let Some(key) = fixed_field_key(&value) else { + println!("Storage field key must be 1 to 50 bytes."); + return None; + }; + Some(key) +} + +async fn load_wallet() -> Option { + 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 + } + } +} + +async fn write_transaction(hash: &str, output: &serde_json::Value) -> Result<(), String> { + let output_str = + serde_json::to_string_pretty(output).map_err(|err| format!("JSON failed: {err}"))?; + let dir_path = "./transactions"; + + create_dir_all(dir_path) + .await + .map_err(|err| format!("Failed to create directory: {err}"))?; + let file_path = format!("{dir_path}/{hash}.json"); + let mut file = File::create(&file_path) + .await + .map_err(|err| format!("Failed to create file: {err}"))?; + file.write_all(output_str.as_bytes()) + .await + .map_err(|err| format!("Failed to write file: {err}"))?; + + println!("{output_str}"); + Ok(()) +} + +async fn create_storage_key(args: &[String], timestamp: u32, wallet: &Wallet) { + let txfee = if args.len() > 2 { + match parse_fee(&args[2]) { + Some(fee) => fee, + None => { + println!("Please enter a valid fee."); + return; + } + } + } else { + prompt_fee(STORAGE_KEY_FEE).await + }; + + let address = wallet.saved.short_address.trim(); + if !Wallet::short_address_validation(address) { + println!("storage key creator wallet invalid: {address}"); + return; + } + + let unsigned = UnsignedStorageKey::new(STORAGE_KEY_TYPE, timestamp, address, txfee).await; + let transaction = match StorageKey::new(unsigned, &wallet.saved.private_key).await { + Ok(transaction) => transaction, + Err(err) => { + eprintln!("Failed to sign storage key transaction: {err}"); + return; + } + }; + + let hash = transaction.unsigned_storagekey.hash().await; + let output = json!({ + "txtype": STORAGE_KEY_TYPE, + "timestamp": timestamp, + "address": address, + "txfee": txfee, + "hash": hash, + "signature": transaction.signature, + }); + + if let Err(err) = write_transaction(&hash, &output).await { + eprintln!("{err}"); + } +} + +macro_rules! create_numeric_storage { + ( + $args:expr, + $timestamp:expr, + $wallet:expr, + $storagekey:expr, + $key:expr, + $value_ty:ty, + $unsigned:ident, + $signed:ident, + $unsigned_field:ident, + $txtype:expr, + $label:expr + ) => {{ + let value_string = if $args.len() > 4 { + $args[4].clone() + } else { + prompt_visible(&format!("Please enter the {} value: ", $label)).await + }; + let value: $value_ty = match value_string.trim().parse() { + Ok(value) => value, + Err(_) => { + println!("Please enter a valid {} value.", $label); + return; + } + }; + let txfee = if $args.len() > 5 { + match parse_fee(&$args[5]) { + Some(fee) => fee, + None => { + println!("Please enter a valid fee."); + return; + } + } + } else { + prompt_fee(STORAGE_VALUE_FEE).await + }; + let address = $wallet.saved.short_address.trim(); + if !Wallet::short_address_validation(address) { + println!("storage value writer wallet invalid: {address}"); + return; + } + let unsigned = $unsigned { + txtype: $txtype, + time: $timestamp, + storagekey: $storagekey, + key: $key, + value, + address: address.to_string(), + txfee, + }; + let transaction = match $signed::new(unsigned, &$wallet.saved.private_key).await { + Ok(transaction) => transaction, + Err(err) => { + eprintln!("Failed to sign storage value transaction: {err}"); + return; + } + }; + let hash = transaction.$unsigned_field.hash().await; + let output = json!({ + "txtype": $txtype, + "timestamp": $timestamp, + "storagekey": transaction.$unsigned_field.storagekey, + "key": transaction.$unsigned_field.key, + "value": transaction.$unsigned_field.value, + "address": address, + "txfee": txfee, + "hash": hash, + "signature": transaction.signature, + }); + if let Err(err) = write_transaction(&hash, &output).await { + eprintln!("{err}"); + } + }}; +} + +async fn create_bool_storage( + args: &[String], + timestamp: u32, + wallet: &Wallet, + storagekey: String, + key: String, +) { + let value_string = if args.len() > 4 { + args[4].clone() + } else { + prompt_visible("Please enter the bool value (true/false): ").await + }; + let value = match value_string.trim().to_lowercase().as_str() { + "true" | "1" | "yes" | "y" => true, + "false" | "0" | "no" | "n" => false, + _ => { + println!("Bool storage value must be true or false."); + return; + } + }; + let txfee = if args.len() > 5 { + match parse_fee(&args[5]) { + Some(fee) => fee, + None => { + println!("Please enter a valid fee."); + return; + } + } + } else { + prompt_fee(STORAGE_VALUE_FEE).await + }; + let address = wallet.saved.short_address.trim(); + if !Wallet::short_address_validation(address) { + println!("storage value writer wallet invalid: {address}"); + return; + } + + let unsigned = UnsignedStorageBool { + txtype: STORAGE_BOOL_TYPE, + time: timestamp, + storagekey, + key, + value, + address: address.to_string(), + txfee, + }; + let transaction = match StorageBool::new(unsigned, &wallet.saved.private_key).await { + Ok(transaction) => transaction, + Err(err) => { + eprintln!("Failed to sign storage bool transaction: {err}"); + return; + } + }; + let hash = transaction.unsigned_storagebool.hash().await; + let output = json!({ + "txtype": STORAGE_BOOL_TYPE, + "timestamp": timestamp, + "storagekey": transaction.unsigned_storagebool.storagekey, + "key": transaction.unsigned_storagebool.key, + "value": transaction.unsigned_storagebool.value, + "address": address, + "txfee": txfee, + "hash": hash, + "signature": transaction.signature, + }); + if let Err(err) = write_transaction(&hash, &output).await { + eprintln!("{err}"); + } +} + +async fn create_string_storage( + args: &[String], + timestamp: u32, + wallet: &Wallet, + storagekey: String, + key: String, +) { + let value_input = if args.len() > 4 { + args[4].clone() + } else { + prompt_visible("Please enter the string value: ").await + }; + let Some(value) = fixed_string_value(&value_input) else { + println!("Storage string value must be 180 bytes or less."); + return; + }; + + let previous = if args.len() > 5 { + args[5].clone() + } else { + prompt_visible_with_default("Please enter the previous string tx hash", ZERO_HASH).await + }; + if !valid_previous_hash(&previous) { + println!("Previous hash must be a 32-byte / 64-character hex string."); + return; + } + + let txfee_arg_index = if args.len() > 6 { Some(6) } else { None }; + let txfee = if let Some(index) = txfee_arg_index { + match parse_fee(&args[index]) { + Some(fee) => fee, + None => { + println!("Please enter a valid fee."); + return; + } + } + } else { + prompt_fee(STORAGE_VALUE_FEE).await + }; + let address = wallet.saved.short_address.trim(); + if !Wallet::short_address_validation(address) { + println!("storage value writer wallet invalid: {address}"); + return; + } + + let unsigned = UnsignedStorageString { + txtype: STORAGE_STRING_TYPE, + time: timestamp, + storagekey, + key, + value, + address: address.to_string(), + previous: previous.trim().to_string(), + txfee, + }; + let transaction = match StorageString::new(unsigned, &wallet.saved.private_key).await { + Ok(transaction) => transaction, + Err(err) => { + eprintln!("Failed to sign storage string transaction: {err}"); + return; + } + }; + let hash = transaction.unsigned_storage.hash().await; + let output = json!({ + "txtype": STORAGE_STRING_TYPE, + "timestamp": timestamp, + "storagekey": transaction.unsigned_storage.storagekey, + "key": transaction.unsigned_storage.key, + "value": transaction.unsigned_storage.value, + "address": address, + "previous": transaction.unsigned_storage.previous, + "txfee": txfee, + "hash": hash, + "signature": transaction.signature, + }); + if let Err(err) = write_transaction(&hash, &output).await { + eprintln!("{err}"); + } +} + +#[tokio::main] +async fn main() { + let args: Vec = env::args().collect(); + let command = if args.len() > 1 { + args[1].clone() + } else { + prompt_visible("Please enter storage transaction type: ").await + }; + let Some(storage_type) = normalize_storage_type(&command) else { + usage(); + return; + }; + + let Some(wallet) = load_wallet().await else { + return; + }; + let timestamp = Utc::now().timestamp() as u32; + + if storage_type == "create_storage" { + create_storage_key(&args, timestamp, &wallet).await; + return; + } + + let Some(storagekey) = prompt_storage_hash(&args, 2).await else { + return; + }; + let Some(key) = prompt_field_key(&args, 3).await else { + return; + }; + + match storage_type { + "bool" => create_bool_storage(&args, timestamp, &wallet, storagekey, key).await, + "u8" => { + create_numeric_storage!( + args, + timestamp, + wallet, + storagekey, + key, + u8, + UnsignedStorageU8, + StorageU8, + unsigned_storageu8, + STORAGE_U8_TYPE, + "u8" + ) + } + "u16" => { + create_numeric_storage!( + args, + timestamp, + wallet, + storagekey, + key, + u16, + UnsignedStorageU16, + StorageU16, + unsigned_storageu16, + STORAGE_U16_TYPE, + "u16" + ) + } + "u32" => { + create_numeric_storage!( + args, + timestamp, + wallet, + storagekey, + key, + u32, + UnsignedStorageU32, + StorageU32, + unsigned_storageu32, + STORAGE_U32_TYPE, + "u32" + ) + } + "u64" => { + create_numeric_storage!( + args, + timestamp, + wallet, + storagekey, + key, + u64, + UnsignedStorageU64, + StorageU64, + unsigned_storageu64, + STORAGE_U64_TYPE, + "u64" + ) + } + "u128" => { + create_numeric_storage!( + args, + timestamp, + wallet, + storagekey, + key, + u128, + UnsignedStorageU128, + StorageU128, + unsigned_storageu128, + STORAGE_U128_TYPE, + "u128" + ) + } + "string" => create_string_storage(&args, timestamp, &wallet, storagekey, key).await, + _ => usage(), + } +} diff --git a/src/blocks/block.rs b/src/blocks/block.rs index 5bc329b..2e67a57 100644 --- a/src/blocks/block.rs +++ b/src/blocks/block.rs @@ -343,6 +343,38 @@ impl Block { let tx_bytes = vanity_tx.to_bytes().await?; buffer.extend_from_slice(&tx_bytes); } + Transaction::StorageKey(storage_key_tx) => { + let tx_bytes = storage_key_tx.to_bytes().await?; + buffer.extend_from_slice(&tx_bytes); + } + Transaction::StorageBool(storage_tx) => { + let tx_bytes = storage_tx.to_bytes().await?; + buffer.extend_from_slice(&tx_bytes); + } + Transaction::StorageU8(storage_tx) => { + let tx_bytes = storage_tx.to_bytes().await?; + buffer.extend_from_slice(&tx_bytes); + } + Transaction::StorageU16(storage_tx) => { + let tx_bytes = storage_tx.to_bytes().await?; + buffer.extend_from_slice(&tx_bytes); + } + Transaction::StorageU32(storage_tx) => { + let tx_bytes = storage_tx.to_bytes().await?; + buffer.extend_from_slice(&tx_bytes); + } + Transaction::StorageU64(storage_tx) => { + let tx_bytes = storage_tx.to_bytes().await?; + buffer.extend_from_slice(&tx_bytes); + } + Transaction::StorageU128(storage_tx) => { + let tx_bytes = storage_tx.to_bytes().await?; + buffer.extend_from_slice(&tx_bytes); + } + Transaction::StorageString(storage_tx) => { + let tx_bytes = storage_tx.to_bytes().await?; + buffer.extend_from_slice(&tx_bytes); + } } } Ok(buffer) diff --git a/src/blocks/mod.rs b/src/blocks/mod.rs index 51f661f..9197851 100644 --- a/src/blocks/mod.rs +++ b/src/blocks/mod.rs @@ -8,6 +8,8 @@ pub mod loans; pub mod marketing; pub mod nft; pub mod rewards; +pub mod storage_key; +pub mod storage_values; pub mod swap; pub mod token; pub mod transfer; diff --git a/src/common/types.rs b/src/common/types.rs index ab9bae3..d55f056 100644 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -7,6 +7,10 @@ use crate::blocks::loans::LoanContractTransaction; use crate::blocks::marketing::MarketingTransaction; use crate::blocks::nft::CreateNftTransaction; use crate::blocks::rewards::RewardsTransaction; +use crate::blocks::storage_key::StorageKey; +use crate::blocks::storage_values::{ + StorageBool, StorageString, StorageU128, StorageU16, StorageU32, StorageU64, StorageU8, +}; use crate::blocks::swap::SwapTransaction; use crate::blocks::token::CreateTokenTransaction; use crate::blocks::transfer::TransferTransaction; @@ -28,6 +32,14 @@ pub const COLLATERAL_TYPE: u8 = 9; pub const BURN_TYPE: u8 = 10; pub const ISSUE_TOKEN_TYPE: u8 = 11; pub const VANITY_ADDRESS_TYPE: u8 = 12; +pub const STORAGE_KEY_TYPE: u8 = 100; +pub const STORAGE_BOOL_TYPE: u8 = 101; +pub const STORAGE_U8_TYPE: u8 = 102; +pub const STORAGE_U16_TYPE: u8 = 103; +pub const STORAGE_U32_TYPE: u8 = 104; +pub const STORAGE_U64_TYPE: u8 = 105; +pub const STORAGE_U128_TYPE: u8 = 106; +pub const STORAGE_STRING_TYPE: u8 = 107; // Coin and asset names are stored in fixed 15-byte fields. pub const COIN_LENGTH: usize = 15; @@ -62,6 +74,10 @@ pub const COLLATERAL_FEE: u64 = 300000000; // cost to clain collateral, 3 CLC pub const BURN_FEE: u64 = 10000; // cost to burn a token or NFT, 0.0001 CLC pub const ISSUE_TOKEN_FEE: u64 = 10000000000; // cost to issue more of an existing token, 100 CLC pub const VANITY_ADDRESS_FEE: u64 = 500000000; // cost to register or update a vanity address, 5 CLC +pub const STORAGE_KEY_FEE: u64 = 50000000000; // cost to create an app storage key, 500 CLC +pub const STORAGE_VALUE_FEE: u64 = 100000000; // cost to write app storage data, 1 CLC +pub const STORAGE_KEY_INDEX_TREE: &str = "storage_key_index"; +pub const STORAGE_VALUE_ROLLBACK_TREE: &str = "storage_value_rollback"; pub fn minimum_transfer_fee(value: u64, is_base_coin: bool) -> u64 { // Base-coin transfers pay the percentage fee using integer math. @@ -92,4 +108,12 @@ pub enum Transaction { Borrower(ContractPaymentTransaction), Collateral(CollateralClaimTransaction), Vanity(VanityAddressTransaction), + StorageKey(StorageKey), + StorageBool(StorageBool), + StorageU8(StorageU8), + StorageU16(StorageU16), + StorageU32(StorageU32), + StorageU64(StorageU64), + StorageU128(StorageU128), + StorageString(StorageString), } diff --git a/src/config.rs b/src/config.rs index b2b1767..aef7098 100644 --- a/src/config.rs +++ b/src/config.rs @@ -32,6 +32,7 @@ pub struct Settings { pub pg_user: String, pub pg_password: Option, pub pg_dbname: String, + pub storage_lookup_fee_per_byte: u64, } impl Settings { @@ -197,6 +198,10 @@ impl Settings { .ok_or("INCOMING_CONNECTIONS not found")? .parse::()?, threads, + storage_lookup_fee_per_byte: conf + .get_from(Some("Settings"), "STORAGE_LOOKUP_FEE_PER_BYTE") + .unwrap_or("0") + .parse::()?, pg_host: pg_section.get("host").unwrap_or("127.0.0.1").to_string(), pg_port: pg_section.get("port").unwrap_or("5432").parse::()?, pg_user: pg_section diff --git a/src/orphans/undo_block_transactions.rs b/src/orphans/undo_block_transactions.rs index acb2c0c..b12f122 100644 --- a/src/orphans/undo_block_transactions.rs +++ b/src/orphans/undo_block_transactions.rs @@ -13,6 +13,8 @@ use crate::orphans::undo_transactions::undo_issue_token::undo_issue_token_transa use crate::orphans::undo_transactions::undo_loan_creation::undo_loan_creation_transaction; use crate::orphans::undo_transactions::undo_marketing::undo_marketing_transaction; use crate::orphans::undo_transactions::undo_rewards::undo_rewards_transaction; +use crate::orphans::undo_transactions::undo_storage_key::undo_storage_key_transaction; +use crate::orphans::undo_transactions::undo_storage_value::undo_storage_value_transaction; 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; @@ -130,6 +132,30 @@ pub async fn undo_transactions( .await?; rolled_back_transactions.push(Transaction::Vanity(vanity_tx)); } + Transaction::StorageKey(storage_key_tx) => { + undo_storage_key_transaction( + storage_key_tx.clone(), + &mining_receiver, + ¶ms.db, + ) + .await?; + rolled_back_transactions.push(Transaction::StorageKey(storage_key_tx)); + } + Transaction::StorageBool(_) + | Transaction::StorageU8(_) + | Transaction::StorageU16(_) + | Transaction::StorageU32(_) + | Transaction::StorageU64(_) + | Transaction::StorageU128(_) + | Transaction::StorageString(_) => { + undo_storage_value_transaction( + transaction.clone(), + &mining_receiver, + ¶ms.db, + ) + .await?; + rolled_back_transactions.push(transaction); + } } } // The block miner loses one mined-block count when this block is removed. diff --git a/src/orphans/undo_transactions/mod.rs b/src/orphans/undo_transactions/mod.rs index 708361a..aa60578 100644 --- a/src/orphans/undo_transactions/mod.rs +++ b/src/orphans/undo_transactions/mod.rs @@ -8,6 +8,8 @@ pub mod undo_issue_token; pub mod undo_loan_creation; pub mod undo_marketing; pub mod undo_rewards; +pub mod undo_storage_key; +pub mod undo_storage_value; pub mod undo_swap; pub mod undo_transfer; pub mod undo_vanity; diff --git a/src/orphans/undo_transactions/restore_mempool.rs b/src/orphans/undo_transactions/restore_mempool.rs index 4fca29b..2a17fac 100644 --- a/src/orphans/undo_transactions/restore_mempool.rs +++ b/src/orphans/undo_transactions/restore_mempool.rs @@ -5,6 +5,8 @@ use crate::blocks::loan_payment::ContractPaymentTransaction; use crate::blocks::loans::LoanContractTransaction; use crate::blocks::marketing::MarketingTransaction; use crate::blocks::nft::CreateNftTransaction; +use crate::blocks::storage_key::StorageKey; +use crate::blocks::storage_values::StorageValueTransaction; use crate::blocks::swap::SwapTransaction; use crate::blocks::token::CreateTokenTransaction; use crate::blocks::transfer::TransferTransaction; @@ -307,6 +309,43 @@ pub async fn restore_vanity(transaction: &VanityAddressTransaction, db: &Db) { .await; } +pub async fn restore_storage_key(transaction: &StorageKey, db: &Db) { + let storage_key = &transaction.unsigned_storagekey; + let spendable = balance_checkup( + db, + 0, + storage_key.txfee, + BASECOIN.clone(), + &storage_key.address, + ) + .await; + let signature = transaction.signature.clone(); + + restore_if_spendable_void( + "storage_key", + "unknown", + &[signature], + spendable, + || async { + let _ = transaction.add_to_memory().await; + }, + ) + .await; +} + +async fn restore_storage_value(tx_label: &str, transaction: &T, db: &Db, insert: F) +where + T: StorageValueTransaction, + F: FnOnce() -> Fut, + Fut: std::future::Future>>, +{ + let parts = transaction.storage_parts(); + let spendable = balance_checkup(db, 0, parts.txfee, BASECOIN.clone(), parts.address).await; + let signature = parts.signature.to_string(); + + restore_if_spendable(tx_label, "unknown", &[signature], spendable, insert).await; +} + pub async fn restore_rolled_back_transaction(transaction: &Transaction, db: &Db) { match transaction { Transaction::Transfer(tx) => restore_transfer(tx, db).await, @@ -320,6 +359,40 @@ pub async fn restore_rolled_back_transaction(transaction: &Transaction, db: &Db) Transaction::Borrower(tx) => restore_borrower(tx, db).await, Transaction::Collateral(tx) => restore_collateral(tx, db).await, Transaction::Vanity(tx) => restore_vanity(tx, db).await, + Transaction::StorageKey(tx) => restore_storage_key(tx, db).await, + Transaction::StorageBool(tx) => { + restore_storage_value("storage_bool", tx, db, || async { + tx.add_to_memory().await + }) + .await + } + Transaction::StorageU8(tx) => { + restore_storage_value("storage_u8", tx, db, || async { tx.add_to_memory().await }).await + } + Transaction::StorageU16(tx) => { + restore_storage_value("storage_u16", tx, db, || async { tx.add_to_memory().await }) + .await + } + Transaction::StorageU32(tx) => { + restore_storage_value("storage_u32", tx, db, || async { tx.add_to_memory().await }) + .await + } + Transaction::StorageU64(tx) => { + restore_storage_value("storage_u64", tx, db, || async { tx.add_to_memory().await }) + .await + } + Transaction::StorageU128(tx) => { + restore_storage_value("storage_u128", tx, db, || async { + tx.add_to_memory().await + }) + .await + } + Transaction::StorageString(tx) => { + restore_storage_value("storage_string", tx, db, || async { + tx.add_to_memory().await + }) + .await + } Transaction::Genesis(_) | Transaction::Rewards(_) => {} } } diff --git a/src/orphans/undo_transactions/undo_storage_key.rs b/src/orphans/undo_transactions/undo_storage_key.rs new file mode 100644 index 0000000..d813fd4 --- /dev/null +++ b/src/orphans/undo_transactions/undo_storage_key.rs @@ -0,0 +1,42 @@ +use crate::blocks::storage_key::StorageKey; +use crate::common::types::STORAGE_KEY_INDEX_TREE; +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::sled::Db; + +pub async fn undo_storage_key_transaction( + transaction: StorageKey, + mining_receiver: &str, + db: &Db, +) -> Result<(), String> { + let txfee = transaction.unsigned_storagekey.txfee; + let address = transaction.unsigned_storagekey.address.clone(); + let txhash = transaction.unsigned_storagekey.hash().await; + let txhash_bytes = decode(&txhash) + .map_err(|err| format!("Could not decode storage key txhash during undo: {err}"))?; + + let _ = balance_sheet_operation_with_db(db, mining_receiver, txfee, &BASECOIN, "subtraction"); + let _ = balance_sheet_operation_with_db(db, &address, txfee, &BASECOIN, "addition"); + + let _ = db + .drop_tree(&txhash_bytes) + .map_err(|err| format!("Could not drop storage key tree during undo: {err}"))?; + let storage_key_index = db + .open_tree(STORAGE_KEY_INDEX_TREE) + .map_err(|err| format!("Could not open storage key index during undo: {err}"))?; + storage_key_index + .remove(&txhash_bytes) + .map_err(|err| format!("Could not remove storage key index during undo: {err}"))?; + + let _ = remove_wallet_transaction_index(db, &[&address, mining_receiver], &txhash_bytes); + let txid_tree = db + .open_tree("txid") + .map_err(|err| format!("Could not open txid tree during storage key undo: {err}"))?; + txid_tree + .remove(txhash_bytes) + .map_err(|err| format!("Could not remove storage key txid mapping during undo: {err}"))?; + + Ok(()) +} diff --git a/src/orphans/undo_transactions/undo_storage_value.rs b/src/orphans/undo_transactions/undo_storage_value.rs new file mode 100644 index 0000000..debdd71 --- /dev/null +++ b/src/orphans/undo_transactions/undo_storage_value.rs @@ -0,0 +1,97 @@ +use crate::blocks::storage_values::{ + storage_record_key, storage_string_record_key, StorageValueTransaction, +}; +use crate::common::types::{Transaction, STORAGE_VALUE_ROLLBACK_TREE}; +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::sled::Db; + +struct StorageUndoParts { + storagekey: String, + key: String, + address: String, + txfee: u64, + hash: String, +} + +fn storage_undo_parts(transaction: &Transaction) -> Result { + let parts = match transaction { + Transaction::StorageBool(tx) => tx.storage_parts(), + Transaction::StorageU8(tx) => tx.storage_parts(), + Transaction::StorageU16(tx) => tx.storage_parts(), + Transaction::StorageU32(tx) => tx.storage_parts(), + Transaction::StorageU64(tx) => tx.storage_parts(), + Transaction::StorageU128(tx) => tx.storage_parts(), + Transaction::StorageString(tx) => tx.storage_parts(), + _ => return Err("Unsupported storage value transaction undo.".to_string()), + }; + + Ok(StorageUndoParts { + storagekey: parts.storagekey.to_string(), + key: parts.key.to_string(), + address: parts.address.to_string(), + txfee: parts.txfee, + hash: parts.hash, + }) +} + +pub async fn undo_storage_value_transaction( + transaction: Transaction, + mining_receiver: &str, + db: &Db, +) -> Result<(), String> { + let parts = storage_undo_parts(&transaction)?; + let storage_tree_name = decode(&parts.storagekey) + .map_err(|err| format!("Could not decode storage tree key during undo: {err}"))?; + let txhash_bytes = decode(&parts.hash) + .map_err(|err| format!("Could not decode storage value txhash during undo: {err}"))?; + let storage_record_key = match &transaction { + Transaction::StorageString(tx) => storage_string_record_key(db, tx).await?, + _ => storage_record_key(&parts.address, &parts.key)?, + }; + + let _ = + balance_sheet_operation_with_db(db, mining_receiver, parts.txfee, &BASECOIN, "subtraction"); + let _ = balance_sheet_operation_with_db(db, &parts.address, parts.txfee, &BASECOIN, "addition"); + + let rollback_tree = db + .open_tree(STORAGE_VALUE_ROLLBACK_TREE) + .map_err(|err| format!("Could not open storage value rollback tree during undo: {err}"))?; + let rollback_value = rollback_tree + .get(&txhash_bytes) + .map_err(|err| format!("Could not read storage value rollback during undo: {err}"))? + .ok_or_else(|| "Missing storage value rollback record.".to_string())?; + + let storage_tree = db + .open_tree(storage_tree_name) + .map_err(|err| format!("Could not open storage tree during undo: {err}"))?; + match rollback_value.first().copied() { + Some(0) => { + storage_tree + .remove(storage_record_key) + .map_err(|err| format!("Could not remove storage value during undo: {err}"))?; + } + Some(1) => { + storage_tree + .insert(storage_record_key, &rollback_value[1..]) + .map_err(|err| format!("Could not restore storage value during undo: {err}"))?; + } + _ => return Err("Invalid storage value rollback marker.".to_string()), + } + + rollback_tree + .remove(&txhash_bytes) + .map_err(|err| format!("Could not remove storage value rollback record: {err}"))?; + + let _ = remove_wallet_transaction_index(db, &[&parts.address, mining_receiver], &txhash_bytes); + let txid_tree = db + .open_tree("txid") + .map_err(|err| format!("Could not open txid tree during storage value undo: {err}"))?; + txid_tree + .remove(txhash_bytes) + .map_err(|err| format!("Could not remove storage value txid mapping during undo: {err}"))?; + + Ok(()) +} diff --git a/src/records/memory/connections.rs b/src/records/memory/connections.rs index 0f5a693..33ab01b 100644 --- a/src/records/memory/connections.rs +++ b/src/records/memory/connections.rs @@ -186,10 +186,10 @@ async fn retry_dropped_outgoing(ip: String, port: u16) { if err_string.contains( "The connection is already in the connection manager Please wait 10 minutes and try again", ) { - info!( - "[reconnect] dropped peer {addr_string} is already in the connection manager; stopping reconnect attempts" + warn!( + "[reconnect] dropped peer {addr_string} is still recorded by remote connection manager on attempt {attempt}/3; retrying" ); - return; + continue; } warn!( "[reconnect] failed to reconnect dropped peer {addr_string} on attempt {attempt}/3: {err_string}" @@ -965,10 +965,7 @@ pub async fn peer_accepts_live_relay(key: &str) -> bool { && connection_key.port == port && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) { - Some( - info.ready - || (info.wallet_registry_synced && info.network_map_synced), - ) + Some(info.ready || (info.wallet_registry_synced && info.network_map_synced)) } else { None } diff --git a/src/records/memory/mempool/lookups.rs b/src/records/memory/mempool/lookups.rs index 9a8b8bf..f35496a 100644 --- a/src/records/memory/mempool/lookups.rs +++ b/src/records/memory/mempool/lookups.rs @@ -24,6 +24,8 @@ pub async fn signature_exists(signature: &str, hash: &str) -> Result { OR EXISTS (SELECT 1 FROM loan_contract j WHERE j.signature2 = $1 AND j.hash = $2 AND j.processed = false) OR EXISTS (SELECT 1 FROM loan_payment k WHERE k.signature = $1 AND k.hash = $2 AND k.processed = false) OR EXISTS (SELECT 1 FROM collateral_claim l WHERE l.signature = $1 AND l.hash = $2 AND l.processed = false) + OR EXISTS (SELECT 1 FROM storage_key sk WHERE sk.signature = $1 AND sk.hash = $2 AND sk.processed = false) + OR EXISTS (SELECT 1 FROM storage_value sv WHERE sv.signature = $1 AND sv.hash = $2 AND sv.processed = false) THEN 1 ELSE 0 END AS signature_found; @@ -74,6 +76,10 @@ pub async fn transaction_by_signature(signature: &str) -> RpcResponse { SELECT original FROM loan_payment WHERE signature = $1 AND processed = false LIMIT 1 UNION ALL SELECT original FROM collateral_claim WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM storage_key WHERE signature = $1 AND processed = false LIMIT 1 + UNION ALL + SELECT original FROM storage_value WHERE signature = $1 AND processed = false LIMIT 1 ) AS subquery LIMIT 1 "#, &[&signature], @@ -129,6 +135,10 @@ pub async fn transactions_by_address(db: &Db, address: &str) -> RpcResponse { SELECT original FROM loan_payment WHERE address = ANY($1) AND processed = false UNION ALL SELECT original FROM collateral_claim WHERE address = ANY($1) AND processed = false + UNION ALL + SELECT original FROM storage_key WHERE address = ANY($1) AND processed = false + UNION ALL + SELECT original FROM storage_value WHERE address = ANY($1) AND processed = false ) AS subquery; "#, &[&addresses], @@ -198,6 +208,12 @@ pub async fn latest_pending_txids_by_address(db: &Db, address: &str, limit: usiz UNION ALL SELECT hash, time, id AS source_id FROM collateral_claim WHERE address = ANY($1) AND processed = false + UNION ALL + SELECT hash, time, id AS source_id FROM storage_key + WHERE address = ANY($1) AND processed = false + UNION ALL + SELECT hash, time, id AS source_id FROM storage_value + WHERE address = ANY($1) AND processed = false ) AS pending ORDER BY hash, time DESC, source_id DESC ) AS deduped @@ -256,6 +272,10 @@ pub async fn pending_transaction_by_txid(txid: &[u8]) -> Option> { SELECT original FROM loan_payment WHERE hash = $1 AND processed = false UNION ALL SELECT original FROM collateral_claim WHERE hash = $1 AND processed = false + UNION ALL + SELECT original FROM storage_key WHERE hash = $1 AND processed = false + UNION ALL + SELECT original FROM storage_value WHERE hash = $1 AND processed = false ) AS subquery LIMIT 1 "#, &[&hash], @@ -301,6 +321,10 @@ pub async fn largest_fee() -> RpcResponse { SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_payment UNION ALL SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM collateral_claim + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM storage_key + UNION ALL + SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM storage_value ) AS combined_max_txids; "#, &[], @@ -481,6 +505,8 @@ pub async fn get_basecoin_balance( + COALESCE((SELECT SUM(m.fee) FROM marketing m WHERE m.advertiser = ANY($1) AND m.processed = false), 0) + COALESCE((SELECT SUM(v.fee) FROM vanity_address v WHERE v.address = ANY($1) AND v.processed = false), 0) + COALESCE((SELECT SUM(cc.fee) FROM collateral_claim cc WHERE cc.address = ANY($1) AND cc.processed = false), 0) + + COALESCE((SELECT SUM(sk.fee) FROM storage_key sk WHERE sk.address = ANY($1) AND sk.processed = false), 0) + + COALESCE((SELECT SUM(sv.fee) FROM storage_value sv WHERE sv.address = ANY($1) AND sv.processed = false), 0) + COALESCE((SELECT SUM(lc.fee) FROM loan_contract lc WHERE lc.lender = ANY($1) AND lc.processed = false), 0) + COALESCE((SELECT SUM(lp.fee) FROM loan_payment lp WHERE lp.address = ANY($1) AND lp.processed = false), 0) + COALESCE((SELECT SUM(lc.loan_amount) @@ -575,6 +601,10 @@ pub async fn total_transactions() -> RpcResponse { SELECT COUNT(*) AS row_count FROM loan_payment UNION ALL SELECT COUNT(*) AS row_count FROM collateral_claim + UNION ALL + SELECT COUNT(*) AS row_count FROM storage_key + UNION ALL + SELECT COUNT(*) AS row_count FROM storage_value ) AS combined; "#, &[], diff --git a/src/records/memory/mempool/mod.rs b/src/records/memory/mempool/mod.rs index 412f197..274ea56 100644 --- a/src/records/memory/mempool/mod.rs +++ b/src/records/memory/mempool/mod.rs @@ -152,6 +152,19 @@ enum SelectedMempoolTransaction { contract_hash: String, hash: String, }, + StorageKey { + id: i64, + fee: i64, + address: String, + hash: String, + }, + StorageValue { + id: i64, + fee: i64, + address: String, + hash: String, + original: Vec, + }, } #[derive(Clone, Default)] @@ -176,6 +189,8 @@ impl SelectedMempoolTransaction { SelectedMempoolTransaction::Lender { .. } => "loan_contract", SelectedMempoolTransaction::Borrower { .. } => "loan_payment", SelectedMempoolTransaction::Collateral { .. } => "collateral_claim", + SelectedMempoolTransaction::StorageKey { .. } => "storage_key", + SelectedMempoolTransaction::StorageValue { .. } => "storage_value", } } @@ -191,7 +206,9 @@ impl SelectedMempoolTransaction { | SelectedMempoolTransaction::Swap { id, .. } | SelectedMempoolTransaction::Lender { id, .. } | SelectedMempoolTransaction::Borrower { id, .. } - | SelectedMempoolTransaction::Collateral { id, .. } => *id, + | SelectedMempoolTransaction::Collateral { id, .. } + | SelectedMempoolTransaction::StorageKey { id, .. } + | SelectedMempoolTransaction::StorageValue { id, .. } => *id, } } } @@ -383,6 +400,8 @@ async fn delete_processed_before_or_at(block_number: u32, limit: i64) -> Result< delete_processed_rows_limited("loan_contract", bn, limit).await?; delete_processed_rows_limited("loan_payment", bn, limit).await?; delete_processed_rows_limited("collateral_claim", bn, limit).await?; + delete_processed_rows_limited("storage_key", bn, limit).await?; + delete_processed_rows_limited("storage_value", bn, limit).await?; Ok(()) } diff --git a/src/records/memory/mempool/processing.rs b/src/records/memory/mempool/processing.rs index dc5d8ef..2063894 100644 --- a/src/records/memory/mempool/processing.rs +++ b/src/records/memory/mempool/processing.rs @@ -46,6 +46,8 @@ pub async fn mark_selected_transactions_processed( bn, ) .await?; + mark_rows_by_ids("storage_key", &ids_for_table(batch, "storage_key"), bn).await?; + mark_rows_by_ids("storage_value", &ids_for_table(batch, "storage_value"), bn).await?; Ok(()) } @@ -68,6 +70,8 @@ pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatc &ids_for_table(batch, "collateral_claim"), ) .await?; + unmark_rows_by_ids("storage_key", &ids_for_table(batch, "storage_key")).await?; + unmark_rows_by_ids("storage_value", &ids_for_table(batch, "storage_value")).await?; Ok(()) } @@ -96,6 +100,8 @@ pub async fn restore_processed_by_signatures(signatures: &[String]) -> Result 0) } @@ -221,6 +227,18 @@ pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u signatures, ) .await?; + pg_execute_block_signatures( + "UPDATE storage_key SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + bn, + signatures, + ) + .await?; + pg_execute_block_signatures( + "UPDATE storage_value SET processed=true, processed_block_number=$1 WHERE signature = ANY($2)", + bn, + signatures, + ) + .await?; Ok(()) } @@ -273,6 +291,16 @@ pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> { signatures, ) .await?; + pg_execute_signatures( + "DELETE FROM storage_key WHERE signature = ANY($1)", + signatures, + ) + .await?; + pg_execute_signatures( + "DELETE FROM storage_value WHERE signature = ANY($1)", + signatures, + ) + .await?; Ok(()) } diff --git a/src/records/memory/mempool/schema.rs b/src/records/memory/mempool/schema.rs index 0f22b1a..0d9f4bb 100644 --- a/src/records/memory/mempool/schema.rs +++ b/src/records/memory/mempool/schema.rs @@ -279,6 +279,31 @@ pub async fn setup_mempool() -> Result<()> { original BYTEA NOT NULL ); + CREATE TABLE IF NOT EXISTS storage_key ( + id BIGSERIAL PRIMARY KEY, + time INTEGER NOT NULL, + fee BIGINT NOT NULL, + address TEXT NOT NULL, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + + CREATE TABLE IF NOT EXISTS storage_value ( + id BIGSERIAL PRIMARY KEY, + txtype SMALLINT NOT NULL, + time INTEGER NOT NULL, + fee BIGINT NOT NULL, + address TEXT NOT NULL, + hash VARCHAR(64) NOT NULL, + signature TEXT NOT NULL, + processed bool DEFAULT false, + processed_block_number INTEGER DEFAULT NULL, + original BYTEA NOT NULL + ); + ALTER TABLE loan_payment ADD COLUMN IF NOT EXISTS txid VARCHAR(64); ALTER TABLE transfer ADD COLUMN IF NOT EXISTS nft_series INTEGER NOT NULL DEFAULT 0; ALTER TABLE nft ADD COLUMN IF NOT EXISTS count BIGINT NOT NULL DEFAULT 1; @@ -314,6 +339,10 @@ pub async fn setup_mempool() -> Result<()> { ALTER TABLE loan_payment ALTER COLUMN signature TYPE TEXT; ALTER TABLE collateral_claim ALTER COLUMN address TYPE TEXT; ALTER TABLE collateral_claim ALTER COLUMN signature TYPE TEXT; + ALTER TABLE storage_key ALTER COLUMN address TYPE TEXT; + ALTER TABLE storage_key ALTER COLUMN signature TYPE TEXT; + ALTER TABLE storage_value ALTER COLUMN address TYPE TEXT; + ALTER TABLE storage_value ALTER COLUMN signature TYPE TEXT; "#; // The schema block creates fresh installs and also carries small migrations @@ -372,6 +401,14 @@ pub async fn setup_mempool() -> Result<()> { DELETE FROM collateral_claim a USING collateral_claim b WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM storage_key a + USING storage_key b + WHERE a.id > b.id AND a.signature = b.signature; + + DELETE FROM storage_value a + USING storage_value b + WHERE a.id > b.id AND a.signature = b.signature; "#; // Remove duplicate rows before unique indexes are created, otherwise stale @@ -426,6 +463,14 @@ pub async fn setup_mempool() -> Result<()> { CREATE INDEX IF NOT EXISTS collateral_claim_pick_idx ON collateral_claim (processed, fee DESC, time ASC, id ASC); CREATE INDEX IF NOT EXISTS collateral_claim_sig_idx ON collateral_claim (signature); CREATE UNIQUE INDEX IF NOT EXISTS collateral_claim_sig_unique_idx ON collateral_claim (signature); + + CREATE INDEX IF NOT EXISTS storage_key_pick_idx ON storage_key (processed, fee DESC, time ASC, id ASC); + CREATE INDEX IF NOT EXISTS storage_key_sig_idx ON storage_key (signature); + CREATE UNIQUE INDEX IF NOT EXISTS storage_key_sig_unique_idx ON storage_key (signature); + + CREATE INDEX IF NOT EXISTS storage_value_pick_idx ON storage_value (processed, fee DESC, time ASC, id ASC); + CREATE INDEX IF NOT EXISTS storage_value_sig_idx ON storage_value (signature); + CREATE UNIQUE INDEX IF NOT EXISTS storage_value_sig_unique_idx ON storage_value (signature); "#; // Pick indexes speed up block selection; signature indexes enforce one // pending copy of each transaction. @@ -457,7 +502,9 @@ pub async fn clear_mempool() -> Result<()> { swap, loan_contract, loan_payment, - collateral_claim + collateral_claim, + storage_key, + storage_value RESTART IDENTITY; "#, ) diff --git a/src/records/memory/mempool/selection.rs b/src/records/memory/mempool/selection.rs index b56f07d..0c0e567 100644 --- a/src/records/memory/mempool/selection.rs +++ b/src/records/memory/mempool/selection.rs @@ -1,7 +1,87 @@ use super::*; +use crate::blocks::storage_values::{ + storage_record_key, storage_string_record_key, StorageBool, StorageString, StorageU128, + StorageU16, StorageU32, StorageU64, StorageU8, StorageValueTransaction, +}; +use crate::common::types::{ + Transaction, STORAGE_BOOL_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, + STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, +}; use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; +struct SelectedStorageValueParts { + storagekey: String, + key: String, + address: String, +} + +async fn storage_value_transaction_from_original(original: &[u8]) -> Result { + let Some((&txtype, body)) = original.split_first() else { + return Err(anyhow!("Empty storage value transaction bytes")); + }; + + match txtype { + STORAGE_BOOL_TYPE => Ok(Transaction::StorageBool( + StorageBool::from_bytes(txtype, body).await?, + )), + STORAGE_U8_TYPE => Ok(Transaction::StorageU8( + StorageU8::from_bytes(txtype, body).await?, + )), + STORAGE_U16_TYPE => Ok(Transaction::StorageU16( + StorageU16::from_bytes(txtype, body).await?, + )), + STORAGE_U32_TYPE => Ok(Transaction::StorageU32( + StorageU32::from_bytes(txtype, body).await?, + )), + STORAGE_U64_TYPE => Ok(Transaction::StorageU64( + StorageU64::from_bytes(txtype, body).await?, + )), + STORAGE_U128_TYPE => Ok(Transaction::StorageU128( + StorageU128::from_bytes(txtype, body).await?, + )), + STORAGE_STRING_TYPE => Ok(Transaction::StorageString( + StorageString::from_bytes(txtype, body).await?, + )), + _ => Err(anyhow!( + "Unsupported storage value transaction type {txtype}" + )), + } +} + +fn selected_storage_value_parts(transaction: &Transaction) -> Result { + let parts = match transaction { + Transaction::StorageBool(tx) => tx.storage_parts(), + Transaction::StorageU8(tx) => tx.storage_parts(), + Transaction::StorageU16(tx) => tx.storage_parts(), + Transaction::StorageU32(tx) => tx.storage_parts(), + Transaction::StorageU64(tx) => tx.storage_parts(), + Transaction::StorageU128(tx) => tx.storage_parts(), + Transaction::StorageString(tx) => tx.storage_parts(), + _ => return Err(anyhow!("Unsupported storage value transaction")), + }; + + Ok(SelectedStorageValueParts { + storagekey: parts.storagekey.to_string(), + key: parts.key.to_string(), + address: parts.address.to_string(), + }) +} + +fn selected_storage_value_bytes(transaction: &Transaction) -> Result> { + match transaction { + Transaction::StorageBool(tx) => tx.storage_value_bytes(), + Transaction::StorageU8(tx) => tx.storage_value_bytes(), + Transaction::StorageU16(tx) => tx.storage_value_bytes(), + Transaction::StorageU32(tx) => tx.storage_value_bytes(), + Transaction::StorageU64(tx) => tx.storage_value_bytes(), + Transaction::StorageU128(tx) => tx.storage_value_bytes(), + Transaction::StorageString(tx) => tx.storage_value_bytes(), + _ => Err("Unsupported storage value transaction".to_string()), + } + .map_err(|err| anyhow!(err)) +} + fn index_selected_wallet_transaction( pending_effects: &mut PendingEffects, addresses: &[&str], @@ -209,6 +289,44 @@ pub async fn select_transactions_for_block(limit: i64) -> Result Result SelectedMempoolTransaction::StorageKey { + id: row.get("id"), + fee: row.get("priority_fee"), + address: required_string(&row, "address")?, + hash: row.get("hash"), + }, + "storage_value" => SelectedMempoolTransaction::StorageValue { + id: row.get("id"), + fee: row.get("priority_fee"), + address: required_string(&row, "address")?, + hash: row.get("hash"), + original: row.get("original"), + }, _ => return Err(anyhow!("Unsupported mempool kind {kind}")), }; transactions.push(transaction); @@ -946,6 +1077,77 @@ pub async fn apply_selected_transaction_math( &decode(hash)?, )?; } + SelectedMempoolTransaction::StorageKey { + fee, address, hash, .. + } => { + add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); + let txhash_bytes = decode(hash)?; + pending_effects.create_tree(txhash_bytes.clone()); + pending_effects.set_tree( + "txid", + txhash_bytes.clone(), + format!("{block_number}:{tx_index}").into_bytes(), + ); + index_selected_wallet_transaction( + pending_effects, + &[address], + block_number, + tx_index, + &txhash_bytes, + )?; + } + SelectedMempoolTransaction::StorageValue { + fee, + address, + hash, + original, + .. + } => { + let transaction = storage_value_transaction_from_original(original).await?; + let parts = selected_storage_value_parts(&transaction)?; + let storage_tree = decode(&parts.storagekey)?; + let txhash_bytes = decode(hash)?; + let storage_record_key = match &transaction { + Transaction::StorageString(tx) => storage_string_record_key(db, tx) + .await + .map_err(|err| anyhow!(err))?, + _ => storage_record_key(&parts.address, &parts.key) + .map_err(|err| anyhow!(err))?, + }; + let value_bytes = selected_storage_value_bytes(&transaction)?; + + pending_effects.set_dynamic_tree( + storage_tree, + storage_record_key, + value_bytes, + txhash_bytes.clone(), + ); + add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); + add_balance_change_bytes( + &mut balance_changes, + miner_address.clone(), + base_coin.clone(), + *fee, + ); + pending_effects.set_tree( + "txid", + txhash_bytes.clone(), + format!("{block_number}:{tx_index}").into_bytes(), + ); + index_selected_wallet_transaction( + pending_effects, + &[address], + block_number, + tx_index, + &txhash_bytes, + )?; + } } } @@ -1028,6 +1230,8 @@ pub async fn delete_selected_transactions(batch: &SelectedMempoolBatch) -> Resul let lender_ids = ids_for_table(batch, "loan_contract"); let borrower_ids = ids_for_table(batch, "loan_payment"); let collateral_ids = ids_for_table(batch, "collateral_claim"); + let storage_key_ids = ids_for_table(batch, "storage_key"); + let storage_value_ids = ids_for_table(batch, "storage_value"); delete_rows("transfer", &transfer_ids).await?; delete_rows("token", &token_ids).await?; @@ -1040,6 +1244,8 @@ pub async fn delete_selected_transactions(batch: &SelectedMempoolBatch) -> Resul delete_rows("loan_contract", &lender_ids).await?; delete_rows("loan_payment", &borrower_ids).await?; delete_rows("collateral_claim", &collateral_ids).await?; + delete_rows("storage_key", &storage_key_ids).await?; + delete_rows("storage_value", &storage_value_ids).await?; Ok(()) } diff --git a/src/records/record_chain/mod.rs b/src/records/record_chain/mod.rs index 40f211f..366bea7 100644 --- a/src/records/record_chain/mod.rs +++ b/src/records/record_chain/mod.rs @@ -18,6 +18,8 @@ pub mod previous_difficulty; pub mod rewards_tx; pub mod save; pub mod save_flags; +pub mod storage_key_tx; +pub mod storage_value_tx; pub mod structs; pub mod swap_tx; pub mod token_provenance; diff --git a/src/records/record_chain/parse_transactions.rs b/src/records/record_chain/parse_transactions.rs index 2b26572..e52e5b3 100644 --- a/src/records/record_chain/parse_transactions.rs +++ b/src/records/record_chain/parse_transactions.rs @@ -10,6 +10,8 @@ use crate::records::record_chain::marketing_tx::process_marketing; use crate::records::record_chain::nft_tx::process_nft; use crate::records::record_chain::pending_effects::PendingEffects; use crate::records::record_chain::rewards_tx::process_rewards; +use crate::records::record_chain::storage_key_tx::process_storage_key; +use crate::records::record_chain::storage_value_tx::process_storage_value; use crate::records::record_chain::swap_tx::process_swap; use crate::records::record_chain::token_tx::process_token; use crate::records::record_chain::transfer_tx::process_transfer; @@ -188,6 +190,36 @@ pub async fn handle_transactions( ) .await } + Transaction::StorageKey(storage_key_transaction) => { + process_storage_key( + storage_key_transaction, + binary_data.clone(), + db, + index_mutex.clone(), + miner.clone(), + block_header_number, + pending_effects, + ) + .await + } + Transaction::StorageBool(_) + | Transaction::StorageU8(_) + | Transaction::StorageU16(_) + | Transaction::StorageU32(_) + | Transaction::StorageU64(_) + | Transaction::StorageU128(_) + | Transaction::StorageString(_) => { + process_storage_value( + transaction, + binary_data.clone(), + db, + index_mutex.clone(), + miner.clone(), + block_header_number, + pending_effects, + ) + .await + } }; match result { Ok(new_binary_data) => { diff --git a/src/records/record_chain/pending_effects.rs b/src/records/record_chain/pending_effects.rs index 2c08f30..aaee549 100644 --- a/src/records/record_chain/pending_effects.rs +++ b/src/records/record_chain/pending_effects.rs @@ -1,3 +1,4 @@ +use crate::common::types::{STORAGE_KEY_INDEX_TREE, STORAGE_VALUE_ROLLBACK_TREE}; use crate::log::error; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::wallet_registry::{ @@ -41,6 +42,12 @@ pub enum PendingEffect { key: Vec, value: Vec, }, + DynamicTreeSet { + tree: Vec, + key: Vec, + value: Vec, + rollback_key: Vec, + }, TreeRemove { tree: &'static str, key: Vec, @@ -56,6 +63,9 @@ pub enum PendingEffect { key: Vec, bytes: Vec, }, + TreeCreate { + name: Vec, + }, TokenSupplyAdd { ticker: String, amount: u64, @@ -108,6 +118,21 @@ impl PendingEffects { self.push(PendingEffect::TreeSet { tree, key, value }); } + pub fn set_dynamic_tree( + &mut self, + tree: Vec, + key: Vec, + value: Vec, + rollback_key: Vec, + ) { + self.push(PendingEffect::DynamicTreeSet { + tree, + key, + value, + rollback_key, + }); + } + pub fn remove_tree(&mut self, tree: &'static str, key: Vec) { self.push(PendingEffect::TreeRemove { tree, key }); } @@ -131,6 +156,10 @@ impl PendingEffects { }); } + pub fn create_tree(&mut self, name: Vec) { + self.push(PendingEffect::TreeCreate { name }); + } + pub fn add_token_supply(&mut self, ticker: &str, amount: u64) { self.push(PendingEffect::TokenSupplyAdd { ticker: ticker.to_string(), @@ -230,6 +259,13 @@ enum AppliedEffect { key: Vec, previous: Option>, }, + DynamicTreeMutation { + tree: Vec, + key: Vec, + previous: Option>, + rollback_key: Vec, + previous_rollback: Option>, + }, Composite(Vec), VanityUpdate { owner_address: String, @@ -237,6 +273,9 @@ enum AppliedEffect { rollback_key: Vec, previous_rollback: Option>, }, + TreeCreated { + name: Vec, + }, Noop, } @@ -270,6 +309,34 @@ fn apply_effect(db: &Db, effect: &PendingEffect) -> Result { + let previous = set_dynamic_tree_value(db, tree, key, value)?; + let rollback_tree = db + .open_tree(STORAGE_VALUE_ROLLBACK_TREE) + .map_err(|err| format!("Failed to open storage value rollback tree: {err}"))?; + let previous_rollback = rollback_tree + .get(rollback_key) + .map_err(|err| format!("Failed to read previous storage rollback value: {err}"))? + .map(|value| value.to_vec()); + rollback_tree + .insert( + rollback_key, + encode_storage_rollback_value(previous.as_deref()), + ) + .map_err(|err| format!("Failed to write storage rollback value: {err}"))?; + Ok(AppliedEffect::DynamicTreeMutation { + tree: tree.clone(), + key: key.clone(), + previous, + rollback_key: rollback_key.clone(), + previous_rollback, + }) + } PendingEffect::TreeRemove { tree, key } => { let tree_handle = db .open_tree(tree) @@ -314,6 +381,27 @@ fn apply_effect(db: &Db, effect: &PendingEffect) -> Result { + let index_tree = db + .open_tree(STORAGE_KEY_INDEX_TREE) + .map_err(|err| format!("Failed to open storage key index tree: {err}"))?; + if index_tree + .contains_key(name) + .map_err(|err| format!("Failed to check storage key index tree: {err}"))? + { + return Err(format!( + "Storage tree already exists for key {}", + crate::encode(name) + )); + } + db.open_tree(name) + .map_err(|err| format!("Failed to create storage tree: {err}"))?; + if let Err(err) = index_tree.insert(name, b"1".as_slice()) { + let _ = db.drop_tree(name); + return Err(format!("Failed to index storage tree: {err}")); + } + Ok(AppliedEffect::TreeCreated { name: name.clone() }) + } PendingEffect::TokenSupplyAdd { ticker, amount } => { let tree = db .open_tree("tokens") @@ -410,6 +498,21 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> { key, previous, } => restore_tree_value(db, tree, &key, previous), + AppliedEffect::DynamicTreeMutation { + tree, + key, + previous, + rollback_key, + previous_rollback, + } => { + restore_dynamic_tree_value(db, &tree, &key, previous)?; + restore_tree_value( + db, + STORAGE_VALUE_ROLLBACK_TREE, + &rollback_key, + previous_rollback, + ) + } AppliedEffect::Composite(mut effects) => { let mut first_error = None; while let Some(effect) = effects.pop() { @@ -438,6 +541,17 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> { previous_rollback, ) } + AppliedEffect::TreeCreated { name } => { + let index_tree = db.open_tree(STORAGE_KEY_INDEX_TREE).map_err(|err| { + format!("Failed to open storage key index tree during rollback: {err}") + })?; + index_tree.remove(&name).map_err(|err| { + format!("Failed to remove storage key index during rollback: {err}") + })?; + db.drop_tree(&name).map(|_| ()).map_err(|err| { + format!("Failed to drop created storage tree during rollback: {err}") + }) + } AppliedEffect::Noop => Ok(()), } } @@ -461,6 +575,37 @@ fn set_tree_value( Ok(previous) } +fn set_dynamic_tree_value( + db: &Db, + tree: &[u8], + key: &[u8], + value: &[u8], +) -> Result>, String> { + let tree_handle = db + .open_tree(tree) + .map_err(|err| format!("Failed to open dynamic storage tree: {err}"))?; + let previous = tree_handle + .get(key) + .map_err(|err| format!("Failed to read previous dynamic storage value: {err}"))? + .map(|value| value.to_vec()); + tree_handle + .insert(key, value) + .map_err(|err| format!("Failed to write dynamic storage value: {err}"))?; + Ok(previous) +} + +fn encode_storage_rollback_value(previous: Option<&[u8]>) -> Vec { + match previous { + Some(value) => { + let mut encoded = Vec::with_capacity(value.len() + 1); + encoded.push(1); + encoded.extend_from_slice(value); + encoded + } + None => vec![0], + } +} + fn append_tree_value( db: &Db, tree: &'static str, @@ -506,6 +651,30 @@ fn restore_tree_value( Ok(()) } +fn restore_dynamic_tree_value( + db: &Db, + tree: &[u8], + key: &[u8], + previous: Option>, +) -> Result<(), String> { + let tree_handle = db + .open_tree(tree) + .map_err(|err| format!("Failed to open dynamic storage tree during rollback: {err}"))?; + match previous { + Some(value) => { + tree_handle + .insert(key, value) + .map_err(|err| format!("Failed to restore dynamic storage value: {err}"))?; + } + None => { + tree_handle.remove(key).map_err(|err| { + format!("Failed to remove dynamic storage value during rollback: {err}") + })?; + } + } + Ok(()) +} + fn read_supply(bytes: &[u8]) -> Result { if bytes.len() != 8 { return Err("Invalid token supply bytes".to_string()); diff --git a/src/records/record_chain/storage_key_tx.rs b/src/records/record_chain/storage_key_tx.rs new file mode 100644 index 0000000..29793ff --- /dev/null +++ b/src/records/record_chain/storage_key_tx.rs @@ -0,0 +1,56 @@ +use crate::blocks::storage_key::StorageKey; +use crate::decode; +use crate::records::memory::mempool::BASECOIN; +use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; +use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; +use crate::sled::Db; +use crate::Arc; +use crate::Mutex; + +pub async fn process_storage_key( + transaction: &StorageKey, + mut binary_data: Vec, + _db: &Db, + index_counter: Arc>, + miner: String, + block_header_number: u32, + pending_effects: &mut PendingEffects, +) -> Result, String> { + let mut index = index_counter.lock().await; + **index += 1; + + let txhash = transaction.unsigned_storagekey.hash().await; + let txhash_bytes = + decode(&txhash).map_err(|err| format!("Could not decode storage key txhash: {err}"))?; + let transaction_bytes = transaction + .to_bytes() + .await + .map_err(|err| format!("Could not serialize storage key transaction: {err}"))?; + binary_data.extend(transaction_bytes); + + pending_effects.create_tree(txhash_bytes.clone()); + pending_effects.add_balance( + &miner, + transaction.unsigned_storagekey.txfee, + &BASECOIN, + BalanceOperand::Addition, + ); + pending_effects.add_balance( + &transaction.unsigned_storagekey.address, + transaction.unsigned_storagekey.txfee, + &BASECOIN, + BalanceOperand::Subtraction, + ); + + let txvalue = format!("{block_header_number}:{}", **index); + pending_effects.set_tree("txid", txhash_bytes.clone(), txvalue.into_bytes()); + index_wallet_transaction( + pending_effects, + &[&transaction.unsigned_storagekey.address], + block_header_number, + **index as u32, + &txhash_bytes, + )?; + + Ok(binary_data) +} diff --git a/src/records/record_chain/storage_value_tx.rs b/src/records/record_chain/storage_value_tx.rs new file mode 100644 index 0000000..bd891b6 --- /dev/null +++ b/src/records/record_chain/storage_value_tx.rs @@ -0,0 +1,141 @@ +use crate::blocks::storage_values::{ + storage_record_key, storage_string_record_key, StorageValueTransaction, +}; +use crate::common::types::Transaction; +use crate::decode; +use crate::records::memory::mempool::BASECOIN; +use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; +use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; +use crate::sled::Db; +use crate::Arc; +use crate::Mutex; + +struct OwnedStorageParts { + storagekey: String, + key: String, + address: String, + txfee: u64, + hash: String, +} + +async fn storage_transaction_bytes(transaction: &Transaction) -> Result, String> { + match transaction { + Transaction::StorageBool(tx) => tx + .to_bytes() + .await + .map_err(|err| format!("Could not serialize storage bool transaction: {err}")), + Transaction::StorageU8(tx) => tx + .to_bytes() + .await + .map_err(|err| format!("Could not serialize storage u8 transaction: {err}")), + Transaction::StorageU16(tx) => tx + .to_bytes() + .await + .map_err(|err| format!("Could not serialize storage u16 transaction: {err}")), + Transaction::StorageU32(tx) => tx + .to_bytes() + .await + .map_err(|err| format!("Could not serialize storage u32 transaction: {err}")), + Transaction::StorageU64(tx) => tx + .to_bytes() + .await + .map_err(|err| format!("Could not serialize storage u64 transaction: {err}")), + Transaction::StorageU128(tx) => tx + .to_bytes() + .await + .map_err(|err| format!("Could not serialize storage u128 transaction: {err}")), + Transaction::StorageString(tx) => tx + .to_bytes() + .await + .map_err(|err| format!("Could not serialize storage string transaction: {err}")), + _ => Err("Unsupported storage value transaction.".to_string()), + } +} + +fn storage_parts(transaction: &Transaction) -> Result { + let parts = match transaction { + Transaction::StorageBool(tx) => tx.storage_parts(), + Transaction::StorageU8(tx) => tx.storage_parts(), + Transaction::StorageU16(tx) => tx.storage_parts(), + Transaction::StorageU32(tx) => tx.storage_parts(), + Transaction::StorageU64(tx) => tx.storage_parts(), + Transaction::StorageU128(tx) => tx.storage_parts(), + Transaction::StorageString(tx) => tx.storage_parts(), + _ => return Err("Unsupported storage value transaction.".to_string()), + }; + + Ok(OwnedStorageParts { + storagekey: parts.storagekey.to_string(), + key: parts.key.to_string(), + address: parts.address.to_string(), + txfee: parts.txfee, + hash: parts.hash, + }) +} + +fn storage_value_bytes(transaction: &Transaction) -> Result, String> { + match transaction { + Transaction::StorageBool(tx) => tx.storage_value_bytes(), + Transaction::StorageU8(tx) => tx.storage_value_bytes(), + Transaction::StorageU16(tx) => tx.storage_value_bytes(), + Transaction::StorageU32(tx) => tx.storage_value_bytes(), + Transaction::StorageU64(tx) => tx.storage_value_bytes(), + Transaction::StorageU128(tx) => tx.storage_value_bytes(), + Transaction::StorageString(tx) => tx.storage_value_bytes(), + _ => Err("Unsupported storage value transaction.".to_string()), + } +} + +pub async fn process_storage_value( + transaction: &Transaction, + mut binary_data: Vec, + db: &Db, + index_counter: Arc>, + miner: String, + block_header_number: u32, + pending_effects: &mut PendingEffects, +) -> Result, String> { + let mut index = index_counter.lock().await; + **index += 1; + + let parts = storage_parts(transaction)?; + let storage_tree = decode(&parts.storagekey) + .map_err(|err| format!("Could not decode storage tree key: {err}"))?; + let txhash_bytes = + decode(&parts.hash).map_err(|err| format!("Could not decode storage txhash: {err}"))?; + + let storage_record_key = match transaction { + Transaction::StorageString(tx) => storage_string_record_key(db, tx).await?, + _ => storage_record_key(&parts.address, &parts.key)?, + }; + + let value_bytes = storage_value_bytes(transaction)?; + let transaction_bytes = storage_transaction_bytes(transaction).await?; + binary_data.extend(transaction_bytes); + + pending_effects.set_dynamic_tree( + storage_tree, + storage_record_key, + value_bytes, + txhash_bytes.clone(), + ); + pending_effects.add_balance(&miner, parts.txfee, &BASECOIN, BalanceOperand::Addition); + pending_effects.add_balance( + &parts.address, + parts.txfee, + &BASECOIN, + BalanceOperand::Subtraction, + ); + + let txvalue = format!("{block_header_number}:{}", **index); + pending_effects.set_tree("txid", txhash_bytes.clone(), txvalue.into_bytes()); + index_wallet_transaction( + pending_effects, + &[&parts.address], + block_header_number, + **index as u32, + &txhash_bytes, + )?; + + Ok(binary_data) +} diff --git a/src/records/unpack_block/load_by_binary_data.rs b/src/records/unpack_block/load_by_binary_data.rs index ffe6ff3..44a670e 100644 --- a/src/records/unpack_block/load_by_binary_data.rs +++ b/src/records/unpack_block/load_by_binary_data.rs @@ -8,14 +8,19 @@ use crate::blocks::loans::LoanContractTransaction; use crate::blocks::marketing::MarketingTransaction; use crate::blocks::nft::CreateNftTransaction; use crate::blocks::rewards::RewardsTransaction; +use crate::blocks::storage_key::StorageKey; +use crate::blocks::storage_values::{ + StorageBool, StorageString, StorageU128, StorageU16, StorageU32, StorageU64, StorageU8, +}; use crate::blocks::swap::SwapTransaction; use crate::blocks::token::CreateTokenTransaction; use crate::blocks::transfer::TransferTransaction; use crate::blocks::vanity::VanityAddressTransaction; use crate::common::types::{ Transaction, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, - GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, SWAP_TYPE, - TRANSFER_TYPE, VANITY_ADDRESS_TYPE, + GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, STORAGE_BOOL_TYPE, + STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, STORAGE_U32_TYPE, + STORAGE_U64_TYPE, STORAGE_U8_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE, }; use crate::rpc::command_maps::get_bytes; @@ -192,6 +197,78 @@ pub async fn load_block_from_binary(binary_data: &[u8]) -> Result i += body_len; vanity } + STORAGE_KEY_TYPE => { + let storage_key = Transaction::StorageKey( + StorageKey::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage_key + } + STORAGE_BOOL_TYPE => { + let storage = Transaction::StorageBool( + StorageBool::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U8_TYPE => { + let storage = Transaction::StorageU8( + StorageU8::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U16_TYPE => { + let storage = Transaction::StorageU16( + StorageU16::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U32_TYPE => { + let storage = Transaction::StorageU32( + StorageU32::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U64_TYPE => { + let storage = Transaction::StorageU64( + StorageU64::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U128_TYPE => { + let storage = Transaction::StorageU128( + StorageU128::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_STRING_TYPE => { + let storage = Transaction::StorageString( + StorageString::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } _ => { return Err(format!("Unsupported transaction type: {txtype}")); } diff --git a/src/records/unpack_block/load_by_block_number.rs b/src/records/unpack_block/load_by_block_number.rs index 1bd6239..c9e05df 100644 --- a/src/records/unpack_block/load_by_block_number.rs +++ b/src/records/unpack_block/load_by_block_number.rs @@ -1,225 +1,302 @@ -use crate::blocks::block::{Block, VrfBlock, VRF_BLOCK_BYTES}; -use crate::blocks::burn::BurnTransaction; -use crate::blocks::collateral::CollateralClaimTransaction; -use crate::blocks::genesis::GenesisTransaction; -use crate::blocks::issue_token::IssueTokenTransaction; -use crate::blocks::loan_payment::ContractPaymentTransaction; -use crate::blocks::loans::LoanContractTransaction; -use crate::blocks::marketing::MarketingTransaction; -use crate::blocks::nft::CreateNftTransaction; -use crate::blocks::rewards::RewardsTransaction; -use crate::blocks::swap::SwapTransaction; -use crate::blocks::token::CreateTokenTransaction; -use crate::blocks::transfer::TransferTransaction; -use crate::blocks::vanity::VanityAddressTransaction; -use crate::common::network_paths_and_settings::block_extension_and_paths; -use crate::common::types::{ - Transaction, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, - GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, SWAP_TYPE, - TRANSFER_TYPE, VANITY_ADDRESS_TYPE, -}; -use crate::fs; -use crate::rpc::command_maps::get_bytes; -use crate::PathBuf; - -// The transaction body helpers keep the block parser aligned with the command map sizes. -fn transaction_body_len(txtype: u8) -> Result { - let total_len = get_bytes(txtype); - if total_len <= 1 { - return Err(format!("Unknown transaction type: {txtype}")); - } - - // get_bytes includes the transaction type byte; parser bodies start after - // that byte has already been consumed. - Ok(total_len - 1) -} - -fn transaction_body_slice( - binary_data: &[u8], - start: usize, - body_len: usize, -) -> Result<&[u8], String> { - // Slice with bounds checking so truncated block files fail cleanly instead - // of panicking during transaction parsing. - binary_data - .get(start..start + body_len) - .ok_or_else(|| format!("Truncated transaction body at offset {start}")) -} - -pub async fn load_block(block_number: u32) -> Result { - // Blocks are loaded from disk by height, then split back into the header and - // variable-length transaction payloads. - let ( - _network_name, - _padded_base_coin, - block_ext, - _torrent_path, - _wallet_path, - block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let file_name = PathBuf::from(block_path) - .join(format!("{block_number}.{block_ext}")) - .to_string_lossy() - .into_owned(); - - // Load the full block because this path reconstructs both the header and - // every transaction for validation or inspection. +use crate::blocks::block::{Block, VrfBlock, VRF_BLOCK_BYTES}; +use crate::blocks::burn::BurnTransaction; +use crate::blocks::collateral::CollateralClaimTransaction; +use crate::blocks::genesis::GenesisTransaction; +use crate::blocks::issue_token::IssueTokenTransaction; +use crate::blocks::loan_payment::ContractPaymentTransaction; +use crate::blocks::loans::LoanContractTransaction; +use crate::blocks::marketing::MarketingTransaction; +use crate::blocks::nft::CreateNftTransaction; +use crate::blocks::rewards::RewardsTransaction; +use crate::blocks::storage_key::StorageKey; +use crate::blocks::storage_values::{ + StorageBool, StorageString, StorageU128, StorageU16, StorageU32, StorageU64, StorageU8, +}; +use crate::blocks::swap::SwapTransaction; +use crate::blocks::token::CreateTokenTransaction; +use crate::blocks::transfer::TransferTransaction; +use crate::blocks::vanity::VanityAddressTransaction; +use crate::common::network_paths_and_settings::block_extension_and_paths; +use crate::common::types::{ + Transaction, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, + GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, STORAGE_BOOL_TYPE, + STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, STORAGE_U32_TYPE, + STORAGE_U64_TYPE, STORAGE_U8_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE, +}; +use crate::fs; +use crate::rpc::command_maps::get_bytes; +use crate::PathBuf; + +// The transaction body helpers keep the block parser aligned with the command map sizes. +fn transaction_body_len(txtype: u8) -> Result { + let total_len = get_bytes(txtype); + if total_len <= 1 { + return Err(format!("Unknown transaction type: {txtype}")); + } + + // get_bytes includes the transaction type byte; parser bodies start after + // that byte has already been consumed. + Ok(total_len - 1) +} + +fn transaction_body_slice( + binary_data: &[u8], + start: usize, + body_len: usize, +) -> Result<&[u8], String> { + // Slice with bounds checking so truncated block files fail cleanly instead + // of panicking during transaction parsing. + binary_data + .get(start..start + body_len) + .ok_or_else(|| format!("Truncated transaction body at offset {start}")) +} + +pub async fn load_block(block_number: u32) -> Result { + // Blocks are loaded from disk by height, then split back into the header and + // variable-length transaction payloads. + let ( + _network_name, + _padded_base_coin, + block_ext, + _torrent_path, + _wallet_path, + block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + let file_name = PathBuf::from(block_path) + .join(format!("{block_number}.{block_ext}")) + .to_string_lossy() + .into_owned(); + + // Load the full block because this path reconstructs both the header and + // every transaction for validation or inspection. let binary_data = match fs::read(&file_name) { Ok(data) => data, Err(err) => { return Err(format!("Unable to read block {block_number}: {err:?}")); } }; - - if binary_data.len() < VRF_BLOCK_BYTES { - return Err("Unable to load block: binary data shorter than VrfBlock header".to_string()); - } - - let vrf_block = VrfBlock::from_bytes(&binary_data[0..VRF_BLOCK_BYTES]) - .await - .map_err(|e| e.to_string())?; - let mut i = VRF_BLOCK_BYTES; - let mut transactions: Vec = Vec::new(); - - while i < binary_data.len() { - // Each stored transaction begins with its type byte, followed by the fixed-size - // body for that transaction family. - let txtype = binary_data[i]; - i += 1; - let body_len = transaction_body_len(txtype)?; - let body = transaction_body_slice(&binary_data, i, body_len)?; - let transaction = match txtype { - GENESIS_TYPE => { - let genesis = Transaction::Genesis( - GenesisTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - genesis - } - REWARDS_TYPE => { - let rewards = Transaction::Rewards( - RewardsTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - rewards - } - TRANSFER_TYPE => { - let transfer = Transaction::Transfer( - TransferTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - transfer - } - BURN_TYPE => { - let burn = Transaction::Burn( - BurnTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - burn - } - CREATE_TOKEN_TYPE => { - let create_token = Transaction::Token( - CreateTokenTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - create_token - } - ISSUE_TOKEN_TYPE => { - let issue_token = Transaction::IssueToken( - IssueTokenTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - issue_token - } - CREATE_NFT_TYPE => { - let create_nft = Transaction::Nft( - CreateNftTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - create_nft - } - MARKETING_TYPE => { - let marketing = Transaction::Marketing( - MarketingTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - marketing - } - SWAP_TYPE => { - let swap = Transaction::Swap( - SwapTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - swap - } - LENDER_TYPE => { - let loan = Transaction::Lender( - LoanContractTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - loan - } - BORROWER_TYPE => { - let payment = Transaction::Borrower( - ContractPaymentTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - payment - } - COLLATERAL_TYPE => { - let collateral = Transaction::Collateral( - CollateralClaimTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - collateral - } - VANITY_ADDRESS_TYPE => { - let vanity = Transaction::Vanity( - VanityAddressTransaction::from_bytes(txtype, body) - .await - .map_err(|e| e.to_string())?, - ); - i += body_len; - vanity - } - _ => { - return Err(format!("Unsupported transaction type: {txtype}")); - } - }; - transactions.push(transaction); - } - - let block = Block { - vrf_block, - transactions, - }; - - Ok(block) -} + + if binary_data.len() < VRF_BLOCK_BYTES { + return Err("Unable to load block: binary data shorter than VrfBlock header".to_string()); + } + + let vrf_block = VrfBlock::from_bytes(&binary_data[0..VRF_BLOCK_BYTES]) + .await + .map_err(|e| e.to_string())?; + let mut i = VRF_BLOCK_BYTES; + let mut transactions: Vec = Vec::new(); + + while i < binary_data.len() { + // Each stored transaction begins with its type byte, followed by the fixed-size + // body for that transaction family. + let txtype = binary_data[i]; + i += 1; + let body_len = transaction_body_len(txtype)?; + let body = transaction_body_slice(&binary_data, i, body_len)?; + let transaction = match txtype { + GENESIS_TYPE => { + let genesis = Transaction::Genesis( + GenesisTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + genesis + } + REWARDS_TYPE => { + let rewards = Transaction::Rewards( + RewardsTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + rewards + } + TRANSFER_TYPE => { + let transfer = Transaction::Transfer( + TransferTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + transfer + } + BURN_TYPE => { + let burn = Transaction::Burn( + BurnTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + burn + } + CREATE_TOKEN_TYPE => { + let create_token = Transaction::Token( + CreateTokenTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + create_token + } + ISSUE_TOKEN_TYPE => { + let issue_token = Transaction::IssueToken( + IssueTokenTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + issue_token + } + CREATE_NFT_TYPE => { + let create_nft = Transaction::Nft( + CreateNftTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + create_nft + } + MARKETING_TYPE => { + let marketing = Transaction::Marketing( + MarketingTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + marketing + } + SWAP_TYPE => { + let swap = Transaction::Swap( + SwapTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + swap + } + LENDER_TYPE => { + let loan = Transaction::Lender( + LoanContractTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + loan + } + BORROWER_TYPE => { + let payment = Transaction::Borrower( + ContractPaymentTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + payment + } + COLLATERAL_TYPE => { + let collateral = Transaction::Collateral( + CollateralClaimTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + collateral + } + VANITY_ADDRESS_TYPE => { + let vanity = Transaction::Vanity( + VanityAddressTransaction::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + vanity + } + STORAGE_KEY_TYPE => { + let storage_key = Transaction::StorageKey( + StorageKey::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage_key + } + STORAGE_BOOL_TYPE => { + let storage = Transaction::StorageBool( + StorageBool::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U8_TYPE => { + let storage = Transaction::StorageU8( + StorageU8::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U16_TYPE => { + let storage = Transaction::StorageU16( + StorageU16::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U32_TYPE => { + let storage = Transaction::StorageU32( + StorageU32::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U64_TYPE => { + let storage = Transaction::StorageU64( + StorageU64::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_U128_TYPE => { + let storage = Transaction::StorageU128( + StorageU128::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + STORAGE_STRING_TYPE => { + let storage = Transaction::StorageString( + StorageString::from_bytes(txtype, body) + .await + .map_err(|e| e.to_string())?, + ); + i += body_len; + storage + } + _ => { + return Err(format!("Unsupported transaction type: {txtype}")); + } + }; + transactions.push(transaction); + } + + let block = Block { + vrf_block, + transactions, + }; + + Ok(block) +} diff --git a/src/rpc/command_maps.rs b/src/rpc/command_maps.rs index 062bd09..8edaa38 100644 --- a/src/rpc/command_maps.rs +++ b/src/rpc/command_maps.rs @@ -7,6 +7,10 @@ use crate::blocks::loans::LoanContractTransaction; use crate::blocks::marketing::MarketingTransaction; use crate::blocks::nft::CreateNftTransaction; use crate::blocks::rewards::RewardsTransaction; +use crate::blocks::storage_key::StorageKey; +use crate::blocks::storage_values::{ + StorageBool, StorageString, StorageU128, StorageU16, StorageU32, StorageU64, StorageU8, +}; use crate::blocks::swap::SwapTransaction; use crate::blocks::token::CreateTokenTransaction; use crate::blocks::transfer::TransferTransaction; @@ -62,6 +66,8 @@ pub const RPC_TOKEN_CATALOG: u8 = 48; pub const RPC_COLLATERAL_STATUS: u8 = 49; pub const RPC_NETWORK_MONITOR_ADD: u8 = 50; pub const RPC_NETWORK_MONITOR_REMOVE: u8 = 51; +pub const RPC_STORAGE_LOOKUP_COST: u8 = 52; +pub const RPC_STORAGE_LOOKUP: u8 = 53; pub const RPC_REPLY: u8 = 255; pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024; @@ -85,6 +91,14 @@ pub fn get_bytes(tx_type: u8) -> usize { 10 => BurnTransaction::BYTE_LENGTH, 11 => IssueTokenTransaction::BYTE_LENGTH, 12 => VanityAddressTransaction::BYTE_LENGTH, + 100 => StorageKey::BYTE_LENGTH, + 101 => StorageBool::BYTE_LENGTH, + 102 => StorageU8::BYTE_LENGTH, + 103 => StorageU16::BYTE_LENGTH, + 104 => StorageU32::BYTE_LENGTH, + 105 => StorageU64::BYTE_LENGTH, + 106 => StorageU128::BYTE_LENGTH, + 107 => StorageString::BYTE_LENGTH, _ => 0, } } diff --git a/src/rpc/commands/address_history.rs b/src/rpc/commands/address_history.rs index f511f82..1712a84 100644 --- a/src/rpc/commands/address_history.rs +++ b/src/rpc/commands/address_history.rs @@ -128,6 +128,14 @@ async fn miner_earnings_from_transactions( } Transaction::Collateral(tx) => earnings.add_fee(tx.unsigned_collateral_claim.txfee)?, Transaction::Vanity(tx) => earnings.add_fee(tx.unsigned_vanity_address.txfee)?, + Transaction::StorageKey(tx) => earnings.add_fee(tx.unsigned_storagekey.txfee)?, + Transaction::StorageBool(tx) => earnings.add_fee(tx.unsigned_storagebool.txfee)?, + Transaction::StorageU8(tx) => earnings.add_fee(tx.unsigned_storageu8.txfee)?, + Transaction::StorageU16(tx) => earnings.add_fee(tx.unsigned_storageu16.txfee)?, + Transaction::StorageU32(tx) => earnings.add_fee(tx.unsigned_storageu32.txfee)?, + Transaction::StorageU64(tx) => earnings.add_fee(tx.unsigned_storageu64.txfee)?, + Transaction::StorageU128(tx) => earnings.add_fee(tx.unsigned_storageu128.txfee)?, + Transaction::StorageString(tx) => earnings.add_fee(tx.unsigned_storage.txfee)?, } } @@ -146,7 +154,7 @@ async fn miner_earnings_for_block(db: &Db, block_height: u32) -> Result, fn signature_count(transaction_type: u8) -> usize { match transaction_type { 6 | 7 => 2, - 2..=5 | 8..=12 => 1, + 2..=5 | 8..=12 | 100..=107 => 1, _ => 0, } } diff --git a/src/rpc/commands/mod.rs b/src/rpc/commands/mod.rs index 6eb2fe4..ada8809 100644 --- a/src/rpc/commands/mod.rs +++ b/src/rpc/commands/mod.rs @@ -27,6 +27,7 @@ pub mod random_node; pub mod receive_torrent; pub mod request_valid_nodes; pub mod route_reply; +pub mod storage_lookup; pub mod structs; pub mod time; pub mod token_list; diff --git a/src/rpc/commands/storage_lookup.rs b/src/rpc/commands/storage_lookup.rs new file mode 100644 index 0000000..91119a9 --- /dev/null +++ b/src/rpc/commands/storage_lookup.rs @@ -0,0 +1,372 @@ +use crate::blocks::storage_values::{ + trim_storage_component, STORAGE_KEY_HASH_BYTES, STORAGE_STRING_CHUNK_LIMIT, + STORAGE_STRING_STORED_HASH_BYTES, STORAGE_VALUE_KEY_BYTES, +}; +use crate::blocks::transfer::TransferTransaction; +use crate::common::types::{STORAGE_KEY_INDEX_TREE, TRANSFER_TYPE}; +use crate::config::SETTINGS; +use crate::records::memory::mempool::BASECOIN; +use crate::records::memory::response_channels::Command; +use crate::rpc::commands::tx_submit::save_and_submit; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::wallets::structures::Wallet; +use crate::{encode, json, to_string, Arc, Mutex, Serialize}; + +#[derive(Serialize)] +struct StorageLookupEntry { + key: String, + value_hex: String, + value_text: Option, + bytes: usize, +} + +#[derive(Serialize)] +struct StorageLookupString { + key: String, + value: String, + chunks: usize, + bytes: usize, +} + +#[derive(Serialize)] +struct StorageLookupPayload { + storage_key: String, + data_key: String, + address: String, + records: Vec, + strings: Vec, + data_bytes: usize, + fee_per_byte: u64, + total_cost: u64, + node_payment_address: String, +} + +fn json_response(value: &T) -> RpcResponse { + match to_string(value) { + Ok(json) => RpcResponse::Binary(json.into_bytes()), + Err(err) => RpcResponse::Binary(format!(r#"{{"error":"{err}"}}"#).into_bytes()), + } +} + +fn error_response(message: &str) -> RpcResponse { + json_response(&json!({ "error": message })) +} + +fn trimmed_key(data_key: &[u8]) -> String { + trim_storage_component(&String::from_utf8_lossy(data_key)) +} + +fn storage_record_prefix(address: &str) -> Result, String> { + Wallet::short_address_to_bytes(address) + .ok_or_else(|| "Invalid storage lookup address.".to_string()) +} + +fn storage_record_key(address: &str, key: &[u8]) -> Result, String> { + let mut record_key = storage_record_prefix(address)?; + record_key.extend_from_slice(key); + Ok(record_key) +} + +fn display_key(prefix_len: usize, key: &[u8]) -> String { + String::from_utf8_lossy(&key[prefix_len..]) + .trim_end_matches(|ch| ch == '\0' || ch == ' ') + .to_string() +} + +fn text_value(value: &[u8]) -> Option { + let text = String::from_utf8(value.to_vec()).ok()?; + if text + .chars() + .any(|ch| ch.is_control() && ch != '\n' && ch != '\r' && ch != '\t') + { + None + } else { + Some(text) + } +} + +fn string_chunk_text(value: &[u8]) -> String { + if value.len() <= STORAGE_STRING_STORED_HASH_BYTES * 2 { + return String::new(); + } + String::from_utf8_lossy(&value[STORAGE_STRING_STORED_HASH_BYTES * 2..]).to_string() +} + +fn storage_key_exists(db: &Db, storage_key: &[u8]) -> Result { + if storage_key.len() != STORAGE_KEY_HASH_BYTES { + return Err("Storage key must be 32 bytes.".to_string()); + } + + let index_tree = db + .open_tree(STORAGE_KEY_INDEX_TREE) + .map_err(|err| format!("Failed to open storage key index: {err}"))?; + index_tree + .contains_key(storage_key) + .map_err(|err| format!("Failed to check storage key index: {err}")) +} + +fn lookup_exact_record( + db: &Db, + storage_key: &[u8], + address: &str, + data_key: &[u8], +) -> Result, String> { + let storage_tree = db + .open_tree(storage_key) + .map_err(|err| format!("Failed to open storage tree: {err}"))?; + let record_key = storage_record_key(address, data_key)?; + let Some(value) = storage_tree + .get(&record_key) + .map_err(|err| format!("Failed to read storage value: {err}"))? + else { + return Ok(Vec::new()); + }; + + Ok(vec![StorageLookupEntry { + key: trimmed_key(data_key), + value_hex: encode(&value), + value_text: text_value(&value), + bytes: value.len(), + }]) +} + +fn lookup_string_records( + db: &Db, + storage_key: &[u8], + address: &str, + base_key: &str, +) -> Result<(Vec, Vec), String> { + if base_key.is_empty() { + return Ok((Vec::new(), Vec::new())); + } + + let storage_tree = db + .open_tree(storage_key) + .map_err(|err| format!("Failed to open storage tree: {err}"))?; + let mut entries = Vec::new(); + let mut assembled = String::new(); + let mut total_bytes = 0usize; + + for index in 0..STORAGE_STRING_CHUNK_LIMIT { + let chunk_key = format!("{base_key}_{index:02}"); + let record_key = storage_record_key(address, chunk_key.as_bytes())?; + let Some(value) = storage_tree + .get(&record_key) + .map_err(|err| format!("Failed to read storage string value: {err}"))? + else { + if index == 0 { + return Ok((Vec::new(), Vec::new())); + } + break; + }; + + let chunk_text = string_chunk_text(&value); + assembled.push_str(&chunk_text); + total_bytes = total_bytes.saturating_add(value.len()); + entries.push(StorageLookupEntry { + key: chunk_key, + value_hex: encode(&value), + value_text: Some(chunk_text), + bytes: value.len(), + }); + } + + let strings = if entries.is_empty() { + Vec::new() + } else { + vec![StorageLookupString { + key: base_key.to_string(), + value: assembled, + chunks: entries.len(), + bytes: total_bytes, + }] + }; + + Ok((entries, strings)) +} + +fn lookup_all_records( + db: &Db, + storage_key: &[u8], + address: &str, +) -> Result<(Vec, Vec), String> { + let storage_tree = db + .open_tree(storage_key) + .map_err(|err| format!("Failed to open storage tree: {err}"))?; + let prefix = storage_record_prefix(address)?; + let prefix_len = prefix.len(); + let mut entries = Vec::new(); + + for item in storage_tree.scan_prefix(&prefix) { + let (key, value) = item.map_err(|err| format!("Failed to scan storage tree: {err}"))?; + entries.push(StorageLookupEntry { + key: display_key(prefix_len, key.as_ref()), + value_hex: encode(&value), + value_text: text_value(&value), + bytes: value.len(), + }); + } + + let mut strings = Vec::new(); + let mut grouped = std::collections::BTreeMap::>::new(); + for entry in &entries { + if let Some(base_key) = entry.key.strip_suffix("_00") { + grouped.entry(base_key.to_string()).or_default(); + } + } + for entry in &entries { + let Some((base_key, suffix)) = entry.key.rsplit_once('_') else { + continue; + }; + if suffix.len() == 2 && suffix.chars().all(|ch| ch.is_ascii_digit()) { + if let Some(chunks) = grouped.get_mut(base_key) { + chunks.push(entry); + } + } + } + for (base_key, mut chunks) in grouped { + chunks.sort_by(|left, right| left.key.cmp(&right.key)); + let mut value = String::new(); + let mut bytes = 0usize; + for chunk in &chunks { + bytes = bytes.saturating_add(chunk.bytes); + if let Ok(raw) = crate::decode(&chunk.value_hex) { + value.push_str(&string_chunk_text(&raw)); + } + } + strings.push(StorageLookupString { + key: base_key, + value, + chunks: chunks.len(), + bytes, + }); + } + + Ok((entries, strings)) +} + +fn lookup_payload( + db: &Db, + storage_key: Vec, + data_key: Vec, + address: String, + wallet: Arc, +) -> Result { + if data_key.len() != STORAGE_VALUE_KEY_BYTES { + return Err("Storage data key must be 50 bytes.".to_string()); + } + if !Wallet::short_address_validation(&address) { + return Err("Invalid lookup wallet address.".to_string()); + } + if !storage_key_exists(db, &storage_key)? { + return Err("Storage key does not exist.".to_string()); + } + + let data_key_text = trimmed_key(&data_key); + let (mut records, strings) = if data_key_text.eq_ignore_ascii_case("all") { + lookup_all_records(db, &storage_key, &address)? + } else { + let mut records = lookup_exact_record(db, &storage_key, &address, &data_key)?; + let (mut string_records, strings) = + lookup_string_records(db, &storage_key, &address, &data_key_text)?; + records.append(&mut string_records); + (records, strings) + }; + + records.sort_by(|left, right| left.key.cmp(&right.key)); + let data_bytes = records.iter().map(|entry| entry.bytes).sum::(); + let fee_per_byte = SETTINGS.storage_lookup_fee_per_byte; + let total_cost = (data_bytes as u64).saturating_mul(fee_per_byte); + + Ok(StorageLookupPayload { + storage_key: encode(storage_key), + data_key: data_key_text, + address, + records, + strings, + data_bytes, + fee_per_byte, + total_cost, + node_payment_address: wallet.saved.short_address.clone(), + }) +} + +pub async fn lookup_cost( + db: &Db, + storage_key: Vec, + data_key: Vec, + address: String, + wallet: Arc, +) -> RpcResponse { + match lookup_payload(db, storage_key, data_key, address, wallet) { + Ok(payload) => json_response(&payload), + Err(err) => error_response(&err), + } +} + +pub async fn lookup_store( + db: &Db, + storage_key: Vec, + data_key: Vec, + address: String, + txtype: u8, + tx: Vec, + wallet: Arc, + map: Arc>, +) -> RpcResponse { + let payload = match lookup_payload(db, storage_key, data_key, address, wallet.clone()) { + Ok(payload) => payload, + Err(err) => return error_response(&err), + }; + + if payload.total_cost == 0 { + return json_response(&json!({ "payment_required": false, "data": payload })); + } + + if txtype != TRANSFER_TYPE { + return error_response("Storage lookup payment must be a transfer transaction."); + } + + let transfer = match TransferTransaction::from_bytes(txtype, &tx).await { + Ok(transfer) => transfer, + Err(err) => return error_response(&format!("Invalid storage lookup payment: {err}")), + }; + + let unsigned = &transfer.unsigned_transfer; + let sender_is_node = unsigned.sender == wallet.saved.short_address; + if sender_is_node { + let hash = unsigned.hash().await; + if Wallet::verify_transaction_with_public_key( + &hash, + &transfer.signature, + &wallet.saved.public_key, + ) + .await + { + return json_response(&json!({ "payment_required": false, "data": payload })); + } + return error_response("Node wallet storage lookup signature is invalid."); + } + + if unsigned.receiver != wallet.saved.short_address { + return error_response("Storage lookup payment receiver must be this node wallet."); + } + if !unsigned.coin.trim().eq_ignore_ascii_case(BASECOIN.trim()) { + return error_response("Storage lookup payment must use the base coin."); + } + if unsigned.value < payload.total_cost { + return error_response("Storage lookup payment value is below the required cost."); + } + + let submit_result = save_and_submit(txtype, tx, db, map).await; + let RpcResponse::Binary(bytes) = submit_result; + let message = String::from_utf8_lossy(&bytes); + if !message.contains("successful_broadcast: true") { + return error_response(&format!( + "Storage lookup payment was not accepted: {message}" + )); + } + + json_response(&json!({ "payment_required": true, "payment_accepted": true, "data": payload })) +} diff --git a/src/rpc/commands/tx_submit.rs b/src/rpc/commands/tx_submit.rs index 49ea940..c98c6ad 100644 --- a/src/rpc/commands/tx_submit.rs +++ b/src/rpc/commands/tx_submit.rs @@ -5,13 +5,19 @@ use crate::blocks::loan_payment::ContractPaymentTransaction; use crate::blocks::loans::LoanContractTransaction; use crate::blocks::marketing::MarketingTransaction; use crate::blocks::nft::CreateNftTransaction; +use crate::blocks::storage_key::StorageKey; +use crate::blocks::storage_values::{ + StorageBool, StorageString, StorageU128, StorageU16, StorageU32, StorageU64, StorageU8, +}; use crate::blocks::swap::SwapTransaction; use crate::blocks::token::CreateTokenTransaction; use crate::blocks::transfer::TransferTransaction; use crate::blocks::vanity::VanityAddressTransaction; use crate::common::types::{ BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, - ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE, + ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, STORAGE_BOOL_TYPE, STORAGE_KEY_TYPE, + STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, STORAGE_U32_TYPE, STORAGE_U64_TYPE, + STORAGE_U8_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE, }; use crate::records::memory::connections::get_client_type_from_memory; use crate::records::memory::enums::ClientType; @@ -59,6 +65,50 @@ async fn broadcast_tx(tx_bytes: Vec, map: Arc>) { } } +macro_rules! submit_storage_value { + ($tx_type:ty, $txtype:expr, $tx:expr, $db:expr, $map:expr) => { + match <$tx_type>::from_bytes($txtype, &$tx).await { + Ok(storage_value) => match storage_value.verify($db).await { + Ok(existing) => { + if !existing.is_empty() { + let msg = "successful_broadcast: false already_in_mempool" + .to_string() + .as_bytes() + .to_vec(); + return RpcResponse::Binary(msg); + } + match storage_value.to_bytes().await { + Ok(storage_value_bytes) => match storage_value.add_to_memory().await { + Ok(_) => { + broadcast_tx(storage_value_bytes, $map.clone()).await; + let msg = + "successful_broadcast: true".to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + Err(err) => { + let msg = format!("error: {err}").to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + }, + Err(err) => { + let msg = format!("error: {err}").to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + } + } + Err(err) => { + let msg = format!("error: {err}").to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + }, + Err(err) => { + let msg = format!("error: {err}").to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + } + }; +} + pub async fn save_and_submit( txtype: u8, tx: Vec, @@ -507,6 +557,52 @@ pub async fn save_and_submit( RpcResponse::Binary(msg) } }, + STORAGE_KEY_TYPE => match StorageKey::from_bytes(txtype, &tx).await { + Ok(storage_key) => match storage_key.verify(db).await { + Ok(existing) => { + if !existing.is_empty() { + let msg = "successful_broadcast: false already_in_mempool" + .to_string() + .as_bytes() + .to_vec(); + return RpcResponse::Binary(msg); + } + match storage_key.to_bytes().await { + Ok(storage_key_bytes) => match storage_key.add_to_memory().await { + Ok(_) => { + broadcast_tx(storage_key_bytes, map.clone()).await; + let msg = + "successful_broadcast: true".to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + Err(err) => { + let msg = format!("error: {err}").to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + }, + Err(err) => { + let msg = format!("error: {err}").to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + } + } + Err(err) => { + let msg = format!("error: {err}").to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + }, + Err(err) => { + let msg = format!("error: {err}").to_string().as_bytes().to_vec(); + RpcResponse::Binary(msg) + } + }, + STORAGE_BOOL_TYPE => submit_storage_value!(StorageBool, txtype, tx, db, map), + STORAGE_U8_TYPE => submit_storage_value!(StorageU8, txtype, tx, db, map), + STORAGE_U16_TYPE => submit_storage_value!(StorageU16, txtype, tx, db, map), + STORAGE_U32_TYPE => submit_storage_value!(StorageU32, txtype, tx, db, map), + STORAGE_U64_TYPE => submit_storage_value!(StorageU64, txtype, tx, db, map), + STORAGE_U128_TYPE => submit_storage_value!(StorageU128, txtype, tx, db, map), + STORAGE_STRING_TYPE => submit_storage_value!(StorageString, txtype, tx, db, map), _ => { let msg = "error: No such transaction type" .to_string() diff --git a/src/rpc/server/rpc_command_loop.rs b/src/rpc/server/rpc_command_loop.rs index 93b90c5..cf2a2be 100644 --- a/src/rpc/server/rpc_command_loop.rs +++ b/src/rpc/server/rpc_command_loop.rs @@ -1005,6 +1005,104 @@ pub async fn start_loop( .send(&stream_locked, Some(&connections_key), uid) .await; } + 52 => { + // request byte count and node fee for storage data lookup + let (uid, _) = read_bytes_from_stream::read_uid_from_stream( + &connections_key, + stream_locked.clone(), + ) + .await?; + let storage_key = read_bytes_from_stream::read_usize_from_stream( + &connections_key, + 32, + stream_locked.clone(), + ) + .await?; + let data_key = read_bytes_from_stream::read_usize_from_stream( + &connections_key, + 50, + stream_locked.clone(), + ) + .await?; + let address = read_bytes_from_stream::read_short_address_string_from_stream( + &connections_key, + stream_locked.clone(), + ) + .await?; + + let result = commands::storage_lookup::lookup_cost( + &db, + storage_key, + data_key, + address, + wallet.clone(), + ) + .await; + result + .send(&stream_locked, Some(&connections_key), uid) + .await; + } + 53 => { + // paid storage data lookup + let (uid, _) = read_bytes_from_stream::read_uid_from_stream( + &connections_key, + stream_locked.clone(), + ) + .await?; + let storage_key = read_bytes_from_stream::read_usize_from_stream( + &connections_key, + 32, + stream_locked.clone(), + ) + .await?; + let data_key = read_bytes_from_stream::read_usize_from_stream( + &connections_key, + 50, + stream_locked.clone(), + ) + .await?; + let address = read_bytes_from_stream::read_short_address_string_from_stream( + &connections_key, + stream_locked.clone(), + ) + .await?; + let txtype = read_bytes_from_stream::read_u8_from_stream( + &connections_key, + stream_locked.clone(), + ) + .await?; + let txsize = command_maps::get_bytes(txtype); + if txsize == 0 { + let result = responses::RpcResponse::Binary( + b"{\"error\":\"No such transaction type\"}".to_vec(), + ); + result + .send(&stream_locked, Some(&connections_key), uid) + .await; + continue; + } + let tx = read_bytes_from_stream::read_usize_from_stream( + &connections_key, + txsize - 1, + stream_locked.clone(), + ) + .await?; + + let result = commands::storage_lookup::lookup_store( + &db, + storage_key, + data_key, + address, + txtype, + tx, + wallet.clone(), + map.clone(), + ) + .await; + result + .send(&stream_locked, Some(&connections_key), uid) + .await; + } 255 => { commands::route_reply::route_reply( &connections_key, diff --git a/src/standalone_tools/transaction_creator.rs b/src/standalone_tools/transaction_creator.rs index bcaa920..c0a88c7 100644 --- a/src/standalone_tools/transaction_creator.rs +++ b/src/standalone_tools/transaction_creator.rs @@ -5,10 +5,20 @@ use crate::blocks::loan_payment::{ContractPaymentTransaction, UnsignedContractPa use crate::blocks::loans::{LoanContractTransaction, UnsignedLoanContractTransaction}; use crate::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction}; use crate::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction}; +use crate::blocks::storage_key::{StorageKey, UnsignedStorageKey}; +use crate::blocks::storage_values::{ + StorageBool, StorageString, StorageU128, StorageU16, StorageU32, StorageU64, StorageU8, + UnsignedStorageBool, UnsignedStorageString, UnsignedStorageU128, UnsignedStorageU16, + UnsignedStorageU32, UnsignedStorageU64, UnsignedStorageU8, +}; use crate::blocks::swap::{SwapTransaction, UnsignedSwapTransaction}; use crate::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction}; use crate::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction}; use crate::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction}; +use crate::common::types::{ + STORAGE_BOOL_TYPE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, STORAGE_U16_TYPE, + STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, +}; use crate::records::memory::response_channels::Byte3; use crate::rpc::command_maps::RPC_SUBMIT_TRANSACTION; use crate::{from_str, Value}; @@ -22,6 +32,76 @@ fn read_u32_field(transaction: &Value, primary: &str, fallback: &str) -> Option< .map(|value| value as u32) } +fn storage_payload<'a>(transaction: &'a Value, field: &str) -> &'a Value { + transaction.get(field).unwrap_or(transaction) +} + +fn read_transaction_type(transaction: &Value) -> Option { + transaction + .get("txtype") + .and_then(|value| value.as_u64()) + .or_else(|| { + transaction + .get("unsigned_storagekey") + .and_then(|value| value.get("txtype")) + .and_then(|value| value.as_u64()) + }) + .or_else(|| { + transaction + .get("unsigned_storagebool") + .and_then(|value| value.get("txtype")) + .and_then(|value| value.as_u64()) + }) + .or_else(|| { + transaction + .get("unsigned_storageu8") + .and_then(|value| value.get("txtype")) + .and_then(|value| value.as_u64()) + }) + .or_else(|| { + transaction + .get("unsigned_storageu16") + .and_then(|value| value.get("txtype")) + .and_then(|value| value.as_u64()) + }) + .or_else(|| { + transaction + .get("unsigned_storageu32") + .and_then(|value| value.get("txtype")) + .and_then(|value| value.as_u64()) + }) + .or_else(|| { + transaction + .get("unsigned_storageu64") + .and_then(|value| value.get("txtype")) + .and_then(|value| value.as_u64()) + }) + .or_else(|| { + transaction + .get("unsigned_storageu128") + .and_then(|value| value.get("txtype")) + .and_then(|value| value.as_u64()) + }) + .or_else(|| { + transaction + .get("unsigned_storage") + .and_then(|value| value.get("txtype")) + .and_then(|value| value.as_u64()) + }) + .map(|value| value as u8) +} + +fn read_u128_field(transaction: &Value, field: &str) -> Option { + let value = transaction.get(field)?; + if let Some(value) = value.as_u64() { + return Some(value as u128); + } + if let Some(value) = value.as_str() { + return value.trim().parse::().ok(); + } + value.to_string().parse::().ok() +} + pub async fn create_transaction_request( command_input: String, hashkey: Byte3, @@ -44,7 +124,7 @@ pub async fn create_transaction_request( } async fn create_transaction_bytes(transaction: &Value) -> Result, String> { - let txtype = transaction["txtype"].as_u64().ok_or("Missing txtype.")? as u8; + let txtype = read_transaction_type(transaction).ok_or("Missing txtype.")?; // Each transaction type is rebuilt through the same unsigned -> signed -> bytes // flow used elsewhere so the standalone tool stays aligned with node serialization. @@ -451,6 +531,276 @@ async fn create_transaction_bytes(transaction: &Value) -> Result, String .await .map_err(|e| format!("Failed to serialize vanity transaction: {e}"))? } + STORAGE_KEY_TYPE => { + let payload = storage_payload(transaction, "unsigned_storagekey"); + let unsigned_storagekey = UnsignedStorageKey::new( + txtype, + read_u32_field(payload, "timestamp", "time").ok_or("Missing timestamp.")?, + payload["address"] + .as_str() + .ok_or("Missing storage key creator wallet address.")?, + payload["txfee"] + .as_u64() + .ok_or("Missing or invalid txfee.")?, + ) + .await; + + StorageKey::load( + unsigned_storagekey, + transaction["signature"] + .as_str() + .ok_or("Missing transaction signature.")?, + ) + .await + .to_bytes() + .await + .map_err(|e| format!("Failed to serialize storage-key transaction: {e}"))? + } + STORAGE_BOOL_TYPE => { + let payload = storage_payload(transaction, "unsigned_storagebool"); + let unsigned_storagebool = UnsignedStorageBool { + txtype, + time: read_u32_field(payload, "timestamp", "time").ok_or("Missing timestamp.")?, + storagekey: payload["storagekey"] + .as_str() + .ok_or("Missing storage key hash.")? + .to_string(), + key: payload["key"] + .as_str() + .ok_or("Missing storage field key.")? + .to_string(), + value: payload["value"].as_bool().ok_or("Missing bool value.")?, + address: payload["address"] + .as_str() + .ok_or("Missing storage writer wallet address.")? + .to_string(), + txfee: payload["txfee"] + .as_u64() + .ok_or("Missing or invalid txfee.")?, + }; + + StorageBool::load( + unsigned_storagebool, + transaction["signature"] + .as_str() + .ok_or("Missing transaction signature.")?, + ) + .await + .to_bytes() + .await + .map_err(|e| format!("Failed to serialize storage-bool transaction: {e}"))? + } + STORAGE_U8_TYPE => { + let payload = storage_payload(transaction, "unsigned_storageu8"); + let unsigned_storageu8 = UnsignedStorageU8 { + txtype, + time: read_u32_field(payload, "timestamp", "time").ok_or("Missing timestamp.")?, + storagekey: payload["storagekey"] + .as_str() + .ok_or("Missing storage key hash.")? + .to_string(), + key: payload["key"] + .as_str() + .ok_or("Missing storage field key.")? + .to_string(), + value: payload["value"].as_u64().ok_or("Missing u8 value.")? as u8, + address: payload["address"] + .as_str() + .ok_or("Missing storage writer wallet address.")? + .to_string(), + txfee: payload["txfee"] + .as_u64() + .ok_or("Missing or invalid txfee.")?, + }; + + StorageU8::load( + unsigned_storageu8, + transaction["signature"] + .as_str() + .ok_or("Missing transaction signature.")?, + ) + .await + .to_bytes() + .await + .map_err(|e| format!("Failed to serialize storage-u8 transaction: {e}"))? + } + STORAGE_U16_TYPE => { + let payload = storage_payload(transaction, "unsigned_storageu16"); + let unsigned_storageu16 = UnsignedStorageU16 { + txtype, + time: read_u32_field(payload, "timestamp", "time").ok_or("Missing timestamp.")?, + storagekey: payload["storagekey"] + .as_str() + .ok_or("Missing storage key hash.")? + .to_string(), + key: payload["key"] + .as_str() + .ok_or("Missing storage field key.")? + .to_string(), + value: payload["value"].as_u64().ok_or("Missing u16 value.")? as u16, + address: payload["address"] + .as_str() + .ok_or("Missing storage writer wallet address.")? + .to_string(), + txfee: payload["txfee"] + .as_u64() + .ok_or("Missing or invalid txfee.")?, + }; + + StorageU16::load( + unsigned_storageu16, + transaction["signature"] + .as_str() + .ok_or("Missing transaction signature.")?, + ) + .await + .to_bytes() + .await + .map_err(|e| format!("Failed to serialize storage-u16 transaction: {e}"))? + } + STORAGE_U32_TYPE => { + let payload = storage_payload(transaction, "unsigned_storageu32"); + let unsigned_storageu32 = UnsignedStorageU32 { + txtype, + time: read_u32_field(payload, "timestamp", "time").ok_or("Missing timestamp.")?, + storagekey: payload["storagekey"] + .as_str() + .ok_or("Missing storage key hash.")? + .to_string(), + key: payload["key"] + .as_str() + .ok_or("Missing storage field key.")? + .to_string(), + value: payload["value"].as_u64().ok_or("Missing u32 value.")? as u32, + address: payload["address"] + .as_str() + .ok_or("Missing storage writer wallet address.")? + .to_string(), + txfee: payload["txfee"] + .as_u64() + .ok_or("Missing or invalid txfee.")?, + }; + + StorageU32::load( + unsigned_storageu32, + transaction["signature"] + .as_str() + .ok_or("Missing transaction signature.")?, + ) + .await + .to_bytes() + .await + .map_err(|e| format!("Failed to serialize storage-u32 transaction: {e}"))? + } + STORAGE_U64_TYPE => { + let payload = storage_payload(transaction, "unsigned_storageu64"); + let unsigned_storageu64 = UnsignedStorageU64 { + txtype, + time: read_u32_field(payload, "timestamp", "time").ok_or("Missing timestamp.")?, + storagekey: payload["storagekey"] + .as_str() + .ok_or("Missing storage key hash.")? + .to_string(), + key: payload["key"] + .as_str() + .ok_or("Missing storage field key.")? + .to_string(), + value: payload["value"].as_u64().ok_or("Missing u64 value.")?, + address: payload["address"] + .as_str() + .ok_or("Missing storage writer wallet address.")? + .to_string(), + txfee: payload["txfee"] + .as_u64() + .ok_or("Missing or invalid txfee.")?, + }; + + StorageU64::load( + unsigned_storageu64, + transaction["signature"] + .as_str() + .ok_or("Missing transaction signature.")?, + ) + .await + .to_bytes() + .await + .map_err(|e| format!("Failed to serialize storage-u64 transaction: {e}"))? + } + STORAGE_U128_TYPE => { + let payload = storage_payload(transaction, "unsigned_storageu128"); + let unsigned_storageu128 = UnsignedStorageU128 { + txtype, + time: read_u32_field(payload, "timestamp", "time").ok_or("Missing timestamp.")?, + storagekey: payload["storagekey"] + .as_str() + .ok_or("Missing storage key hash.")? + .to_string(), + key: payload["key"] + .as_str() + .ok_or("Missing storage field key.")? + .to_string(), + value: read_u128_field(payload, "value").ok_or("Missing u128 value.")?, + address: payload["address"] + .as_str() + .ok_or("Missing storage writer wallet address.")? + .to_string(), + txfee: payload["txfee"] + .as_u64() + .ok_or("Missing or invalid txfee.")?, + }; + + StorageU128::load( + unsigned_storageu128, + transaction["signature"] + .as_str() + .ok_or("Missing transaction signature.")?, + ) + .await + .to_bytes() + .await + .map_err(|e| format!("Failed to serialize storage-u128 transaction: {e}"))? + } + STORAGE_STRING_TYPE => { + let payload = storage_payload(transaction, "unsigned_storage"); + let unsigned_storage = UnsignedStorageString { + txtype, + time: read_u32_field(payload, "timestamp", "time").ok_or("Missing timestamp.")?, + storagekey: payload["storagekey"] + .as_str() + .ok_or("Missing storage key hash.")? + .to_string(), + key: payload["key"] + .as_str() + .ok_or("Missing storage field key.")? + .to_string(), + value: payload["value"] + .as_str() + .ok_or("Missing string value.")? + .to_string(), + address: payload["address"] + .as_str() + .ok_or("Missing storage writer wallet address.")? + .to_string(), + previous: payload["previous"] + .as_str() + .ok_or("Missing previous string hash.")? + .to_string(), + txfee: payload["txfee"] + .as_u64() + .ok_or("Missing or invalid txfee.")?, + }; + + StorageString::load( + unsigned_storage, + transaction["signature"] + .as_str() + .ok_or("Missing transaction signature.")?, + ) + .await + .to_bytes() + .await + .map_err(|e| format!("Failed to serialize storage-string transaction: {e}"))? + } _ => return Err("No such transaction type".to_string()), }; diff --git a/src/startup/connections.rs b/src/startup/connections.rs index e087778..7bdd0ca 100644 --- a/src/startup/connections.rs +++ b/src/startup/connections.rs @@ -90,10 +90,21 @@ async fn attempt_bootstrap_connections( Err(err) => err.to_string(), }; - // A peer can reject us because it already has this connection recorded. - // In that case retrying other bootstrap peers would not fix the local duplicate state. - if err_string.contains("The connection is already in the connection manager Please wait 10 minutes and try again") { - return Err(err_string); + // During outage recovery, a peer may already have the connection + // recorded from an overlapping reconnect path. That is not a fatal + // bootstrap-recovery condition; try the next configured peer. + if err_string.contains( + "The connection is already in the connection manager Please wait 10 minutes and try again", + ) { + if context == "startup" { + error!("Error connecting to {server}: {err_string}"); + } else { + warn!( + "[reconnect] bootstrap recovery skipped duplicate peer {server}: {err_string}" + ); + } + last_error = Some(err_string.clone()); + continue; } if context == "startup" { error!("Error connecting to {server}: {err_string}"); diff --git a/src/verifications/async_funcs/checks/block_balance.rs b/src/verifications/async_funcs/checks/block_balance.rs index abf3f7f..d211c08 100644 --- a/src/verifications/async_funcs/checks/block_balance.rs +++ b/src/verifications/async_funcs/checks/block_balance.rs @@ -439,6 +439,136 @@ async fn transaction_balance_view( check_saved_txid: true, }) } + Transaction::StorageKey(tx) => { + let storage_key = &tx.unsigned_storagekey; + let mut debits = Vec::new(); + + add_debit( + &mut debits, + &storage_key.address, + BASECOIN.clone(), + storage_key.txfee, + ); + + Ok(TransactionBalanceView { + hash: storage_key.hash().await, + signatures: vec![tx.signature.clone()], + debits, + check_saved_txid: true, + }) + } + Transaction::StorageBool(tx) => { + let storage = &tx.unsigned_storagebool; + let mut debits = Vec::new(); + add_debit( + &mut debits, + &storage.address, + BASECOIN.clone(), + storage.txfee, + ); + Ok(TransactionBalanceView { + hash: storage.hash().await, + signatures: vec![tx.signature.clone()], + debits, + check_saved_txid: true, + }) + } + Transaction::StorageU8(tx) => { + let storage = &tx.unsigned_storageu8; + let mut debits = Vec::new(); + add_debit( + &mut debits, + &storage.address, + BASECOIN.clone(), + storage.txfee, + ); + Ok(TransactionBalanceView { + hash: storage.hash().await, + signatures: vec![tx.signature.clone()], + debits, + check_saved_txid: true, + }) + } + Transaction::StorageU16(tx) => { + let storage = &tx.unsigned_storageu16; + let mut debits = Vec::new(); + add_debit( + &mut debits, + &storage.address, + BASECOIN.clone(), + storage.txfee, + ); + Ok(TransactionBalanceView { + hash: storage.hash().await, + signatures: vec![tx.signature.clone()], + debits, + check_saved_txid: true, + }) + } + Transaction::StorageU32(tx) => { + let storage = &tx.unsigned_storageu32; + let mut debits = Vec::new(); + add_debit( + &mut debits, + &storage.address, + BASECOIN.clone(), + storage.txfee, + ); + Ok(TransactionBalanceView { + hash: storage.hash().await, + signatures: vec![tx.signature.clone()], + debits, + check_saved_txid: true, + }) + } + Transaction::StorageU64(tx) => { + let storage = &tx.unsigned_storageu64; + let mut debits = Vec::new(); + add_debit( + &mut debits, + &storage.address, + BASECOIN.clone(), + storage.txfee, + ); + Ok(TransactionBalanceView { + hash: storage.hash().await, + signatures: vec![tx.signature.clone()], + debits, + check_saved_txid: true, + }) + } + Transaction::StorageU128(tx) => { + let storage = &tx.unsigned_storageu128; + let mut debits = Vec::new(); + add_debit( + &mut debits, + &storage.address, + BASECOIN.clone(), + storage.txfee, + ); + Ok(TransactionBalanceView { + hash: storage.hash().await, + signatures: vec![tx.signature.clone()], + debits, + check_saved_txid: true, + }) + } + Transaction::StorageString(tx) => { + let storage = &tx.unsigned_storage; + let mut debits = Vec::new(); + add_debit( + &mut debits, + &storage.address, + BASECOIN.clone(), + storage.txfee, + ); + Ok(TransactionBalanceView { + hash: storage.hash().await, + signatures: vec![tx.signature.clone()], + debits, + check_saved_txid: true, + }) + } } } diff --git a/src/verifications/async_funcs/mod.rs b/src/verifications/async_funcs/mod.rs index d09645f..44c785f 100644 --- a/src/verifications/async_funcs/mod.rs +++ b/src/verifications/async_funcs/mod.rs @@ -16,6 +16,8 @@ pub mod verify_issue_token; pub mod verify_lender; pub mod verify_marketing; pub mod verify_rewards; +pub mod verify_storage_key; +pub mod verify_storage_values; pub mod verify_swap; pub mod verify_transfer; pub mod verify_vanity; diff --git a/src/verifications/async_funcs/transactions.rs b/src/verifications/async_funcs/transactions.rs index 118338a..03bfade 100644 --- a/src/verifications/async_funcs/transactions.rs +++ b/src/verifications/async_funcs/transactions.rs @@ -251,6 +251,110 @@ async fn verify_transaction( } } } + if let Transaction::StorageKey(storage_key_tx) = &transaction { + match storage_key_tx.verify(db).await { + Ok(value) => { + reserve_verified_transaction(db, &transaction, balance_tracker).await?; + return Ok(value); + } + Err(err) => { + return Err(format!( + "Validation failed for storage key transaction: {err}" + )); + } + } + } + if let Transaction::StorageBool(storage_tx) = &transaction { + match storage_tx.verify(db).await { + Ok(value) => { + reserve_verified_transaction(db, &transaction, balance_tracker).await?; + return Ok(value); + } + Err(err) => { + return Err(format!( + "Validation failed for storage bool transaction: {err}" + )); + } + } + } + if let Transaction::StorageU8(storage_tx) = &transaction { + match storage_tx.verify(db).await { + Ok(value) => { + reserve_verified_transaction(db, &transaction, balance_tracker).await?; + return Ok(value); + } + Err(err) => { + return Err(format!( + "Validation failed for storage u8 transaction: {err}" + )); + } + } + } + if let Transaction::StorageU16(storage_tx) = &transaction { + match storage_tx.verify(db).await { + Ok(value) => { + reserve_verified_transaction(db, &transaction, balance_tracker).await?; + return Ok(value); + } + Err(err) => { + return Err(format!( + "Validation failed for storage u16 transaction: {err}" + )); + } + } + } + if let Transaction::StorageU32(storage_tx) = &transaction { + match storage_tx.verify(db).await { + Ok(value) => { + reserve_verified_transaction(db, &transaction, balance_tracker).await?; + return Ok(value); + } + Err(err) => { + return Err(format!( + "Validation failed for storage u32 transaction: {err}" + )); + } + } + } + if let Transaction::StorageU64(storage_tx) = &transaction { + match storage_tx.verify(db).await { + Ok(value) => { + reserve_verified_transaction(db, &transaction, balance_tracker).await?; + return Ok(value); + } + Err(err) => { + return Err(format!( + "Validation failed for storage u64 transaction: {err}" + )); + } + } + } + if let Transaction::StorageU128(storage_tx) = &transaction { + match storage_tx.verify(db).await { + Ok(value) => { + reserve_verified_transaction(db, &transaction, balance_tracker).await?; + return Ok(value); + } + Err(err) => { + return Err(format!( + "Validation failed for storage u128 transaction: {err}" + )); + } + } + } + if let Transaction::StorageString(storage_tx) = &transaction { + match storage_tx.verify(db).await { + Ok(value) => { + reserve_verified_transaction(db, &transaction, balance_tracker).await?; + return Ok(value); + } + Err(err) => { + return Err(format!( + "Validation failed for storage string transaction: {err}" + )); + } + } + } Err( "Validation failed: unsupported transaction variant reached verification dispatch." .to_string(), diff --git a/src/verifications/async_funcs/verify_storage_key.rs b/src/verifications/async_funcs/verify_storage_key.rs new file mode 100644 index 0000000..4ada8ee --- /dev/null +++ b/src/verifications/async_funcs/verify_storage_key.rs @@ -0,0 +1,77 @@ +use crate::blocks::storage_key::StorageKey; +use crate::common::types::{STORAGE_KEY_FEE, STORAGE_KEY_TYPE}; +use crate::encode; +use crate::records::memory::mempool::BASECOIN; +use crate::records::wallet_registry::{ + require_canonical_registered_short_address, resolve_pubkey_from_short_address, +}; +use crate::sled::Db; +use crate::verifications::async_funcs::checks::balance_check::balance_checkup; +use crate::verifications::async_funcs::checks::mempool_check::memcheck; +use crate::verifications::async_funcs::checks::verify_db::db_hex_verification; +use crate::wallets::structures::Wallet; + +impl StorageKey { + pub async fn verify(&self, db: &Db) -> Result { + // Storage key transactions create a new app-scoped sled tree. + // The transaction hash becomes the tree name, so the unsigned + // payload must use the storage-key transaction type. + let storage_key = &self.unsigned_storagekey; + if storage_key.txtype != STORAGE_KEY_TYPE { + return Err("Storage key transaction type is invalid.".to_string()); + } + + // Transactions already present in the mempool can short-circuit + // deeper verification and return their stored signature. + let hash = storage_key.hash().await; + if memcheck(&self.signature, &hash).await { + return Ok(self.signature.clone()); + } + + // Only canonical registered short addresses can create storage + // keys. Vanity aliases are resolved by callers before they reach + // this fixed transaction format. + let address = &storage_key.address; + if !Wallet::short_address_validation(address) { + return Err("Storage key wallet address is invalid.".to_string()); + } + require_canonical_registered_short_address(db, address, "Storage key wallet address")?; + + // The registered public key for the creating wallet is the + // authority for the storage-key signature. + let sender_pubkey = resolve_pubkey_from_short_address(db, address) + .map_err(|_| "Storage key wallet address is not registered.".to_string())? + .ok_or_else(|| "Storage key wallet address is not registered.".to_string())?; + let sender_pubkey_hex = encode(&sender_pubkey); + if !Wallet::verify_transaction_with_public_key(&hash, &self.signature, &sender_pubkey_hex) + .await + { + return Err("Invalid signature for the Storage Key Transaction.".to_string()); + } + + // Storage keys are intentionally expensive because they create a + // new app namespace in the chain's local record store. + let txfee = storage_key.txfee; + if txfee < STORAGE_KEY_FEE { + return Err(format!( + "Storage key transaction fee is below the minimum required fee of {STORAGE_KEY_FEE}." + )); + } + + // Creating the storage namespace is fee-only, so the sender needs + // enough base coin to cover the storage-key fee. + if !balance_checkup(db, 0, txfee, BASECOIN.clone(), address).await { + return Err("Insuficient funds for this Storage Key Transaction!".to_string()); + } + + // Saved-chain duplicates are rejected by txid even if the mempool + // did not already contain the transaction. + if !db_hex_verification(db, "txid", &hash).await { + return Err("This transaction already exists.".to_string()); + } + + // Verification returns no auxiliary cleanup marker for this + // transaction type. + Ok(String::new()) + } +} diff --git a/src/verifications/async_funcs/verify_storage_values.rs b/src/verifications/async_funcs/verify_storage_values.rs new file mode 100644 index 0000000..0999d02 --- /dev/null +++ b/src/verifications/async_funcs/verify_storage_values.rs @@ -0,0 +1,197 @@ +use crate::blocks::storage_values::{ + storage_string_record_key, StorageBool, StorageString, StorageU128, StorageU16, StorageU32, + StorageU64, StorageU8, StorageValueTransaction, STORAGE_KEY_HASH_BYTES, + STORAGE_STRING_VALUE_BYTES, STORAGE_VALUE_KEY_BYTES, +}; +use crate::common::types::{ + STORAGE_BOOL_TYPE, STORAGE_KEY_INDEX_TREE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE, + STORAGE_U16_TYPE, STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, STORAGE_VALUE_FEE, +}; +use crate::decode; +use crate::encode; +use crate::records::memory::mempool::BASECOIN; +use crate::records::wallet_registry::{ + require_canonical_registered_short_address, resolve_pubkey_from_short_address, +}; +use crate::sled::Db; +use crate::verifications::async_funcs::checks::balance_check::balance_checkup; +use crate::verifications::async_funcs::checks::mempool_check::memcheck; +use crate::verifications::async_funcs::checks::verify_db::db_hex_verification; +use crate::wallets::structures::Wallet; + +async fn verify_storage_value( + tx: &T, + db: &Db, + expected_type: u8, + label: &str, +) -> Result { + // All concrete storage-value transactions share these fields: + // transaction type, storage namespace, field key, wallet address, + // fee, hash, and signature. + let parts = tx.storage_parts(); + + // Match the rest of transaction verification: if this exact signed + // transaction is already in the mempool, return before doing deeper + // database checks. + if memcheck(parts.signature, &parts.hash).await { + return Ok(parts.signature.to_string()); + } + + // The shared verifier is called by each concrete storage type, so + // confirm the embedded transaction type matches that caller. + if parts.txtype != expected_type { + return Err(format!("{label} transaction type is invalid.")); + } + + // The storage key is the 32-byte hash of a confirmed StorageKey + // transaction. It is decoded here before checking the storage-key index. + let storagekey_bytes = decode(parts.storagekey) + .map_err(|err| format!("{label} storage key hash is invalid: {err}"))?; + if storagekey_bytes.len() != STORAGE_KEY_HASH_BYTES { + return Err(format!("{label} storage key hash length is invalid.")); + } + + // Storage field keys are fixed-width so all value transactions have a + // deterministic byte layout. String storage may later map this logical + // key to key_00 through key_19 in sled. + if parts.key.as_bytes().len() != STORAGE_VALUE_KEY_BYTES { + return Err(format!("{label} storage field key length is invalid.")); + } + + // Storage values may only write into a namespace created by a prior + // StorageKey transaction. Checking the index avoids accidentally + // creating a dynamic sled tree during verification. + let storage_index = db + .open_tree(STORAGE_KEY_INDEX_TREE) + .map_err(|err| format!("Could not open storage key index: {err}"))?; + if !storage_index + .contains_key(&storagekey_bytes) + .map_err(|err| format!("Could not check storage key index: {err}"))? + { + return Err(format!("{label} storage key does not exist.")); + } + + // Storage writes are owned by the wallet address in the transaction, + // and must use the canonical registered short address form. + if !Wallet::short_address_validation(parts.address) { + return Err(format!("{label} wallet address is invalid.")); + } + require_canonical_registered_short_address(db, parts.address, "Storage value wallet address")?; + + // The registered public key for the writing wallet must verify the + // signature over the exact unsigned storage-value payload. + let sender_pubkey = resolve_pubkey_from_short_address(db, parts.address) + .map_err(|_| format!("{label} wallet address is not registered."))? + .ok_or_else(|| format!("{label} wallet address is not registered."))?; + let sender_pubkey_hex = encode(&sender_pubkey); + if !Wallet::verify_transaction_with_public_key(&parts.hash, parts.signature, &sender_pubkey_hex) + .await + { + return Err(format!("Invalid signature for the {label} Transaction.")); + } + + // Every storage value write pays the fixed storage-value fee. The fee is + // independent of the stored type so bool/u8/string all share one rule. + if parts.txfee < STORAGE_VALUE_FEE { + return Err(format!( + "{label} transaction fee is below the minimum required fee of {STORAGE_VALUE_FEE}." + )); + } + + // Storage writes do not transfer an asset value, but the sender must + // have enough base coin to cover the storage-value fee. + if !balance_checkup(db, 0, parts.txfee, BASECOIN.clone(), parts.address).await { + return Err(format!("Insuficient funds for this {label} Transaction!")); + } + + // Saved-chain duplicates are rejected by txid even if the mempool did + // not already contain the transaction. + if !db_hex_verification(db, "txid", &parts.hash).await { + return Err("This transaction already exists.".to_string()); + } + + // Verification returns no auxiliary cleanup marker for these + // transaction types. + Ok(String::new()) +} + +impl StorageBool { + pub async fn verify(&self, db: &Db) -> Result { + verify_storage_value(self, db, STORAGE_BOOL_TYPE, "Storage Bool").await + } +} + +impl StorageU8 { + pub async fn verify(&self, db: &Db) -> Result { + verify_storage_value(self, db, STORAGE_U8_TYPE, "Storage U8").await + } +} + +impl StorageU16 { + pub async fn verify(&self, db: &Db) -> Result { + verify_storage_value(self, db, STORAGE_U16_TYPE, "Storage U16").await + } +} + +impl StorageU32 { + pub async fn verify(&self, db: &Db) -> Result { + verify_storage_value(self, db, STORAGE_U32_TYPE, "Storage U32").await + } +} + +impl StorageU64 { + pub async fn verify(&self, db: &Db) -> Result { + verify_storage_value(self, db, STORAGE_U64_TYPE, "Storage U64").await + } +} + +impl StorageU128 { + pub async fn verify(&self, db: &Db) -> Result { + verify_storage_value(self, db, STORAGE_U128_TYPE, "Storage U128").await + } +} + +impl StorageString { + pub async fn verify(&self, db: &Db) -> Result { + // String storage has extra chain-linking rules before it can use + // the common storage-value verifier. + let parts = self.storage_parts(); + + // Existing mempool entries can still short-circuit before walking + // the previous-hash string chain. + if memcheck(parts.signature, &parts.hash).await { + return Ok(parts.signature.to_string()); + } + + // The on-chain string chunk is fixed at 180 bytes. Shorter text is + // padded by the creator and trimmed only when saved into sled. + if self.unsigned_storage.value.as_bytes().len() != STORAGE_STRING_VALUE_BYTES { + return Err("Storage String value length is invalid.".to_string()); + } + + // A zero previous hash starts a string at key_00. Any non-zero + // previous hash must point to an existing StorageString transaction. + let previous_bytes = decode(&self.unsigned_storage.previous) + .map_err(|err| format!("Storage String previous hash is invalid: {err}"))?; + if previous_bytes.len() != STORAGE_KEY_HASH_BYTES { + return Err("Storage String previous hash length is invalid.".to_string()); + } + + // Non-zero previous links must already exist on-chain so a string + // chain cannot point to missing history. + if previous_bytes.iter().any(|byte| *byte != 0) + && db_hex_verification(db, "txid", &self.unsigned_storage.previous).await + { + return Err("Storage String previous transaction does not exist.".to_string()); + } + + // This validates the previous chain belongs to the same storage + // namespace, same wallet, and same logical key, then derives the + // concrete sled key such as key_00, key_01, ... key_19. + storage_string_record_key(db, self).await?; + + // After string-specific checks pass, reuse the same ownership, + // signature, fee, balance, and duplicate checks as other values. + verify_storage_value(self, db, STORAGE_STRING_TYPE, "Storage String").await + } +}