added storage transactions

This commit is contained in:
viraladmin 2026-07-11 10:28:11 -06:00
parent bb6862658f
commit 5c68786eef
36 changed files with 3418 additions and 237 deletions

537
src/bin/data_storage_tx.rs Normal file
View File

@ -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 <storage_key_hash> <field_key> <true|false> [txfee]");
println!(" ./data_storage_tx u8|8bit <storage_key_hash> <field_key> <value> [txfee]");
println!(" ./data_storage_tx u16|16bit <storage_key_hash> <field_key> <value> [txfee]");
println!(" ./data_storage_tx u32|32bit <storage_key_hash> <field_key> <value> [txfee]");
println!(" ./data_storage_tx u64|64bit <storage_key_hash> <field_key> <value> [txfee]");
println!(" ./data_storage_tx u128|128bit <storage_key_hash> <field_key> <value> [txfee]");
println!(
" ./data_storage_tx string <storage_key_hash> <field_key> <value> [previous_hash] [txfee]"
);
}
fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
fn parse_fee(value: &str) -> Option<u64> {
let parsed = value.trim().parse::<f64>().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<String> {
let trimmed = input.trim();
if trimmed.is_empty() || trimmed.as_bytes().len() > STORAGE_VALUE_KEY_BYTES {
return None;
}
Some(format!("{trimmed:<STORAGE_VALUE_KEY_BYTES$}"))
}
fn fixed_string_value(input: &str) -> Option<String> {
if input.as_bytes().len() > STORAGE_STRING_VALUE_BYTES {
return None;
}
Some(format!("{input:<STORAGE_STRING_VALUE_BYTES$}"))
}
fn valid_storage_key_hash(value: &str) -> 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<String> {
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<String> {
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<Wallet> {
let wallet_path = prompt_wallet_path().await;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
Ok(wallet) => Some(wallet),
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
None
}
}
}
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<String> = 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(),
}
}

View File

@ -343,6 +343,38 @@ impl Block {
let tx_bytes = vanity_tx.to_bytes().await?; let tx_bytes = vanity_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes); 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) Ok(buffer)

View File

@ -8,6 +8,8 @@ pub mod loans;
pub mod marketing; pub mod marketing;
pub mod nft; pub mod nft;
pub mod rewards; pub mod rewards;
pub mod storage_key;
pub mod storage_values;
pub mod swap; pub mod swap;
pub mod token; pub mod token;
pub mod transfer; pub mod transfer;

View File

@ -7,6 +7,10 @@ use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::rewards::RewardsTransaction; 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::swap::SwapTransaction;
use crate::blocks::token::CreateTokenTransaction; use crate::blocks::token::CreateTokenTransaction;
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
@ -28,6 +32,14 @@ pub const COLLATERAL_TYPE: u8 = 9;
pub const BURN_TYPE: u8 = 10; pub const BURN_TYPE: u8 = 10;
pub const ISSUE_TOKEN_TYPE: u8 = 11; pub const ISSUE_TOKEN_TYPE: u8 = 11;
pub const VANITY_ADDRESS_TYPE: u8 = 12; 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. // Coin and asset names are stored in fixed 15-byte fields.
pub const COIN_LENGTH: usize = 15; 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 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 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 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 { pub fn minimum_transfer_fee(value: u64, is_base_coin: bool) -> u64 {
// Base-coin transfers pay the percentage fee using integer math. // Base-coin transfers pay the percentage fee using integer math.
@ -92,4 +108,12 @@ pub enum Transaction {
Borrower(ContractPaymentTransaction), Borrower(ContractPaymentTransaction),
Collateral(CollateralClaimTransaction), Collateral(CollateralClaimTransaction),
Vanity(VanityAddressTransaction), Vanity(VanityAddressTransaction),
StorageKey(StorageKey),
StorageBool(StorageBool),
StorageU8(StorageU8),
StorageU16(StorageU16),
StorageU32(StorageU32),
StorageU64(StorageU64),
StorageU128(StorageU128),
StorageString(StorageString),
} }

View File

@ -32,6 +32,7 @@ pub struct Settings {
pub pg_user: String, pub pg_user: String,
pub pg_password: Option<String>, pub pg_password: Option<String>,
pub pg_dbname: String, pub pg_dbname: String,
pub storage_lookup_fee_per_byte: u64,
} }
impl Settings { impl Settings {
@ -197,6 +198,10 @@ impl Settings {
.ok_or("INCOMING_CONNECTIONS not found")? .ok_or("INCOMING_CONNECTIONS not found")?
.parse::<u8>()?, .parse::<u8>()?,
threads, threads,
storage_lookup_fee_per_byte: conf
.get_from(Some("Settings"), "STORAGE_LOOKUP_FEE_PER_BYTE")
.unwrap_or("0")
.parse::<u64>()?,
pg_host: pg_section.get("host").unwrap_or("127.0.0.1").to_string(), pg_host: pg_section.get("host").unwrap_or("127.0.0.1").to_string(),
pg_port: pg_section.get("port").unwrap_or("5432").parse::<u16>()?, pg_port: pg_section.get("port").unwrap_or("5432").parse::<u16>()?,
pg_user: pg_section pg_user: pg_section

View File

@ -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_loan_creation::undo_loan_creation_transaction;
use crate::orphans::undo_transactions::undo_marketing::undo_marketing_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_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_swap::undo_swap_transaction;
use crate::orphans::undo_transactions::undo_transfer::undo_transfer_transaction; use crate::orphans::undo_transactions::undo_transfer::undo_transfer_transaction;
use crate::orphans::undo_transactions::undo_vanity::undo_vanity_transaction; use crate::orphans::undo_transactions::undo_vanity::undo_vanity_transaction;
@ -130,6 +132,30 @@ pub async fn undo_transactions(
.await?; .await?;
rolled_back_transactions.push(Transaction::Vanity(vanity_tx)); rolled_back_transactions.push(Transaction::Vanity(vanity_tx));
} }
Transaction::StorageKey(storage_key_tx) => {
undo_storage_key_transaction(
storage_key_tx.clone(),
&mining_receiver,
&params.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,
&params.db,
)
.await?;
rolled_back_transactions.push(transaction);
}
} }
} }
// The block miner loses one mined-block count when this block is removed. // The block miner loses one mined-block count when this block is removed.

