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:
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), ""]]),