iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 6d05a879332bd75391e0fdb2a91931b1b4ee090b
parent de1afc8ccd0fb0a0cada1501aadcc1de9b3be331
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Fri,  4 Sep 2026 14:22:53 +0200

Show compact storage bytes in explorer

Diffstat:
Msrc/adapters/chain_store/compact.rs | 113++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/adapters/http/api.rs | 6++++--
Msrc/adapters/http/index_html.rs | 6+++---
Msrc/adapters/http/types.rs | 1+
Msrc/adapters/http/ui.rs | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/app/ledger_view.rs | 9+++++++++
Msrc/domain/ledger_ops.rs | 18+++++++++++++++++-
Mwww/assets/iuna-ui.js | 11++---------
8 files changed, 209 insertions(+), 53 deletions(-)

diff --git a/src/adapters/chain_store/compact.rs b/src/adapters/chain_store/compact.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, sync::Arc}; use anyhow::{Context, Result, bail}; @@ -34,6 +34,18 @@ struct EncodeTables { #[derive(Clone, Debug, Default)] pub(crate) struct CompactBlockContext { tables: EncodeTables, + size_breakdowns: Arc<BTreeMap<String, CompactBlockSizeBreakdown>>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(crate) struct CompactBlockSizeBreakdown { + pub(crate) total_bytes: usize, + pub(crate) header_and_proof_bytes: usize, + pub(crate) transaction_bytes: usize, + pub(crate) transfer_bytes: usize, + pub(crate) burn_bytes: usize, + pub(crate) mine_bytes: usize, + pub(crate) burn_bundle_bytes: usize, } impl CompactBlockContext { @@ -46,34 +58,47 @@ impl CompactBlockContext { context.tables.register_address(address); } for block in blocks { - let mut writer = CompactWriter::default(); - encode_block_body(&mut writer, block, &mut context.tables)?; - context.tables.register_protocol_id(&block.hash); + context.append_block_with_size_breakdown(block)?; } Ok(context) } pub(crate) fn block_size_bytes(&self, block: &Block) -> Result<usize> { + Ok(self.block_size_breakdown(block)?.total_bytes) + } + + pub(crate) fn block_size_breakdown(&self, block: &Block) -> Result<CompactBlockSizeBreakdown> { let mut tables = self.tables.clone(); let mut writer = CompactWriter::default(); - encode_block_body(&mut writer, block, &mut tables)?; - Ok(writer.into_inner().len()) + encode_block_body_with_size_breakdown(&mut writer, block, &mut tables) } pub(crate) fn append_block(&mut self, block: &Block) -> Result<()> { + self.append_block_with_size_breakdown(block).map(|_| ()) + } + + pub(crate) fn append_block_with_size_breakdown( + &mut self, + block: &Block, + ) -> Result<CompactBlockSizeBreakdown> { let mut tables = self.tables.clone(); let mut writer = CompactWriter::default(); - encode_block_body(&mut writer, block, &mut tables)?; + let breakdown = encode_block_body_with_size_breakdown(&mut writer, block, &mut tables)?; tables.register_protocol_id(&block.hash); self.tables = tables; - Ok(()) + Arc::make_mut(&mut self.size_breakdowns).insert(block.hash.clone(), breakdown.clone()); + Ok(breakdown) } pub(crate) fn append_trusted_block(&mut self, block: &Block) -> Result<()> { - let mut writer = CompactWriter::default(); - encode_block_body(&mut writer, block, &mut self.tables)?; - self.tables.register_protocol_id(&block.hash); - Ok(()) + self.append_block(block) + } + + pub(crate) fn stored_block_size_breakdown( + &self, + block_hash: &str, + ) -> Option<&CompactBlockSizeBreakdown> { + self.size_breakdowns.get(block_hash) } } @@ -216,6 +241,15 @@ fn encode_block_body( block: &Block, tables: &mut EncodeTables, ) -> Result<()> { + encode_block_body_with_size_breakdown(writer, block, tables).map(|_| ()) +} + +fn encode_block_body_with_size_breakdown( + writer: &mut CompactWriter, + block: &Block, + tables: &mut EncodeTables, +) -> Result<CompactBlockSizeBreakdown> { + let block_start = writer.bytes.len(); writer.varint(block.timestamp_ms); writer.address(&block.miner, tables)?; writer.u8(match block.finalizer_mode { @@ -233,12 +267,36 @@ fn encode_block_body( writer.fixed_hex::<64>(&proof.signature, "leader signature")?; } writer.varint(block.transactions.len() as u64); + let header_end = writer.bytes.len(); + let transaction_start = writer.bytes.len(); + let mut transfer_bytes = 0_usize; + let mut burn_bytes = 0_usize; + let mut mine_bytes = 0_usize; for transaction in &block.transactions { + let item_start = writer.bytes.len(); encode_transaction(writer, transaction, tables)?; + let item_bytes = writer.bytes.len().saturating_sub(item_start); + match transaction { + Transaction::Transfer { .. } => { + transfer_bytes = transfer_bytes.saturating_add(item_bytes) + } + Transaction::Burn { .. } => burn_bytes = burn_bytes.saturating_add(item_bytes), + Transaction::Mine { .. } => mine_bytes = mine_bytes.saturating_add(item_bytes), + } tables.register_protocol_id(transaction.signature()); } + let burn_bundle_start = writer.bytes.len(); encode_burn_bundle_section(writer, block, tables)?; - Ok(()) + let block_end = writer.bytes.len(); + Ok(CompactBlockSizeBreakdown { + total_bytes: block_end.saturating_sub(block_start), + header_and_proof_bytes: header_end.saturating_sub(block_start), + transaction_bytes: burn_bundle_start.saturating_sub(transaction_start), + transfer_bytes, + burn_bytes, + mine_bytes, + burn_bundle_bytes: block_end.saturating_sub(burn_bundle_start), + }) } fn decode_block_body( @@ -933,11 +991,12 @@ mod tests { }; use super::{ - COMPACT_SNAPSHOT_MAGIC, COMPACT_SNAPSHOT_VERSION, CompactReader, CompactWriter, - DecodeTables, EncodeTables, MAX_COMPACT_BYTE_FIELD, MAX_COMPACT_GENESIS_ALLOCATIONS, - MAX_COMPACT_SNAPSHOT_BLOCKS, MAX_COMPACT_VEC_ITEMS, MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION, - decode_block_body, decode_compact_snapshot, decode_launch_profile, decode_transaction, - encode_block_body, encode_compact_snapshot, encode_launch_profile, encode_transaction, + COMPACT_SNAPSHOT_MAGIC, COMPACT_SNAPSHOT_VERSION, CompactBlockContext, CompactReader, + CompactWriter, DecodeTables, EncodeTables, MAX_COMPACT_BYTE_FIELD, + MAX_COMPACT_GENESIS_ALLOCATIONS, MAX_COMPACT_SNAPSHOT_BLOCKS, MAX_COMPACT_VEC_ITEMS, + MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION, decode_block_body, decode_compact_snapshot, + decode_launch_profile, decode_transaction, encode_block_body, encode_compact_snapshot, + encode_launch_profile, encode_transaction, }; #[test] @@ -1150,6 +1209,24 @@ mod tests { let with_bytes = encode(&with); assert_eq!(with_bytes.len() - without_bytes.len(), 2); + let mut context = CompactBlockContext::default(); + let breakdown = context.append_block_with_size_breakdown(&with).unwrap(); + assert_eq!(breakdown.total_bytes, with_bytes.len()); + assert_eq!(breakdown.burn_bundle_bytes, 4); + assert!(breakdown.burn_bytes > 0); + assert_eq!(breakdown.transfer_bytes, 0); + assert_eq!(breakdown.mine_bytes, 0); + assert_eq!( + breakdown.total_bytes, + breakdown.header_and_proof_bytes + + breakdown.transaction_bytes + + breakdown.burn_bundle_bytes + ); + assert_eq!( + context.stored_block_size_breakdown(&with.hash), + Some(&breakdown) + ); + let mut reader = CompactReader::new(&with_bytes); let decoded = decode_block_body( &mut reader, diff --git a/src/adapters/http/api.rs b/src/adapters/http/api.rs @@ -39,13 +39,14 @@ pub(super) async fn api_blocks( .limit .unwrap_or(EXPLORER_PAGE_LIMIT) .min(EXPLORER_LIMIT); - let (tip_hash, blocks) = { + let (tip_hash, blocks, storage_size_breakdowns) = { let node = state.node.lock().await; let blocks = match query.before_height { Some(before_height) => node.blocks_before(before_height, limit), None => node.recent_blocks(limit), }; - (node.chain_tip_hash(), blocks) + let storage_size_breakdowns = node.block_storage_size_breakdowns(&blocks); + (node.chain_tip_hash(), blocks, storage_size_breakdowns) }; let store = state.ui_data_store.clone(); let view = tokio::task::spawn_blocking(move || store.load_ui_chain_index(&tip_hash)) @@ -58,6 +59,7 @@ pub(super) async fn api_blocks( blocks, &view.outputs, &view.burn_leader_ranks_by_hash, + &storage_size_breakdowns, )) } diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs @@ -1140,8 +1140,8 @@ pub(super) const INDEX_HTML: &str = concat!( <div class="detail-kv"><div class="key">Transfers</div><div x-text="blockTransferCount(selectedBlock)"></div></div> <div class="detail-kv"><div class="key">Total Lost</div><div>IUNA <span x-text="amountLabel(blockLostIuna(selectedBlock))"></span></div></div> <div class="detail-kv"> - <div class="key">Bytes</div> - <button class="detail-link" type="button" @click="openBlockBytesModal(selectedBlock)" title="Block byte breakdown"> + <div class="key">Storage</div> + <button class="detail-link" type="button" @click="openBlockBytesModal(selectedBlock)" title="Compact storage byte breakdown"> <span x-text="blockTotalBytes(selectedBlock)"></span>B </button> </div> @@ -1584,7 +1584,7 @@ pub(super) const INDEX_HTML: &str = concat!( <section class="tx-modal"> <div class="tx-modal-head"> <div class="tx-modal-title"> - <h2 id="block-bytes-title" x-text="selectedByteBlock ? `Block ${selectedByteBlock.height} Bytes` : 'Block Bytes'"></h2> + <h2 id="block-bytes-title" x-text="selectedByteBlock ? `Block ${selectedByteBlock.height} Storage` : 'Block Storage'"></h2> <div class="tx-field"><span class="tx-label">Total</span><span class="tx-value number"><span x-text="blockTotalBytes(selectedByteBlock)"></span>B</span></div> </div> <button type="button" @click="closeBlockBytesModal">Close</button> diff --git a/src/adapters/http/types.rs b/src/adapters/http/types.rs @@ -400,6 +400,7 @@ pub(super) struct UiBlock { pub(super) total_fees: Amount, pub(super) lost_iuna: Amount, pub(super) total_bytes: usize, + pub(super) header_and_proof_bytes: usize, pub(super) transaction_bytes: usize, pub(super) transaction_byte_breakdown: Vec<UiByteBreakdown>, pub(super) burn_bundle_bytes: usize, diff --git a/src/adapters/http/ui.rs b/src/adapters/http/ui.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; +use crate::compact::CompactBlockSizeBreakdown; use crate::domain::{ Amount, Block, BurnLeaderRank, FinalizerMode, MINE_REWARD, OutPoint, Transaction, TxInput, TxOutput, @@ -155,10 +156,14 @@ pub(super) fn ui_blocks_from_indexes( blocks: Vec<Block>, outputs: &BTreeMap<OutPoint, TxOutput>, burn_leader_ranks: &BTreeMap<String, Vec<BurnLeaderRank>>, + storage_size_breakdowns: &BTreeMap<String, CompactBlockSizeBreakdown>, ) -> Vec<UiBlock> { blocks .into_iter() - .map(|block| ui_block(block, outputs, burn_leader_ranks)) + .map(|block| { + let storage_size = storage_size_breakdowns.get(&block.hash); + ui_block(block, outputs, burn_leader_ranks, storage_size) + }) .collect() } @@ -166,6 +171,7 @@ pub(super) fn ui_block( block: Block, outputs: &BTreeMap<OutPoint, TxOutput>, burn_leader_ranks: &BTreeMap<String, Vec<BurnLeaderRank>>, + storage_size: Option<&CompactBlockSizeBreakdown>, ) -> UiBlock { let ranks = burn_leader_ranks .get(&block.hash) @@ -177,12 +183,19 @@ pub(super) fn ui_block( .iter() .fold(0_u64, |total, tx| total.saturating_add(tx.fee())); let total_fees = public_fees; - let transaction_bytes = block - .transactions - .iter() - .map(|tx| tx.serialized_size_bytes().unwrap_or_default()) - .sum::<usize>(); - let transaction_byte_breakdown = transaction_byte_breakdown(&block.transactions); + let transaction_bytes = storage_size + .map(|size| size.transaction_bytes) + .unwrap_or_else(|| { + block + .transactions + .iter() + .map(|tx| tx.serialized_size_bytes().unwrap_or_default()) + .sum() + }); + let transaction_byte_breakdown = transaction_byte_breakdown(&block.transactions, storage_size); + let header_and_proof_bytes = storage_size + .map(|size| size.header_and_proof_bytes) + .unwrap_or_default(); let transactions = block .transactions .iter() @@ -204,13 +217,21 @@ pub(super) fn ui_block( .collect(), }) .collect(); - let burn_bundle_bytes = burn_bundles - .iter() - .map(|bundle: &UiBurnBundle| bundle.byte_size) - .sum::<usize>(); - let total_bytes = block - .json_size_bytes() - .unwrap_or_else(|_| transaction_bytes.saturating_add(burn_bundle_bytes)); + let burn_bundle_bytes = storage_size + .map(|size| size.burn_bundle_bytes) + .unwrap_or_else(|| { + burn_bundles + .iter() + .map(|bundle: &UiBurnBundle| bundle.byte_size) + .sum::<usize>() + }); + let total_bytes = storage_size + .map(|size| size.total_bytes) + .unwrap_or_else(|| { + block + .json_size_bytes() + .unwrap_or_else(|_| transaction_bytes.saturating_add(burn_bundle_bytes)) + }); UiBlock { height: block.height, prev_hash: block.prev_hash, @@ -222,6 +243,7 @@ pub(super) fn ui_block( total_fees, lost_iuna: block_lost_iuna(&block.transactions, block.reward), total_bytes, + header_and_proof_bytes, transaction_bytes, transaction_byte_breakdown, burn_bundle_bytes, @@ -275,7 +297,17 @@ fn block_lost_iuna(transactions: &[Transaction], reward: Amount) -> Amount { burned.saturating_add(existing_supply_fees.saturating_sub(returned_fees)) } -fn transaction_byte_breakdown(transactions: &[Transaction]) -> Vec<UiByteBreakdown> { +fn transaction_byte_breakdown( + transactions: &[Transaction], + storage_size: Option<&CompactBlockSizeBreakdown>, +) -> Vec<UiByteBreakdown> { + if let Some(storage_size) = storage_size { + return byte_breakdown_rows( + storage_size.transfer_bytes, + storage_size.burn_bytes, + storage_size.mine_bytes, + ); + } let mut transfer_bytes = 0_usize; let mut burn_bytes = 0_usize; let mut mine_bytes = 0_usize; @@ -287,6 +319,14 @@ fn transaction_byte_breakdown(transactions: &[Transaction]) -> Vec<UiByteBreakdo Transaction::Mine { .. } => mine_bytes = mine_bytes.saturating_add(bytes), } } + byte_breakdown_rows(transfer_bytes, burn_bytes, mine_bytes) +} + +fn byte_breakdown_rows( + transfer_bytes: usize, + burn_bytes: usize, + mine_bytes: usize, +) -> Vec<UiByteBreakdown> { [ ("transfer", transfer_bytes), ("burn", burn_bytes), @@ -447,6 +487,7 @@ fn index_transaction_outputs( mod tests { use std::collections::BTreeMap; + use crate::compact::CompactBlockSizeBreakdown; use crate::domain::{ Amount, Block, BurnBundleSection, BurnLeaderRank, FinalizerMode, MaskedBurn, OutPoint, Transaction, TxInput, TxOutput, @@ -576,7 +617,21 @@ mod tests { hash: "hash".to_string(), }; - let ui = ui_block(block, &BTreeMap::new(), &BTreeMap::new()); + let storage_size = CompactBlockSizeBreakdown { + total_bytes: 120, + header_and_proof_bytes: 10, + transaction_bytes: 80, + transfer_bytes: 0, + burn_bytes: 80, + mine_bytes: 0, + burn_bundle_bytes: 30, + }; + let ui = ui_block( + block, + &BTreeMap::new(), + &BTreeMap::new(), + Some(&storage_size), + ); assert_eq!(ui.burn_bundle_quorum.burn_bundles_included, 2); assert_eq!(ui.burn_bundle_quorum.committee_size, 2); @@ -584,7 +639,10 @@ mod tests { assert_eq!(ui.burn_bundles.len(), 1); assert_eq!(ui.burn_bundles[0].slot, 1); assert_eq!(ui.burn_bundles[0].burns.len(), 1); - assert!(ui.burn_bundle_bytes > 0); + assert_eq!(ui.total_bytes, 120); + assert_eq!(ui.header_and_proof_bytes, 10); + assert_eq!(ui.transaction_bytes, 80); + assert_eq!(ui.burn_bundle_bytes, 30); } #[test] @@ -616,7 +674,7 @@ mod tests { }], )]); - let ui = ui_block(block, &BTreeMap::new(), &ranks); + let ui = ui_block(block, &BTreeMap::new(), &ranks, None); assert_eq!(ui.burn_bundle_quorum.burn_bundles_included, 1); assert_eq!(ui.burn_bundle_quorum.committee_size, 1); @@ -692,7 +750,7 @@ mod tests { ], )]); - let ui = ui_block(block, &BTreeMap::new(), &ranks); + let ui = ui_block(block, &BTreeMap::new(), &ranks, None); assert_eq!(ui.burn_bundle_quorum.burn_bundles_included, 2); assert_eq!(ui.burn_bundle_quorum.committee_size, 2); @@ -760,7 +818,7 @@ mod tests { ], )]); - let ui = ui_block(block, &BTreeMap::new(), &ranks); + let ui = ui_block(block, &BTreeMap::new(), &ranks, None); assert_eq!(ui.burn_bundle_quorum.burn_bundles_included, 2); assert_eq!(ui.burn_bundle_quorum.committee_size, 2); diff --git a/src/app/ledger_view.rs b/src/app/ledger_view.rs @@ -2,7 +2,9 @@ use anyhow::Result; use std::collections::BTreeSet; +use crate::compact::CompactBlockSizeBreakdown; use crate::domain::{Block, BurnLeaderRank, Ledger, OutPoint, Transaction}; +use std::collections::BTreeMap; use super::{NodeCore, helpers::transaction_input_outpoints}; @@ -60,6 +62,13 @@ impl NodeCore { self.ledger.blocks_before(before_height, limit) } + pub(crate) fn block_storage_size_breakdowns( + &self, + blocks: &[Block], + ) -> BTreeMap<String, CompactBlockSizeBreakdown> { + self.ledger.storage_size_breakdowns(blocks) + } + pub fn burn_leader_ranks_for_block(&self, height: u64) -> Result<Vec<BurnLeaderRank>> { self.ledger.burn_leader_ranks_for_block(height) } diff --git a/src/domain/ledger_ops.rs b/src/domain/ledger_ops.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use anyhow::{Context, Result, bail}; -use crate::compact::CompactBlockContext; +use crate::compact::{CompactBlockContext, CompactBlockSizeBreakdown}; use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use super::hex::hex_hash; @@ -27,6 +27,22 @@ impl Ledger { pub(crate) fn consensus_block_size_bytes(&self, block: &Block) -> Result<usize> { self.compact_block_context.block_size_bytes(block) } + + pub(crate) fn storage_size_breakdowns( + &self, + requested_blocks: &[Block], + ) -> BTreeMap<String, CompactBlockSizeBreakdown> { + requested_blocks + .iter() + .filter_map(|block| { + self.compact_block_context + .stored_block_size_breakdown(&block.hash) + .cloned() + .or_else(|| self.compact_block_context.block_size_breakdown(block).ok()) + .map(|breakdown| (block.hash.clone(), breakdown)) + }) + .collect() + } } pub(super) fn validate_genesis_allocations( diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -3119,13 +3119,6 @@ window.iunaApp = function iunaApp() { return Number(block?.totalBytes ?? block?.total_bytes ?? 0); }, - blockPayloadBytes(block) { - return ( - Number(block?.transactionBytes ?? block?.transaction_bytes ?? 0) + - Number(block?.burnBundleBytes ?? block?.burn_bundle_bytes ?? 0) - ); - }, - blockBurnBundleQuorum(block) { return block?.burnBundleQuorum ?? block?.burn_bundle_quorum ?? {}; }, @@ -3165,13 +3158,13 @@ window.iunaApp = function iunaApp() { burnBundleSlotDetail(slot) { if (slot?.implicit) return "Implicit block attestation"; const count = slot?.burns?.length || 0; - return `${count} burn${count === 1 ? "" : "s"} · ${slot?.byteSize || 0}B`; + return `${count} burn${count === 1 ? "" : "s"} · ${slot?.byteSize || 0}B gossip JSON`; }, blockByteBreakdown(block) { const transactionRows = this.blockTransactionByteBreakdown(block); return [ - ["Header and proof", Math.max(0, this.blockTotalBytes(block) - this.blockPayloadBytes(block)), ""], + ["Header and proof", Number(block?.headerAndProofBytes ?? block?.header_and_proof_bytes ?? 0), ""], ...(transactionRows.length ? transactionRows : [["Transactions", Number(block?.transactionBytes ?? block?.transaction_bytes ?? 0), ""]]),