View File

@ -8,6 +8,8 @@ pub mod undo_issue_token;
pub mod undo_loan_creation; pub mod undo_loan_creation;
pub mod undo_marketing; pub mod undo_marketing;
pub mod undo_rewards; pub mod undo_rewards;
pub mod undo_storage_key;
pub mod undo_storage_value;
pub mod undo_swap; pub mod undo_swap;
pub mod undo_transfer; pub mod undo_transfer;
pub mod undo_vanity; pub mod undo_vanity;

View File

@ -5,6 +5,8 @@ use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; 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::swap::SwapTransaction;
use crate::blocks::token::CreateTokenTransaction; use crate::blocks::token::CreateTokenTransaction;
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
@ -307,6 +309,43 @@ pub async fn restore_vanity(transaction: &VanityAddressTransaction, db: &Db) {
.await; .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<T, F, Fut>(tx_label: &str, transaction: &T, db: &Db, insert: F)
where
T: StorageValueTransaction,
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>>,
{
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) { pub async fn restore_rolled_back_transaction(transaction: &Transaction, db: &Db) {
match transaction { match transaction {
Transaction::Transfer(tx) => restore_transfer(tx, db).await, 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::Borrower(tx) => restore_borrower(tx, db).await,
Transaction::Collateral(tx) => restore_collateral(tx, db).await, Transaction::Collateral(tx) => restore_collateral(tx, db).await,
Transaction::Vanity(tx) => restore_vanity(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(_) => {} Transaction::Genesis(_) | Transaction::Rewards(_) => {}
} }
} }

View File

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

View File

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

View File

@ -186,10 +186,10 @@ async fn retry_dropped_outgoing(ip: String, port: u16) {
if err_string.contains( if err_string.contains(
"The connection is already in the connection manager Please wait 10 minutes and try again", "The connection is already in the connection manager Please wait 10 minutes and try again",
) { ) {
info!( warn!(
"[reconnect] dropped peer {addr_string} is already in the connection manager; stopping reconnect attempts" "[reconnect] dropped peer {addr_string} is still recorded by remote connection manager on attempt {attempt}/3; retrying"
); );
return; continue;
} }
warn!( warn!(
"[reconnect] failed to reconnect dropped peer {addr_string} on attempt {attempt}/3: {err_string}" "[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 && connection_key.port == port
&& ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner)
{ {
Some( Some(info.ready || (info.wallet_registry_synced && info.network_map_synced))
info.ready
|| (info.wallet_registry_synced && info.network_map_synced),
)
} else { } else {
None None
} }

View File

@ -24,6 +24,8 @@ pub async fn signature_exists(signature: &str, hash: &str) -> Result<bool> {
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_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 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 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 THEN 1
ELSE 0 ELSE 0
END AS signature_found; 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 SELECT original FROM loan_payment WHERE signature = $1 AND processed = false LIMIT 1
UNION ALL UNION ALL
SELECT original FROM collateral_claim WHERE signature = $1 AND processed = false LIMIT 1 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 ) AS subquery LIMIT 1
"#, "#,
&[&signature], &[&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 SELECT original FROM loan_payment WHERE address = ANY($1) AND processed = false
UNION ALL UNION ALL
SELECT original FROM collateral_claim WHERE address = ANY($1) AND processed = false 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; ) AS subquery;
"#, "#,
&[&addresses], &[&addresses],
@ -198,6 +208,12 @@ pub async fn latest_pending_txids_by_address(db: &Db, address: &str, limit: usiz
UNION ALL UNION ALL
SELECT hash, time, id AS source_id FROM collateral_claim SELECT hash, time, id AS source_id FROM collateral_claim
WHERE address = ANY($1) AND processed = false 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 ) AS pending
ORDER BY hash, time DESC, source_id DESC ORDER BY hash, time DESC, source_id DESC
) AS deduped ) AS deduped
@ -256,6 +272,10 @@ pub async fn pending_transaction_by_txid(txid: &[u8]) -> Option<Vec<u8>> {
SELECT original FROM loan_payment WHERE hash = $1 AND processed = false SELECT original FROM loan_payment WHERE hash = $1 AND processed = false
UNION ALL UNION ALL
SELECT original FROM collateral_claim WHERE hash = $1 AND processed = false 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 ) AS subquery LIMIT 1
"#, "#,
&[&hash], &[&hash],
@ -301,6 +321,10 @@ pub async fn largest_fee() -> RpcResponse {
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_payment SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM loan_payment
UNION ALL UNION ALL
SELECT CAST(MAX(fee) AS BIGINT) AS fee FROM collateral_claim 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; ) 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(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(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(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(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(lp.fee) FROM loan_payment lp WHERE lp.address = ANY($1) AND lp.processed = false), 0)
+ COALESCE((SELECT SUM(lc.loan_amount) + COALESCE((SELECT SUM(lc.loan_amount)
@ -575,6 +601,10 @@ pub async fn total_transactions() -> RpcResponse {
SELECT COUNT(*) AS row_count FROM loan_payment SELECT COUNT(*) AS row_count FROM loan_payment
UNION ALL UNION ALL
SELECT COUNT(*) AS row_count FROM collateral_claim 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; ) AS combined;
"#, "#,
&[], &[],

View File

@ -152,6 +152,19 @@ enum SelectedMempoolTransaction {
contract_hash: String, contract_hash: String,
hash: String, hash: String,
}, },
StorageKey {
id: i64,
fee: i64,
address: String,
hash: String,
},
StorageValue {
id: i64,
fee: i64,
address: String,
hash: String,
original: Vec<u8>,
},
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -176,6 +189,8 @@ impl SelectedMempoolTransaction {
SelectedMempoolTransaction::Lender { .. } => "loan_contract", SelectedMempoolTransaction::Lender { .. } => "loan_contract",
SelectedMempoolTransaction::Borrower { .. } => "loan_payment", SelectedMempoolTransaction::Borrower { .. } => "loan_payment",
SelectedMempoolTransaction::Collateral { .. } => "collateral_claim", SelectedMempoolTransaction::Collateral { .. } => "collateral_claim",
SelectedMempoolTransaction::StorageKey { .. } => "storage_key",
SelectedMempoolTransaction::StorageValue { .. } => "storage_value",
} }
} }
@ -191,7 +206,9 @@ impl SelectedMempoolTransaction {
| SelectedMempoolTransaction::Swap { id, .. } | SelectedMempoolTransaction::Swap { id, .. }
| SelectedMempoolTransaction::Lender { id, .. } | SelectedMempoolTransaction::Lender { id, .. }
| SelectedMempoolTransaction::Borrower { 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_contract", bn, limit).await?;
delete_processed_rows_limited("loan_payment", 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("collateral_claim", bn, limit).await?;
delete_processed_rows_limited("storage_key", bn, limit).await?;
delete_processed_rows_limited("storage_value", bn, limit).await?;
Ok(()) Ok(())
} }

View File

@ -46,6 +46,8 @@ pub async fn mark_selected_transactions_processed(
bn, bn,
) )
.await?; .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(()) Ok(())
} }
@ -68,6 +70,8 @@ pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatc
&ids_for_table(batch, "collateral_claim"), &ids_for_table(batch, "collateral_claim"),
) )
.await?; .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(()) Ok(())
} }
@ -96,6 +100,8 @@ pub async fn restore_processed_by_signatures(signatures: &[String]) -> Result<bo
restored += unmark_by_signatures("loan_contract", "signature2", signatures).await?; restored += unmark_by_signatures("loan_contract", "signature2", signatures).await?;
restored += unmark_by_signatures("loan_payment", "signature", signatures).await?; restored += unmark_by_signatures("loan_payment", "signature", signatures).await?;
restored += unmark_by_signatures("collateral_claim", "signature", signatures).await?; restored += unmark_by_signatures("collateral_claim", "signature", signatures).await?;
restored += unmark_by_signatures("storage_key", "signature", signatures).await?;
restored += unmark_by_signatures("storage_value", "signature", signatures).await?;
Ok(restored > 0) Ok(restored > 0)
} }
@ -221,6 +227,18 @@ pub async fn mark_processed_by_signatures(signatures: &[String], block_number: u
signatures, signatures,
) )
.await?; .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(()) Ok(())
} }
@ -273,6 +291,16 @@ pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> {
signatures, signatures,
) )
.await?; .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(()) Ok(())
} }

