iuna

iuna

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

commit 4626fa644b154600fb532dd145bb0ab9fe3835b0
parent 140520595896dbd3869e56166e834810578feca4
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Thu, 24 Sep 2026 22:45:44 +0200

feat(metrics): track total UTXOs

Diffstat:
Msrc/adapters/http/index_html.rs | 1+
Msrc/adapters/http/metrics.rs | 18++++++++++++++++++
Msrc/adapters/ui_data_store.rs | 140++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
3 files changed, 119 insertions(+), 40 deletions(-)

diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs @@ -1325,6 +1325,7 @@ pub(super) const INDEX_HTML: &str = concat!( <div class="metric"><div class="label">Latest block</div><div class="value" x-text="metricsLatest().height ?? '-'"></div></div> <div class="metric"><div class="label">Supply</div><div class="value" x-text="metricAmountLabel(metricsLatest().circulatingSupply)"></div></div> <div class="metric"><div class="label">Known addresses</div><div class="value" x-text="metricsLatest().knownWalletAddresses ?? '-'"></div></div> + <div class="metric"><div class="label">Total UTXOs</div><div class="value" x-text="metricsLatest().utxoCount ?? '-'"></div></div> <div class="metric"><div class="label">Total burned</div><div class="value" x-text="metricAmountLabel(metricsLatest().totalBurnedAmount)"></div></div> <div class="metric"><div class="label">Difficulty</div><div class="value" x-text="metricsLatest().mineDifficultyBits ?? '-'"></div></div> </div> diff --git a/src/adapters/http/metrics.rs b/src/adapters/http/metrics.rs @@ -89,6 +89,14 @@ pub(super) fn metrics_response( |row| Some(row.known_wallet_addresses as f64), ), metrics_chart( + "total-utxos", + "Total UTXOs", + "UTXOs", + MetricsValueKind::Number, + &rows, + |row| Some(row.utxo_count as f64), + ), + metrics_chart( "transactions", "Transactions", "tx", @@ -395,6 +403,7 @@ mod tests { mine_difficulty_bits: 10, circulating_supply: 100, known_wallet_addresses: 2, + utxo_count: 4, transaction_count: 1, transfer_count: 0, burn_count: 1, @@ -429,6 +438,15 @@ mod tests { .map(|point| point.value), Some(12_345.0) ); + assert_eq!( + response + .charts + .iter() + .find(|chart| chart.id == "total-utxos") + .and_then(|chart| chart.points.first()) + .map(|point| point.value), + Some(4.0) + ); } #[test] diff --git a/src/adapters/ui_data_store.rs b/src/adapters/ui_data_store.rs @@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS block_metrics ( mine_difficulty_bits INTEGER NOT NULL, circulating_supply INTEGER NOT NULL, known_wallet_addresses INTEGER NOT NULL DEFAULT 0, + utxo_count INTEGER NOT NULL DEFAULT 0, transaction_count INTEGER NOT NULL, transfer_count INTEGER NOT NULL, burn_count INTEGER NOT NULL, @@ -163,7 +164,7 @@ DROP TABLE IF EXISTS ui_burn_leader_rank_blocks; const UI_DATA_SCHEMA_VERSION: u32 = 2; const UI_CACHE_SCHEMA_VERSION: u32 = 5; -const METRICS_CACHE_SCHEMA_VERSION: u32 = 1; +const METRICS_CACHE_SCHEMA_VERSION: u32 = 2; #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] @@ -175,6 +176,7 @@ pub struct BlockMetricRow { pub mine_difficulty_bits: u32, pub circulating_supply: Amount, pub known_wallet_addresses: u64, + pub utxo_count: u64, pub transaction_count: u64, pub transfer_count: u64, pub burn_count: u64, @@ -422,7 +424,7 @@ impl SqliteUiDataStore { .prepare( r#" SELECT height, block_hash, timestamp_ms, block_time_ms, mine_difficulty_bits, - circulating_supply, known_wallet_addresses, transaction_count, transfer_count, burn_count, + circulating_supply, known_wallet_addresses, utxo_count, transaction_count, transfer_count, burn_count, mine_count, burned_amount, total_burned_amount, fees_amount, reward_amount, vdf_rounds, finalizer_rank FROM block_metrics @@ -440,16 +442,17 @@ ORDER BY height ASC mine_difficulty_bits: row.get(4)?, circulating_supply: row.get(5)?, known_wallet_addresses: row.get(6)?, - transaction_count: row.get(7)?, - transfer_count: row.get(8)?, - burn_count: row.get(9)?, - mine_count: row.get(10)?, - burned_amount: row.get(11)?, - total_burned_amount: row.get(12)?, - fees_amount: row.get(13)?, - reward_amount: row.get(14)?, - vdf_rounds: row.get(15)?, - finalizer_rank: row.get(16)?, + utxo_count: row.get(7)?, + transaction_count: row.get(8)?, + transfer_count: row.get(9)?, + burn_count: row.get(10)?, + mine_count: row.get(11)?, + burned_amount: row.get(12)?, + total_burned_amount: row.get(13)?, + fees_amount: row.get(14)?, + reward_amount: row.get(15)?, + vdf_rounds: row.get(16)?, + finalizer_rank: row.get(17)?, }) }) .context("failed to load block metrics")?; @@ -464,7 +467,7 @@ ORDER BY height ASC .prepare( r#" SELECT height, block_hash, timestamp_ms, block_time_ms, mine_difficulty_bits, - circulating_supply, known_wallet_addresses, transaction_count, transfer_count, burn_count, + circulating_supply, known_wallet_addresses, utxo_count, transaction_count, transfer_count, burn_count, mine_count, burned_amount, total_burned_amount, fees_amount, reward_amount, vdf_rounds, finalizer_rank FROM block_metrics @@ -483,16 +486,17 @@ LIMIT ?1 mine_difficulty_bits: row.get(4)?, circulating_supply: row.get(5)?, known_wallet_addresses: row.get(6)?, - transaction_count: row.get(7)?, - transfer_count: row.get(8)?, - burn_count: row.get(9)?, - mine_count: row.get(10)?, - burned_amount: row.get(11)?, - total_burned_amount: row.get(12)?, - fees_amount: row.get(13)?, - reward_amount: row.get(14)?, - vdf_rounds: row.get(15)?, - finalizer_rank: row.get(16)?, + utxo_count: row.get(7)?, + transaction_count: row.get(8)?, + transfer_count: row.get(9)?, + burn_count: row.get(10)?, + mine_count: row.get(11)?, + burned_amount: row.get(12)?, + total_burned_amount: row.get(13)?, + fees_amount: row.get(14)?, + reward_amount: row.get(15)?, + vdf_rounds: row.get(16)?, + finalizer_rank: row.get(17)?, }) }) .context("failed to load recent block metrics")?; @@ -658,6 +662,7 @@ fn initialize_ui_data_schema(connection: &mut Connection, path: &Path) -> Result "known_wallet_addresses", "INTEGER NOT NULL DEFAULT 0", )?; + ensure_block_metrics_column(connection, "utxo_count", "INTEGER NOT NULL DEFAULT 0")?; connection .pragma_update(None, "user_version", UI_DATA_SCHEMA_VERSION) .context("failed to record UI data database schema version")?; @@ -866,7 +871,7 @@ fn load_metric_at_height( .query_row( r#" SELECT height, block_hash, timestamp_ms, block_time_ms, mine_difficulty_bits, - circulating_supply, known_wallet_addresses, transaction_count, transfer_count, burn_count, + circulating_supply, known_wallet_addresses, utxo_count, transaction_count, transfer_count, burn_count, mine_count, burned_amount, total_burned_amount, fees_amount, reward_amount, vdf_rounds, finalizer_rank FROM block_metrics @@ -882,16 +887,17 @@ WHERE height = ?1 mine_difficulty_bits: row.get(4)?, circulating_supply: row.get(5)?, known_wallet_addresses: row.get(6)?, - transaction_count: row.get(7)?, - transfer_count: row.get(8)?, - burn_count: row.get(9)?, - mine_count: row.get(10)?, - burned_amount: row.get(11)?, - total_burned_amount: row.get(12)?, - fees_amount: row.get(13)?, - reward_amount: row.get(14)?, - vdf_rounds: row.get(15)?, - finalizer_rank: row.get(16)?, + utxo_count: row.get(7)?, + transaction_count: row.get(8)?, + transfer_count: row.get(9)?, + burn_count: row.get(10)?, + mine_count: row.get(11)?, + burned_amount: row.get(12)?, + total_burned_amount: row.get(13)?, + fees_amount: row.get(14)?, + reward_amount: row.get(15)?, + vdf_rounds: row.get(16)?, + finalizer_rank: row.get(17)?, }) }, ) @@ -905,10 +911,10 @@ fn insert_metric(transaction: &rusqlite::Transaction<'_>, metric: &BlockMetricRo r#" INSERT INTO block_metrics ( height, block_hash, timestamp_ms, block_time_ms, mine_difficulty_bits, - circulating_supply, known_wallet_addresses, transaction_count, transfer_count, burn_count, + circulating_supply, known_wallet_addresses, utxo_count, transaction_count, transfer_count, burn_count, mine_count, burned_amount, total_burned_amount, fees_amount, reward_amount, vdf_rounds, finalizer_rank -) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17) +) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18) "#, params![ metric.height, @@ -918,6 +924,7 @@ INSERT INTO block_metrics ( metric.mine_difficulty_bits, metric.circulating_supply, metric.known_wallet_addresses, + metric.utxo_count, metric.transaction_count, metric.transfer_count, metric.burn_count, @@ -2009,20 +2016,40 @@ fn incremental_metric_for_block( let mut mine_count = 0_u64; let mut burned_amount = 0_u64; let mut fees_amount = 0_u64; + let mut utxo_count = previous.map(|row| row.utxo_count).unwrap_or_else(|| { + snapshot + .genesis_allocations + .values() + .filter(|amount| **amount > 0) + .count() as u64 + }); for transaction in &block.transactions { fees_amount = fees_amount .checked_add(transaction.fee()) .context("block metric fees overflow")?; match transaction { - Transaction::Transfer { fee, .. } => { + Transaction::Transfer { + inputs, + outputs, + fee, + .. + } => { transfer_count += 1; + apply_utxo_count_delta(&mut utxo_count, inputs.len(), outputs.len())?; circulating_supply = circulating_supply .checked_sub(*fee) .context("transfer fee exceeds circulating supply")?; } - Transaction::Burn { amount, fee, .. } => { + Transaction::Burn { + inputs, + change, + amount, + fee, + .. + } => { burn_count += 1; + apply_utxo_count_delta(&mut utxo_count, inputs.len(), change.len())?; burned_amount = burned_amount .checked_add(*amount) .context("block metric burns overflow")?; @@ -2033,6 +2060,7 @@ fn incremental_metric_for_block( } Transaction::Mine { .. } => { mine_count += 1; + utxo_count = utxo_count.checked_add(1).context("UTXO count overflows")?; circulating_supply = circulating_supply .checked_add(MINE_REWARD) .context("mine reward circulating supply overflows")?; @@ -2049,11 +2077,26 @@ fn incremental_metric_for_block( .checked_sub(transaction.fee()) .context("transaction v2 fee exceeds circulating supply")?; match transaction { - TransactionV2::Migration { .. } | TransactionV2::Transfer { .. } => { + TransactionV2::Migration { + inputs, outputs, .. + } => { transfer_count += 1; + apply_utxo_count_delta(&mut utxo_count, inputs.len(), outputs.len())?; } - TransactionV2::Burn { amount, .. } => { + TransactionV2::Transfer { + inputs, outputs, .. + } => { + transfer_count += 1; + apply_utxo_count_delta(&mut utxo_count, inputs.len(), outputs.len())?; + } + TransactionV2::Burn { + inputs, + change, + amount, + .. + } => { burn_count += 1; + apply_utxo_count_delta(&mut utxo_count, inputs.len(), change.len())?; burned_amount = burned_amount .checked_add(amount) .context("block metric transaction v2 burns overflow")?; @@ -2069,6 +2112,9 @@ fn incremental_metric_for_block( circulating_supply = circulating_supply .checked_add(block.reward) .context("block reward circulating supply overflows")?; + utxo_count = utxo_count + .checked_add(projected_reward_outputs(block).len() as u64) + .context("UTXO count overflows")?; let total_burned_amount = previous .map(|row| row.total_burned_amount) .unwrap_or_default() @@ -2083,6 +2129,7 @@ fn incremental_metric_for_block( mine_difficulty_bits: metric_difficulty_for_block(snapshot, block, previous), circulating_supply, known_wallet_addresses: 0, + utxo_count, transaction_count: block .transactions .len() @@ -2099,6 +2146,15 @@ fn incremental_metric_for_block( }) } +fn apply_utxo_count_delta(count: &mut u64, spent: usize, created: usize) -> Result<()> { + *count = count + .checked_sub(spent as u64) + .context("transaction spends more UTXOs than exist")? + .checked_add(created as u64) + .context("UTXO count overflows")?; + Ok(()) +} + fn metric_difficulty_for_block( snapshot: &ChainSnapshot, block: &Block, @@ -2452,6 +2508,10 @@ END; assert_eq!(metrics.len(), snapshot.blocks.len()); assert_eq!(metrics.last().unwrap().block_hash, ledger.tip_hash()); assert_eq!( + metrics.last().unwrap().utxo_count, + ledger.all_utxos().len() as u64 + ); + assert_eq!( metrics.last().unwrap().circulating_supply, ledger .all_utxos()