From 05b027f30efd7249be209eaf14ff0fb539f486c7 Mon Sep 17 00:00:00 2001 From: viraladmin <00purple@gmail.com> Date: Wed, 24 Jun 2026 08:04:59 -0600 Subject: [PATCH] miner rewards reporting update --- src/orphans/undo_transactions/undo_rewards.rs | 2 + src/records/memory/mempool/mod.rs | 6 +- src/records/memory/mempool/processing.rs | 18 +- src/records/memory/mempool/selection.rs | 71 ++++- src/records/record_chain/miner_earnings.rs | 248 ++++++++++++++++++ src/records/record_chain/mod.rs | 1 + src/records/record_chain/rewards_tx.rs | 10 + src/records/record_chain/save.rs | 6 +- src/rpc/commands/address_history.rs | 19 +- 9 files changed, 355 insertions(+), 26 deletions(-) create mode 100644 src/records/record_chain/miner_earnings.rs diff --git a/src/orphans/undo_transactions/undo_rewards.rs b/src/orphans/undo_transactions/undo_rewards.rs index dc11103..018149f 100644 --- a/src/orphans/undo_transactions/undo_rewards.rs +++ b/src/orphans/undo_transactions/undo_rewards.rs @@ -2,6 +2,7 @@ use crate::blocks::rewards::RewardsTransaction; use crate::common::network_paths_and_settings::block_extension_and_paths; use crate::decode; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; +use crate::records::record_chain::miner_earnings::remove_finalized_miner_earnings; use crate::records::record_chain::rewards_tx::{ remove_immature_reward, remove_reward_credit_marker, reward_credit_applied, }; @@ -41,6 +42,7 @@ pub async fn undo_rewards_transaction( let hash = decode(transaction.unsigned.hash().await).unwrap(); let _ = remove_wallet_transaction_index(db, &[mining_receiver], &hash); + remove_finalized_miner_earnings(db, &hash); // Remove the reward transaction lookup from the txid tree. let tree = db.open_tree("txid").unwrap(); diff --git a/src/records/memory/mempool/mod.rs b/src/records/memory/mempool/mod.rs index 9b981ab..f39c4f6 100644 --- a/src/records/memory/mempool/mod.rs +++ b/src/records/memory/mempool/mod.rs @@ -379,11 +379,7 @@ async fn delete_processed_before_or_at(block_number: u32, limit: i64) -> Result< Ok(()) } -async fn delete_processed_rows_limited( - table: &str, - block_number: i32, - limit: i64, -) -> Result { +async fn delete_processed_rows_limited(table: &str, block_number: i32, limit: i64) -> Result { let statement = format!( r#" DELETE FROM {table} diff --git a/src/records/memory/mempool/processing.rs b/src/records/memory/mempool/processing.rs index 499907f..dc5d8ef 100644 --- a/src/records/memory/mempool/processing.rs +++ b/src/records/memory/mempool/processing.rs @@ -59,22 +59,10 @@ pub async fn restore_selected_transactions_processed(batch: &SelectedMempoolBatc unmark_rows_by_ids("burn", &ids_for_table(batch, "burn")).await?; unmark_rows_by_ids("nft", &ids_for_table(batch, "nft")).await?; unmark_rows_by_ids("marketing", &ids_for_table(batch, "marketing")).await?; - unmark_rows_by_ids( - "vanity_address", - &ids_for_table(batch, "vanity_address"), - ) - .await?; + unmark_rows_by_ids("vanity_address", &ids_for_table(batch, "vanity_address")).await?; unmark_rows_by_ids("swap", &ids_for_table(batch, "swap")).await?; - unmark_rows_by_ids( - "loan_contract", - &ids_for_table(batch, "loan_contract"), - ) - .await?; - unmark_rows_by_ids( - "loan_payment", - &ids_for_table(batch, "loan_payment"), - ) - .await?; + unmark_rows_by_ids("loan_contract", &ids_for_table(batch, "loan_contract")).await?; + unmark_rows_by_ids("loan_payment", &ids_for_table(batch, "loan_payment")).await?; unmark_rows_by_ids( "collateral_claim", &ids_for_table(batch, "collateral_claim"), diff --git a/src/records/memory/mempool/selection.rs b/src/records/memory/mempool/selection.rs index c606704..385464e 100644 --- a/src/records/memory/mempool/selection.rs +++ b/src/records/memory/mempool/selection.rs @@ -1,4 +1,5 @@ use super::*; +use crate::records::record_chain::miner_earnings::MinerEarnings; use crate::records::record_chain::pending_effects::{BalanceOperand, PendingEffects}; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction; @@ -347,13 +348,14 @@ pub async fn apply_selected_transaction_math( block_number: u32, start_index: usize, pending_effects: &mut PendingEffects, -) -> Result<()> { +) -> Result { // Net balance deltas are collected first so multiple changes to the same // address/asset pair become one balance-sheet write. let mut balance_changes: HashMap = HashMap::new(); let miner_address = address_key_bytes(db, &miner); let base_coin = BASECOIN.as_bytes().to_vec(); let mut tx_index = start_index; + let mut miner_earnings = MinerEarnings::default(); for tx in &batch.transactions { // Transaction index is stored with the block number for later txid @@ -370,6 +372,9 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected transfer fee")?) + .map_err(|err| anyhow!(err))?; // Transfers move the asset, charge the base fee, and credit // that fee to the miner including this transaction. let transfer_asset = nft_asset_name(coin, *nft_series as u32); @@ -411,6 +416,9 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected token fee")?) + .map_err(|err| anyhow!(err))?; // Token creation mints the initial supply and records metadata // used by later issue and burn validation. add_balance_change(db, &mut balance_changes, creator, ticker, *number); @@ -478,6 +486,9 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected issue-token fee")?) + .map_err(|err| anyhow!(err))?; add_balance_change(db, &mut balance_changes, creator, ticker, *number); add_balance_change(db, &mut balance_changes, creator, &BASECOIN, -*fee); add_balance_change_bytes( @@ -517,6 +528,9 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected burn fee")?) + .map_err(|err| anyhow!(err))?; let burned_asset = nft_asset_name(coin, *nft_series as u32); add_balance_change(db, &mut balance_changes, address, &burned_asset, -*value); add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); @@ -552,6 +566,9 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected NFT fee")?) + .map_err(|err| anyhow!(err))?; // Series-one NFT creation expands into numbered assets; later // series use the given NFT name as the asset key. if *series == 1 { @@ -622,6 +639,9 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected marketing fee")?) + .map_err(|err| anyhow!(err))?; add_balance_change(db, &mut balance_changes, advertiser, &BASECOIN, -*fee); add_balance_change_bytes( &mut balance_changes, @@ -649,6 +669,9 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected vanity fee")?) + .map_err(|err| anyhow!(err))?; // Vanity transactions update the alias registry during the // final block-effect commit so rollback can restore it. pending_effects.update_vanity(address, vanity_address, hash); @@ -688,6 +711,28 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee1, "selected swap fee1")?) + .map_err(|err| anyhow!(err))?; + miner_earnings + .add_fee(nonnegative_u64(*fee2, "selected swap fee2")?) + .map_err(|err| anyhow!(err))?; + miner_earnings + .add_tip( + ticker1, + u32::try_from(*nft_series1) + .map_err(|_| anyhow!("Selected swap NFT series1 was negative"))?, + nonnegative_u64(*tip1, "selected swap tip1")?, + ) + .map_err(|err| anyhow!(err))?; + miner_earnings + .add_tip( + ticker2, + u32::try_from(*nft_series2) + .map_err(|_| anyhow!("Selected swap NFT series2 was negative"))?, + nonnegative_u64(*tip2, "selected swap tip2")?, + ) + .map_err(|err| anyhow!(err))?; // Swaps debit both sides first, then credit received assets, // miner fees, and optional asset tips. let asset1 = nft_asset_name(ticker1, *nft_series1 as u32); @@ -764,6 +809,9 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected loan fee")?) + .map_err(|err| anyhow!(err))?; // Loan creation moves the loan asset to the borrower and locks // collateral under a contract-specific holding key. add_balance_change(db, &mut balance_changes, lender, &BASECOIN, -*fee); @@ -833,6 +881,16 @@ pub async fn apply_selected_transaction_math( // Loan payments resolve the contract, then move repayment from // borrower to lender while paying any tip to the miner. let (loan_coin, lender) = resolve_loan_details(db, contract_hash).await?; + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected loan payment fee")?) + .map_err(|err| anyhow!(err))?; + miner_earnings + .add_tip( + &binary_to_string(loan_coin.clone()), + 0, + nonnegative_u64(*tip, "selected loan payment tip")?, + ) + .map_err(|err| anyhow!(err))?; add_balance_change(db, &mut balance_changes, address, &BASECOIN, -*fee); add_balance_change_bytes( &mut balance_changes, @@ -897,6 +955,9 @@ pub async fn apply_selected_transaction_math( hash, .. } => { + miner_earnings + .add_fee(nonnegative_u64(*fee, "selected collateral fee")?) + .map_err(|err| anyhow!(err))?; // Collateral claims release the contract holding to the claimant // and mark the loan as closed. let (collateral, collateral_amount) = @@ -965,7 +1026,11 @@ pub async fn apply_selected_transaction_math( pending_effects.add_balance(&address, change.unsigned_abs(), &coin, operand); } - Ok(()) + Ok(miner_earnings) +} + +fn nonnegative_u64(value: i64, field: &str) -> Result { + u64::try_from(value).map_err(|_| anyhow!("{field} cannot be negative")) } pub async fn clear_selected_transaction_sql( @@ -979,7 +1044,7 @@ pub async fn clear_selected_transaction_sql( // selected batch into the block file afterward. let batch = select_transactions_for_block(limit).await?; let mut pending_effects = PendingEffects::default(); - apply_selected_transaction_math( + let _miner_earnings = apply_selected_transaction_math( &batch, db, miner, diff --git a/src/records/record_chain/miner_earnings.rs b/src/records/record_chain/miner_earnings.rs new file mode 100644 index 0000000..f94b8ca --- /dev/null +++ b/src/records/record_chain/miner_earnings.rs @@ -0,0 +1,248 @@ +use crate::blocks::loans::LoanContractTransaction; +use crate::common::types::Transaction; +use crate::decode; +use crate::records::memory::mempool::BASECOIN; +use crate::rpc::commands::transaction_by_txid::request_transaction_by_txid; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use std::collections::BTreeMap; + +pub const MINER_EARNING_FEE: u8 = 0; +pub const MINER_EARNING_TIP: u8 = 1; +pub const MINER_EARNING_ASSET_BYTES: usize = 15; +pub const MINER_EARNING_ENTRY_BYTES: usize = 1 + MINER_EARNING_ASSET_BYTES + 4 + 8; +const FINALIZED_MINER_EARNINGS_TREE: &str = "finalized_miner_earnings"; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct MinerEarnings { + entries: BTreeMap<(u8, String, u32), u64>, +} + +impl MinerEarnings { + pub fn add_fee(&mut self, amount: u64) -> Result<(), String> { + self.add(MINER_EARNING_FEE, BASECOIN.clone(), 0, amount) + } + + pub fn add_tip(&mut self, asset: &str, nft_series: u32, amount: u64) -> Result<(), String> { + self.add(MINER_EARNING_TIP, asset.to_string(), nft_series, amount) + } + + fn add( + &mut self, + earning_type: u8, + asset: String, + nft_series: u32, + amount: u64, + ) -> Result<(), String> { + if amount == 0 { + return Ok(()); + } + let asset = fixed_asset_name(&asset)?; + let entry = self + .entries + .entry((earning_type, asset, nft_series)) + .or_default(); + *entry = entry + .checked_add(amount) + .ok_or_else(|| "Miner earnings amount overflowed u64.".to_string())?; + Ok(()) + } + + pub fn merge(&mut self, other: MinerEarnings) -> Result<(), String> { + for ((earning_type, asset, nft_series), amount) in other.entries { + self.add(earning_type, asset, nft_series, amount)?; + } + Ok(()) + } + + pub fn encode(&self) -> Result, String> { + let count = u32::try_from(self.entries.len()) + .map_err(|_| "Miner earnings entry count overflowed u32.".to_string())?; + let mut bytes = + Vec::with_capacity(4 + self.entries.len().saturating_mul(MINER_EARNING_ENTRY_BYTES)); + bytes.extend_from_slice(&count.to_le_bytes()); + + for ((earning_type, asset, nft_series), amount) in &self.entries { + bytes.push(*earning_type); + bytes.extend_from_slice(asset.as_bytes()); + bytes.extend_from_slice(&nft_series.to_le_bytes()); + bytes.extend_from_slice(&amount.to_le_bytes()); + } + Ok(bytes) + } +} + +fn fixed_asset_name(asset: &str) -> Result { + let bytes = asset.as_bytes(); + if bytes.len() != MINER_EARNING_ASSET_BYTES { + return Err(format!( + "Miner earning asset must be exactly {MINER_EARNING_ASSET_BYTES} bytes: {asset:?}" + )); + } + Ok(asset.to_string()) +} + +async fn loan_payment_tip_asset(db: &Db, contract_hash: &str) -> Result { + let contract_hash = decode(contract_hash) + .map_err(|err| format!("Failed to decode loan contract hash for miner tip: {err}"))?; + let RpcResponse::Binary(bytes) = request_transaction_by_txid(db, contract_hash).await; + if bytes.is_empty() || bytes[0] != 7 { + return Err("Loan payment miner tip references an invalid loan contract.".to_string()); + } + let loan = LoanContractTransaction::from_bytes(7, &bytes[1..]) + .await + .map_err(|err| format!("Failed to decode loan contract for miner tip: {err}"))?; + fixed_asset_name(&loan.unsigned_loan_contract.loan_coin) +} + +pub async fn earnings_from_transactions( + db: &Db, + transactions: &[Transaction], +) -> Result { + let mut earnings = MinerEarnings::default(); + + for transaction in transactions { + match transaction { + Transaction::Genesis(_) | Transaction::Rewards(_) => {} + Transaction::Transfer(tx) => earnings.add_fee(tx.unsigned_transfer.txfee)?, + Transaction::Token(tx) => earnings.add_fee(tx.unsigned_create_token.txfee)?, + Transaction::IssueToken(tx) => earnings.add_fee(tx.unsigned_issue_token.txfee)?, + Transaction::Burn(tx) => earnings.add_fee(tx.unsigned_burn.txfee)?, + Transaction::Nft(tx) => earnings.add_fee(tx.unsigned_create_nft.txfee)?, + Transaction::Marketing(tx) => earnings.add_fee(tx.unsigned_marketing.txfee)?, + Transaction::Swap(tx) => { + earnings.add_fee(tx.unsigned_swap.txfee1)?; + earnings.add_fee(tx.unsigned_swap.txfee2)?; + earnings.add_tip( + &tx.unsigned_swap.ticker1, + tx.unsigned_swap.nft_series1, + tx.unsigned_swap.tip1, + )?; + earnings.add_tip( + &tx.unsigned_swap.ticker2, + tx.unsigned_swap.nft_series2, + tx.unsigned_swap.tip2, + )?; + } + Transaction::Lender(tx) => earnings.add_fee(tx.unsigned_loan_contract.txfee)?, + Transaction::Borrower(tx) => { + earnings.add_fee(tx.unsigned_contract_payment.txfee)?; + if tx.unsigned_contract_payment.tip > 0 { + let asset = + loan_payment_tip_asset(db, &tx.unsigned_contract_payment.contract_hash) + .await?; + earnings.add_tip(&asset, 0, tx.unsigned_contract_payment.tip)?; + } + } + Transaction::Collateral(tx) => earnings.add_fee(tx.unsigned_collateral_claim.txfee)?, + Transaction::Vanity(tx) => earnings.add_fee(tx.unsigned_vanity_address.txfee)?, + } + } + + Ok(earnings) +} + +pub fn store_finalized_miner_earnings( + db: &Db, + reward_txid: &[u8], + earnings: &MinerEarnings, +) -> Result<(), String> { + let encoded = earnings.encode()?; + db.open_tree(FINALIZED_MINER_EARNINGS_TREE) + .map_err(|err| format!("Failed to open finalized miner earnings tree: {err}"))? + .insert(reward_txid, encoded) + .map_err(|err| format!("Failed to store finalized miner earnings: {err}"))?; + Ok(()) +} + +pub fn finalized_miner_earnings_bytes(db: &Db, reward_txid: &[u8]) -> Result, String> { + let tree = db + .open_tree(FINALIZED_MINER_EARNINGS_TREE) + .map_err(|err| format!("Failed to open finalized miner earnings tree: {err}"))?; + let encoded = tree + .get(reward_txid) + .map_err(|err| format!("Failed to read finalized miner earnings: {err}"))? + .map(|value| value.to_vec()) + .unwrap_or_else(|| 0u32.to_le_bytes().to_vec()); + validate_encoded_miner_earnings(&encoded)?; + Ok(encoded) +} + +pub fn remove_finalized_miner_earnings(db: &Db, reward_txid: &[u8]) { + if let Ok(tree) = db.open_tree(FINALIZED_MINER_EARNINGS_TREE) { + let _ = tree.remove(reward_txid); + } +} + +fn validate_encoded_miner_earnings(encoded: &[u8]) -> Result<(), String> { + let count_bytes = encoded + .get(0..4) + .ok_or_else(|| "Finalized miner earnings record is missing its count.".to_string())?; + let count = u32::from_le_bytes( + count_bytes + .try_into() + .map_err(|_| "Finalized miner earnings count was invalid.".to_string())?, + ) as usize; + let expected = 4usize + .checked_add( + count + .checked_mul(MINER_EARNING_ENTRY_BYTES) + .ok_or_else(|| "Finalized miner earnings length overflowed.".to_string())?, + ) + .ok_or_else(|| "Finalized miner earnings length overflowed.".to_string())?; + if encoded.len() != expected { + return Err(format!( + "Finalized miner earnings record has invalid length: expected {expected}, found {}.", + encoded.len() + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn combines_matching_fees_and_tips() { + let mut earnings = MinerEarnings::default(); + earnings.add_fee(10).unwrap(); + earnings.add_fee(20).unwrap(); + earnings.add_tip("example ", 0, 7).unwrap(); + earnings.add_tip("example ", 0, 8).unwrap(); + + let encoded = earnings.encode().unwrap(); + assert_eq!(u32::from_le_bytes(encoded[0..4].try_into().unwrap()), 2); + assert_eq!(encoded.len(), 4 + (2 * MINER_EARNING_ENTRY_BYTES)); + assert!(encoded.windows(8).any(|bytes| bytes == 30u64.to_le_bytes())); + assert!(encoded.windows(8).any(|bytes| bytes == 15u64.to_le_bytes())); + } + + #[test] + fn separates_nft_tip_series() { + let mut earnings = MinerEarnings::default(); + earnings.add_tip("example ", 1, 1).unwrap(); + earnings.add_tip("example ", 2, 1).unwrap(); + + let encoded = earnings.encode().unwrap(); + assert_eq!(u32::from_le_bytes(encoded[0..4].try_into().unwrap()), 2); + } + + #[test] + fn persists_and_removes_finalized_earnings() { + let db = crate::sled::Config::new().temporary(true).open().unwrap(); + let reward_txid = [7u8; 32]; + let mut earnings = MinerEarnings::default(); + earnings.add_fee(42).unwrap(); + + store_finalized_miner_earnings(&db, &reward_txid, &earnings).unwrap(); + let stored = finalized_miner_earnings_bytes(&db, &reward_txid).unwrap(); + assert_eq!(u32::from_le_bytes(stored[0..4].try_into().unwrap()), 1); + + remove_finalized_miner_earnings(&db, &reward_txid); + assert_eq!( + finalized_miner_earnings_bytes(&db, &reward_txid).unwrap(), + 0u32.to_le_bytes() + ); + } +} diff --git a/src/records/record_chain/mod.rs b/src/records/record_chain/mod.rs index c65fbe7..0ee12f8 100644 --- a/src/records/record_chain/mod.rs +++ b/src/records/record_chain/mod.rs @@ -8,6 +8,7 @@ pub mod header_number; pub mod issue_token_tx; pub mod lender_tx; pub mod marketing_tx; +pub mod miner_earnings; pub mod nft_provenance; pub mod nft_tx; pub mod parse_transactions; diff --git a/src/records/record_chain/rewards_tx.rs b/src/records/record_chain/rewards_tx.rs index c6de6b2..44c014b 100644 --- a/src/records/record_chain/rewards_tx.rs +++ b/src/records/record_chain/rewards_tx.rs @@ -5,6 +5,9 @@ use crate::lazy_static; use crate::records::balance_sheet::operations::balance_sheet_operation_with_db; use crate::records::block_height::get_block_height::get_height; use crate::records::memory::mempool::BASECOIN; +use crate::records::record_chain::miner_earnings::{ + earnings_from_transactions, store_finalized_miner_earnings, MinerEarnings, +}; use crate::records::record_chain::pending_effects::PendingEffects; use crate::records::record_chain::wallet_tx_index::index_wallet_transaction_direct; use crate::records::unpack_block::load_by_block_number::load_block; @@ -23,6 +26,7 @@ struct ImmatureReward { amount: u64, txid: Vec, entry_index: u32, + earnings: MinerEarnings, } lazy_static! { @@ -56,6 +60,8 @@ fn mark_reward_credit_applied(db: &Db, block_height: u32) -> Result<(), String> } fn credit_matured_reward(db: &Db, reward: ImmatureReward) -> Result<(), String> { + store_finalized_miner_earnings(db, &reward.txid, &reward.earnings)?; + if reward_credit_applied(db, reward.block_height) { return Ok(()); } @@ -82,6 +88,7 @@ pub async fn record_saved_reward( amount: u64, txid: Vec, entry_index: u32, + earnings: MinerEarnings, ) -> Result<(), String> { let matured = { let mut rewards = IMMATURE_REWARDS.lock().await; @@ -92,6 +99,7 @@ pub async fn record_saved_reward( amount, txid, entry_index, + earnings, }); if rewards.len() > IMMATURE_REWARD_WINDOW { @@ -124,6 +132,7 @@ pub async fn rebuild_immature_reward_window(db: &Db) -> Result<(), String> { Err(_) => continue, }; let miner = block.vrf_block.unmined_block.miner; + let earnings = earnings_from_transactions(db, &block.transactions).await?; for (position, transaction) in block.transactions.into_iter().enumerate() { if let Transaction::Rewards(rewards) = transaction { @@ -137,6 +146,7 @@ pub async fn rebuild_immature_reward_window(db: &Db) -> Result<(), String> { amount: rewards.unsigned.value, txid, entry_index, + earnings: earnings.clone(), }); break; } diff --git a/src/records/record_chain/save.rs b/src/records/record_chain/save.rs index 18088b2..537fcf4 100644 --- a/src/records/record_chain/save.rs +++ b/src/records/record_chain/save.rs @@ -22,6 +22,7 @@ use crate::records::memory::mempool::{ }; use crate::records::memory::network_mapping::NodeInfo; use crate::records::memory::torrent_status::prune_torrent_statuses_through_height; +use crate::records::record_chain::miner_earnings::earnings_from_transactions; use crate::records::record_chain::parse_transactions::handle_transactions; use crate::records::record_chain::pending_effects::PendingEffects; use crate::records::record_chain::previous_difficulty::previous_block_difficulty; @@ -132,6 +133,7 @@ pub async fn save_block(params: SaveBlockParams) -> Result<(), String> { let mut index_counter = 0; let index_mutex = Arc::new(Mutex::new(&mut index_counter)); let mut pending_effects = PendingEffects::default(); + let mut miner_earnings = earnings_from_transactions(&db, &block.transactions).await?; // Append transaction-derived record data to the block binary while // tracking the index offset where mempool-derived records begin. @@ -171,7 +173,7 @@ pub async fn save_block(params: SaveBlockParams) -> Result<(), String> { .await .map_err(|e| e.to_string())?; - apply_selected_transaction_math( + let selected_earnings = apply_selected_transaction_math( &selected, &db, miner.clone(), @@ -181,6 +183,7 @@ pub async fn save_block(params: SaveBlockParams) -> Result<(), String> { ) .await .map_err(|e| e.to_string())?; + miner_earnings.merge(selected_earnings)?; save_binary_data_with_mempool_stream(SaveBinaryDataWithMempoolStreamParams { data: &binary_data, @@ -227,6 +230,7 @@ pub async fn save_block(params: SaveBlockParams) -> Result<(), String> { amount, txid, entry_index, + miner_earnings, ) .await { diff --git a/src/rpc/commands/address_history.rs b/src/rpc/commands/address_history.rs index c1dafad..ebb9674 100644 --- a/src/rpc/commands/address_history.rs +++ b/src/rpc/commands/address_history.rs @@ -1,4 +1,5 @@ use crate::records::memory::mempool::latest_pending_txids_by_address; +use crate::records::record_chain::miner_earnings::finalized_miner_earnings_bytes; use crate::records::record_chain::wallet_tx_index::WALLET_TX_INDEX_TREE; use crate::rpc::commands::transaction_by_txid::transaction_bytes_with_block; use crate::rpc::responses::RpcResponse; @@ -38,6 +39,16 @@ async fn transaction_records_response( for txid in txid_bytes.chunks_exact(TXID_LENGTH) { match transaction_bytes_with_block(db, txid).await { Ok((block_height, transaction_bytes)) => { + let miner_earnings = if transaction_bytes.first() == Some(&1) { + match finalized_miner_earnings_bytes(db, txid) { + Ok(bytes) => bytes, + Err(err) => { + return RpcResponse::Binary(format!("error: {err}").into_bytes()); + } + } + } else { + 0u32.to_le_bytes().to_vec() + }; let payload = if strip_signatures { match without_signature_bytes(&transaction_bytes) { Ok(payload) => payload, @@ -46,7 +57,7 @@ async fn transaction_records_response( } else { transaction_bytes }; - entries.push((txid.to_vec(), block_height, payload)); + entries.push((txid.to_vec(), block_height, payload, miner_earnings)); } Err(err) => { return RpcResponse::Binary(err.into_bytes()); @@ -56,7 +67,7 @@ async fn transaction_records_response( let mut response = Vec::new(); response.extend_from_slice(&(entries.len() as u32).to_le_bytes()); - for (txid, block_height, transaction_bytes) in entries { + for (txid, block_height, transaction_bytes, miner_earnings) in entries { let Ok(tx_len) = u32::try_from(transaction_bytes.len()) else { return RpcResponse::Binary(b"error: Transaction payload too large".to_vec()); }; @@ -64,6 +75,10 @@ async fn transaction_records_response( response.extend_from_slice(&block_height.to_le_bytes()); response.extend_from_slice(&tx_len.to_le_bytes()); response.extend_from_slice(&transaction_bytes); + // Every history record ends with a miner-earnings count. Reward + // records may then include fixed entries of: + // type:u8, asset:[u8;15], nft_series:u32, amount:u64. + response.extend_from_slice(&miner_earnings); } RpcResponse::Binary(response)