View File

@ -279,6 +279,31 @@ pub async fn setup_mempool() -> Result<()> {
original BYTEA NOT NULL 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 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 transfer ADD COLUMN IF NOT EXISTS nft_series INTEGER NOT NULL DEFAULT 0;
ALTER TABLE nft ADD COLUMN IF NOT EXISTS count BIGINT NOT NULL DEFAULT 1; ALTER TABLE 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 loan_payment ALTER COLUMN signature TYPE TEXT;
ALTER TABLE collateral_claim ALTER COLUMN address TYPE TEXT; ALTER TABLE collateral_claim ALTER COLUMN address TYPE TEXT;
ALTER TABLE collateral_claim ALTER COLUMN signature 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 // 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 DELETE FROM collateral_claim a
USING collateral_claim b USING collateral_claim b
WHERE a.id > b.id AND a.signature = b.signature; 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 // 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_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 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 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 // Pick indexes speed up block selection; signature indexes enforce one
// pending copy of each transaction. // pending copy of each transaction.
@ -457,7 +502,9 @@ pub async fn clear_mempool() -> Result<()> {
swap, swap,
loan_contract, loan_contract,
loan_payment, loan_payment,
collateral_claim collateral_claim,
storage_key,
storage_value
RESTART IDENTITY; RESTART IDENTITY;
"#, "#,
) )

View File

@ -1,7 +1,87 @@
use super::*; 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::pending_effects::{BalanceOperand, PendingEffects};
use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; 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<Transaction> {
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<SelectedStorageValueParts> {
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<Vec<u8>> {
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( fn index_selected_wallet_transaction(
pending_effects: &mut PendingEffects, pending_effects: &mut PendingEffects,
addresses: &[&str], addresses: &[&str],
@ -209,6 +289,44 @@ pub async fn select_transactions_for_block(limit: i64) -> Result<SelectedMempool
signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original signature AS signature1, NULL::TEXT AS signature2, NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM collateral_claim FROM collateral_claim
WHERE processed = false WHERE processed = false
UNION ALL
SELECT
'storage_key'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2,
NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2,
NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2,
NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount,
NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount,
NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash,
address, hash, signature AS signature1, NULL::TEXT AS signature2,
NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM storage_key
WHERE processed = false
UNION ALL
SELECT
'storage_value'::TEXT AS kind, id, fee AS priority_fee, time,
NULL::TEXT AS sender, NULL::TEXT AS receiver, NULL::TEXT AS coin, NULL::BIGINT AS value, NULL::INTEGER AS nft_series,
NULL::BIGINT AS number, NULL::TEXT AS creator, NULL::TEXT AS ticker,
NULL::TEXT AS nft_name, NULL::SMALLINT AS series, NULL::BIGINT AS count,
NULL::TEXT AS advertiser, NULL::BIGINT AS fee1, NULL::BIGINT AS fee2,
NULL::TEXT AS ticker1, NULL::INTEGER AS nft_series1, NULL::TEXT AS ticker2,
NULL::INTEGER AS nft_series2, NULL::BIGINT AS value1, NULL::BIGINT AS value2,
NULL::TEXT AS sender1, NULL::BIGINT AS tip1, NULL::BIGINT AS tip2,
NULL::TEXT AS sender2, NULL::TEXT AS loan_coin, NULL::BIGINT AS loan_amount,
NULL::TEXT AS lender, NULL::TEXT AS collateral, NULL::BIGINT AS collateral_amount,
NULL::TEXT AS borrower, NULL::BIGINT AS payback_amount, NULL::TEXT AS contract_hash,
address, hash, signature AS signature1, NULL::TEXT AS signature2,
NULL::TEXT AS stored_txid, NULL::SMALLINT AS hard_limit, original
FROM storage_value
WHERE processed = false
) AS combined ) AS combined
ORDER BY priority_fee DESC, time ASC, kind ASC, id ASC ORDER BY priority_fee DESC, time ASC, kind ASC, id ASC
LIMIT $1 LIMIT $1
@ -329,6 +447,19 @@ pub async fn select_transactions_for_block(limit: i64) -> Result<SelectedMempool
contract_hash: required_string(&row, "contract_hash")?, contract_hash: required_string(&row, "contract_hash")?,
hash: row.get("hash"), hash: row.get("hash"),
}, },
"storage_key" => 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}")), _ => return Err(anyhow!("Unsupported mempool kind {kind}")),
}; };
transactions.push(transaction); transactions.push(transaction);
@ -946,6 +1077,77 @@ pub async fn apply_selected_transaction_math(
&decode(hash)?, &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 lender_ids = ids_for_table(batch, "loan_contract");
let borrower_ids = ids_for_table(batch, "loan_payment"); let borrower_ids = ids_for_table(batch, "loan_payment");
let collateral_ids = ids_for_table(batch, "collateral_claim"); 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("transfer", &transfer_ids).await?;
delete_rows("token", &token_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_contract", &lender_ids).await?;
delete_rows("loan_payment", &borrower_ids).await?; delete_rows("loan_payment", &borrower_ids).await?;
delete_rows("collateral_claim", &collateral_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(()) Ok(())
} }

View File

@ -18,6 +18,8 @@ pub mod previous_difficulty;
pub mod rewards_tx; pub mod rewards_tx;
pub mod save; pub mod save;
pub mod save_flags; pub mod save_flags;
pub mod storage_key_tx;
pub mod storage_value_tx;
pub mod structs; pub mod structs;
pub mod swap_tx; pub mod swap_tx;
pub mod token_provenance; pub mod token_provenance;

View File

@ -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::nft_tx::process_nft;
use crate::records::record_chain::pending_effects::PendingEffects; use crate::records::record_chain::pending_effects::PendingEffects;
use crate::records::record_chain::rewards_tx::process_rewards; 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::swap_tx::process_swap;
use crate::records::record_chain::token_tx::process_token; use crate::records::record_chain::token_tx::process_token;
use crate::records::record_chain::transfer_tx::process_transfer; use crate::records::record_chain::transfer_tx::process_transfer;
@ -188,6 +190,36 @@ pub async fn handle_transactions(
) )
.await .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 { match result {
Ok(new_binary_data) => { Ok(new_binary_data) => {

View File

@ -1,3 +1,4 @@
use crate::common::types::{STORAGE_KEY_INDEX_TREE, STORAGE_VALUE_ROLLBACK_TREE};
use crate::log::error; use crate::log::error;
use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db;
use crate::records::wallet_registry::{ use crate::records::wallet_registry::{
@ -41,6 +42,12 @@ pub enum PendingEffect {
key: Vec<u8>, key: Vec<u8>,
value: Vec<u8>, value: Vec<u8>,
}, },
DynamicTreeSet {
tree: Vec<u8>,
key: Vec<u8>,
value: Vec<u8>,
rollback_key: Vec<u8>,
},
TreeRemove { TreeRemove {
tree: &'static str, tree: &'static str,
key: Vec<u8>, key: Vec<u8>,
@ -56,6 +63,9 @@ pub enum PendingEffect {
key: Vec<u8>, key: Vec<u8>,
bytes: Vec<u8>, bytes: Vec<u8>,
}, },
TreeCreate {
name: Vec<u8>,
},
TokenSupplyAdd { TokenSupplyAdd {
ticker: String, ticker: String,
amount: u64, amount: u64,
@ -108,6 +118,21 @@ impl PendingEffects {
self.push(PendingEffect::TreeSet { tree, key, value }); self.push(PendingEffect::TreeSet { tree, key, value });
} }
pub fn set_dynamic_tree(
&mut self,
tree: Vec<u8>,
key: Vec<u8>,
value: Vec<u8>,
rollback_key: Vec<u8>,
) {
self.push(PendingEffect::DynamicTreeSet {
tree,
key,
value,
rollback_key,
});
}
pub fn remove_tree(&mut self, tree: &'static str, key: Vec<u8>) { pub fn remove_tree(&mut self, tree: &'static str, key: Vec<u8>) {
self.push(PendingEffect::TreeRemove { tree, key }); self.push(PendingEffect::TreeRemove { tree, key });
} }
@ -131,6 +156,10 @@ impl PendingEffects {
}); });
} }
pub fn create_tree(&mut self, name: Vec<u8>) {
self.push(PendingEffect::TreeCreate { name });
}
pub fn add_token_supply(&mut self, ticker: &str, amount: u64) { pub fn add_token_supply(&mut self, ticker: &str, amount: u64) {
self.push(PendingEffect::TokenSupplyAdd { self.push(PendingEffect::TokenSupplyAdd {
ticker: ticker.to_string(), ticker: ticker.to_string(),
@ -230,6 +259,13 @@ enum AppliedEffect {
key: Vec<u8>, key: Vec<u8>,
previous: Option<Vec<u8>>, previous: Option<Vec<u8>>,
}, },
DynamicTreeMutation {
tree: Vec<u8>,
key: Vec<u8>,
previous: Option<Vec<u8>>,
rollback_key: Vec<u8>,
previous_rollback: Option<Vec<u8>>,
},
Composite(Vec<AppliedEffect>), Composite(Vec<AppliedEffect>),
VanityUpdate { VanityUpdate {
owner_address: String, owner_address: String,
@ -237,6 +273,9 @@ enum AppliedEffect {
rollback_key: Vec<u8>, rollback_key: Vec<u8>,
previous_rollback: Option<Vec<u8>>, previous_rollback: Option<Vec<u8>>,
}, },
TreeCreated {
name: Vec<u8>,
},
Noop, Noop,
} }
@ -270,6 +309,34 @@ fn apply_effect(db: &Db, effect: &PendingEffect) -> Result<AppliedEffect, String
previous, previous,
}) })
} }
PendingEffect::DynamicTreeSet {
tree,
key,
value,
rollback_key,
} => {
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 } => { PendingEffect::TreeRemove { tree, key } => {
let tree_handle = db let tree_handle = db
.open_tree(tree) .open_tree(tree)
@ -314,6 +381,27 @@ fn apply_effect(db: &Db, effect: &PendingEffect) -> Result<AppliedEffect, String
previous, previous,
}) })
} }
PendingEffect::TreeCreate { name } => {
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 } => { PendingEffect::TokenSupplyAdd { ticker, amount } => {
let tree = db let tree = db
.open_tree("tokens") .open_tree("tokens")
@ -410,6 +498,21 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> {
key, key,
previous, previous,
} => restore_tree_value(db, tree, &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) => { AppliedEffect::Composite(mut effects) => {
let mut first_error = None; let mut first_error = None;
while let Some(effect) = effects.pop() { while let Some(effect) = effects.pop() {
@ -438,6 +541,17 @@ fn rollback_effect(db: &Db, effect: AppliedEffect) -> Result<(), String> {
previous_rollback, 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(()), AppliedEffect::Noop => Ok(()),
} }
} }
@ -461,6 +575,37 @@ fn set_tree_value(
Ok(previous) Ok(previous)
} }
fn set_dynamic_tree_value(
db: &Db,
tree: &[u8],
key: &[u8],
value: &[u8],
) -> Result<Option<Vec<u8>>, 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<u8> {
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( fn append_tree_value(
db: &Db, db: &Db,
tree: &'static str, tree: &'static str,
@ -506,6 +651,30 @@ fn restore_tree_value(
Ok(()) Ok(())
} }
fn restore_dynamic_tree_value(
db: &Db,
tree: &[u8],
key: &[u8],
previous: Option<Vec<u8>>,
) -> 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<u64, String> { fn read_supply(bytes: &[u8]) -> Result<u64, String> {
if bytes.len() != 8 { if bytes.len() != 8 {
return Err("Invalid token supply bytes".to_string()); return Err("Invalid token supply bytes".to_string());

View File

@ -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<u8>,
_db: &Db,
index_counter: Arc<Mutex<&mut usize>>,
miner: String,
block_header_number: u32,
pending_effects: &mut PendingEffects,
) -> Result<Vec<u8>, 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)
}

View File

@ -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<Vec<u8>, 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<OwnedStorageParts, String> {
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<Vec<u8>, 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<u8>,
db: &Db,
index_counter: Arc<Mutex<&mut usize>>,
miner: String,
block_header_number: u32,
pending_effects: &mut PendingEffects,
) -> Result<Vec<u8>, 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)
}

View File

@ -8,14 +8,19 @@ use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::rewards::RewardsTransaction; 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::swap::SwapTransaction;
use crate::blocks::token::CreateTokenTransaction; use crate::blocks::token::CreateTokenTransaction;
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
use crate::blocks::vanity::VanityAddressTransaction; use crate::blocks::vanity::VanityAddressTransaction;
use crate::common::types::{ use crate::common::types::{
Transaction, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, 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, GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, STORAGE_BOOL_TYPE,
TRANSFER_TYPE, VANITY_ADDRESS_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; use crate::rpc::command_maps::get_bytes;
@ -192,6 +197,78 @@ pub async fn load_block_from_binary(binary_data: &[u8]) -> Result<Block, String>
i += body_len; i += body_len;
vanity 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}")); return Err(format!("Unsupported transaction type: {txtype}"));
} }

View File

@ -8,6 +8,10 @@ use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::rewards::RewardsTransaction; 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::swap::SwapTransaction;
use crate::blocks::token::CreateTokenTransaction; use crate::blocks::token::CreateTokenTransaction;
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
@ -15,8 +19,9 @@ use crate::blocks::vanity::VanityAddressTransaction;
use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::common::types::{ use crate::common::types::{
Transaction, BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, 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, GENESIS_TYPE, ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, STORAGE_BOOL_TYPE,
TRANSFER_TYPE, VANITY_ADDRESS_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::fs;
use crate::rpc::command_maps::get_bytes; use crate::rpc::command_maps::get_bytes;
@ -209,6 +214,78 @@ pub async fn load_block(block_number: u32) -> Result<Block, String> {
i += body_len; i += body_len;
vanity 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}")); return Err(format!("Unsupported transaction type: {txtype}"));
} }

View File

@ -7,6 +7,10 @@ use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::rewards::RewardsTransaction; 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::swap::SwapTransaction;
use crate::blocks::token::CreateTokenTransaction; use crate::blocks::token::CreateTokenTransaction;
use crate::blocks::transfer::TransferTransaction; 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_COLLATERAL_STATUS: u8 = 49;
pub const RPC_NETWORK_MONITOR_ADD: u8 = 50; pub const RPC_NETWORK_MONITOR_ADD: u8 = 50;
pub const RPC_NETWORK_MONITOR_REMOVE: u8 = 51; 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 RPC_REPLY: u8 = 255;
pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024; 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, 10 => BurnTransaction::BYTE_LENGTH,
11 => IssueTokenTransaction::BYTE_LENGTH, 11 => IssueTokenTransaction::BYTE_LENGTH,
12 => VanityAddressTransaction::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, _ => 0,
} }
} }

View File

@ -128,6 +128,14 @@ async fn miner_earnings_from_transactions(
} }
Transaction::Collateral(tx) => earnings.add_fee(tx.unsigned_collateral_claim.txfee)?, Transaction::Collateral(tx) => earnings.add_fee(tx.unsigned_collateral_claim.txfee)?,
Transaction::Vanity(tx) => earnings.add_fee(tx.unsigned_vanity_address.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<Vec<u8>,
fn signature_count(transaction_type: u8) -> usize { fn signature_count(transaction_type: u8) -> usize {
match transaction_type { match transaction_type {
6 | 7 => 2, 6 | 7 => 2,
2..=5 | 8..=12 => 1, 2..=5 | 8..=12 | 100..=107 => 1,
_ => 0, _ => 0,
} }
} }

View File

@ -27,6 +27,7 @@ pub mod random_node;
pub mod receive_torrent; pub mod receive_torrent;
pub mod request_valid_nodes; pub mod request_valid_nodes;
pub mod route_reply; pub mod route_reply;
pub mod storage_lookup;
pub mod structs; pub mod structs;
pub mod time; pub mod time;
pub mod token_list; pub mod token_list;

View File

@ -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<String>,
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<StorageLookupEntry>,
strings: Vec<StorageLookupString>,
data_bytes: usize,
fee_per_byte: u64,
total_cost: u64,
node_payment_address: String,
}
fn json_response<T: Serialize>(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<Vec<u8>, 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<Vec<u8>, 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<String> {
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<bool, String> {
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<Vec<StorageLookupEntry>, 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<StorageLookupEntry>, Vec<StorageLookupString>), 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<StorageLookupEntry>, Vec<StorageLookupString>), 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::<String, Vec<&StorageLookupEntry>>::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<u8>,
data_key: Vec<u8>,
address: String,
wallet: Arc<Wallet>,
) -> Result<StorageLookupPayload, String> {
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::<usize>();
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<u8>,
data_key: Vec<u8>,
address: String,
wallet: Arc<Wallet>,
) -> 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<u8>,
data_key: Vec<u8>,
address: String,
txtype: u8,
tx: Vec<u8>,
wallet: Arc<Wallet>,
map: Arc<Mutex<Command>>,
) -> 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 }))
}

View File

@ -5,13 +5,19 @@ use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction; use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction; use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction; 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::swap::SwapTransaction;
use crate::blocks::token::CreateTokenTransaction; use crate::blocks::token::CreateTokenTransaction;
use crate::blocks::transfer::TransferTransaction; use crate::blocks::transfer::TransferTransaction;
use crate::blocks::vanity::VanityAddressTransaction; use crate::blocks::vanity::VanityAddressTransaction;
use crate::common::types::{ use crate::common::types::{
BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, 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::connections::get_client_type_from_memory;
use crate::records::memory::enums::ClientType; use crate::records::memory::enums::ClientType;
@ -59,6 +65,50 @@ async fn broadcast_tx(tx_bytes: Vec<u8>, map: Arc<Mutex<Command>>) {
} }
} }
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( pub async fn save_and_submit(
txtype: u8, txtype: u8,
tx: Vec<u8>, tx: Vec<u8>,
@ -507,6 +557,52 @@ pub async fn save_and_submit(
RpcResponse::Binary(msg) 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" let msg = "error: No such transaction type"
.to_string() .to_string()

View File

@ -1005,6 +1005,104 @@ pub async fn start_loop(
.send(&stream_locked, Some(&connections_key), uid) .send(&stream_locked, Some(&connections_key), uid)
.await; .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 => { 255 => {
commands::route_reply::route_reply( commands::route_reply::route_reply(
&connections_key, &connections_key,

View File

@ -5,10 +5,20 @@ use crate::blocks::loan_payment::{ContractPaymentTransaction, UnsignedContractPa
use crate::blocks::loans::{LoanContractTransaction, UnsignedLoanContractTransaction}; use crate::blocks::loans::{LoanContractTransaction, UnsignedLoanContractTransaction};
use crate::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction}; use crate::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
use crate::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction}; 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::swap::{SwapTransaction, UnsignedSwapTransaction};
use crate::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction}; use crate::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction};
use crate::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction}; use crate::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
use crate::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction}; 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::records::memory::response_channels::Byte3;
use crate::rpc::command_maps::RPC_SUBMIT_TRANSACTION; use crate::rpc::command_maps::RPC_SUBMIT_TRANSACTION;
use crate::{from_str, Value}; 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) .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<u8> {
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<u128> {
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::<u128>().ok();
}
value.to_string().parse::<u128>().ok()
}
pub async fn create_transaction_request( pub async fn create_transaction_request(
command_input: String, command_input: String,
hashkey: Byte3, hashkey: Byte3,
@ -44,7 +124,7 @@ pub async fn create_transaction_request(
} }
async fn create_transaction_bytes(transaction: &Value) -> Result<Vec<u8>, String> { async fn create_transaction_bytes(transaction: &Value) -> Result<Vec<u8>, 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 // Each transaction type is rebuilt through the same unsigned -> signed -> bytes
// flow used elsewhere so the standalone tool stays aligned with node serialization. // flow used elsewhere so the standalone tool stays aligned with node serialization.
@ -451,6 +531,276 @@ async fn create_transaction_bytes(transaction: &Value) -> Result<Vec<u8>, String
.await .await
.map_err(|e| format!("Failed to serialize vanity transaction: {e}"))? .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()), _ => return Err("No such transaction type".to_string()),
}; };

View File

@ -90,10 +90,21 @@ async fn attempt_bootstrap_connections(
Err(err) => err.to_string(), Err(err) => err.to_string(),
}; };
// A peer can reject us because it already has this connection recorded. // During outage recovery, a peer may already have the connection
// In that case retrying other bootstrap peers would not fix the local duplicate state. // recorded from an overlapping reconnect path. That is not a fatal
if err_string.contains("The connection is already in the connection manager Please wait 10 minutes and try again") { // bootstrap-recovery condition; try the next configured peer.
return Err(err_string); 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" { if context == "startup" {
error!("Error connecting to {server}: {err_string}"); error!("Error connecting to {server}: {err_string}");

View File

@ -439,6 +439,136 @@ async fn transaction_balance_view(
check_saved_txid: true, 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,
})
}
} }
} }

View File

@ -16,6 +16,8 @@ pub mod verify_issue_token;
pub mod verify_lender; pub mod verify_lender;
pub mod verify_marketing; pub mod verify_marketing;
pub mod verify_rewards; pub mod verify_rewards;
pub mod verify_storage_key;
pub mod verify_storage_values;
pub mod verify_swap; pub mod verify_swap;
pub mod verify_transfer; pub mod verify_transfer;
pub mod verify_vanity; pub mod verify_vanity;

View File

@ -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( Err(
"Validation failed: unsupported transaction variant reached verification dispatch." "Validation failed: unsupported transaction variant reached verification dispatch."
.to_string(), .to_string(),

View File

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

View File

@ -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<T: StorageValueTransaction>(
tx: &T,
db: &Db,
expected_type: u8,
label: &str,
) -> Result<String, String> {
// 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<String, String> {
verify_storage_value(self, db, STORAGE_BOOL_TYPE, "Storage Bool").await
}
}
impl StorageU8 {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
verify_storage_value(self, db, STORAGE_U8_TYPE, "Storage U8").await
}
}
impl StorageU16 {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
verify_storage_value(self, db, STORAGE_U16_TYPE, "Storage U16").await
}
}
impl StorageU32 {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
verify_storage_value(self, db, STORAGE_U32_TYPE, "Storage U32").await
}
}
impl StorageU64 {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
verify_storage_value(self, db, STORAGE_U64_TYPE, "Storage U64").await
}
}
impl StorageU128 {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
verify_storage_value(self, db, STORAGE_U128_TYPE, "Storage U128").await
}
}
impl StorageString {
pub async fn verify(&self, db: &Db) -> Result<String, String> {
// 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
}
}