commit 93e0aa65d53dc7986f99f0f2c4513c82b74d3d9a
parent 15ded2b144f71ce971dd2a037b6d52189663350c
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Fri, 7 Aug 2026 15:36:49 +0200
Count blinded inputs in circulation until reveal
Diffstat:
1 file changed, 259 insertions(+), 6 deletions(-)
diff --git a/src/adapters/chain_store.rs b/src/adapters/chain_store.rs
@@ -1,5 +1,5 @@
use std::{
- collections::BTreeSet,
+ collections::{BTreeMap, BTreeSet},
fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
@@ -13,9 +13,9 @@ use serde::Serialize;
use crate::domain::{
Amount, BLINDED_COMMITTER_FEE_BPS, BLINDED_FEE_BPS_DENOMINATOR,
BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS, BlindedReveal, BlindedTransaction, Block, ChainSnapshot,
- FinalizerMode, LaunchProfile, LeaderProof, Ledger, MaskedBlindedReveal, OutPoint,
+ FinalizerMode, LaunchProfile, LeaderProof, Ledger, MINE_REWARD, MaskedBlindedReveal, OutPoint,
REVEAL_COMMITTEE_SIZE, RevealBundleSection, RevealBundleSignature, Transaction, TxInput,
- TxOutput, blinded_reveal_finalizer_fee, revealed_blinded_transactions,
+ TxOutput, blinded_reveal_finalizer_fee, hex_hash, revealed_blinded_transactions,
};
const SCHEMA: &str = r#"
@@ -936,7 +936,7 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
})
.context("failed to rebuild genesis ledger for metrics")?;
let revealed = revealed_blinded_transactions(snapshot)?.into_iter().fold(
- std::collections::BTreeMap::<u64, Vec<crate::domain::RevealedBlindedTransaction>>::new(),
+ BTreeMap::<u64, Vec<crate::domain::RevealedBlindedTransaction>>::new(),
|mut by_height, revealed| {
by_height.entry(revealed.height).or_default().push(revealed);
by_height
@@ -950,7 +950,9 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
let mut total_burned_amount = 0_u64;
let mut rows = Vec::with_capacity(snapshot.blocks.len());
let mut previous_timestamp_ms = None;
- let mut active_blinded = std::collections::BTreeMap::<String, BlindedTransaction>::new();
+ let mut active_blinded = BTreeMap::<String, BlindedTransaction>::new();
+ let mut metric_utxos = metric_genesis_utxos(snapshot);
+ let mut metric_locked_blinded_inputs = BTreeMap::<String, Amount>::new();
for block in &snapshot.blocks {
let revealed_transactions = revealed.get(&block.height).cloned().unwrap_or_default();
@@ -967,6 +969,7 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
}
for transaction in &block.transactions {
collect_transaction_addresses(transaction, &mut known_wallet_addresses);
+ metric_apply_public_transaction(transaction, &mut metric_utxos)?;
fees_amount = fees_amount
.checked_add(transaction.fee())
.context("block metric fees overflow")?;
@@ -987,6 +990,18 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
let transaction = &revealed.transaction;
known_wallet_addresses.insert(revealed.included_by.clone());
collect_transaction_addresses(transaction, &mut known_wallet_addresses);
+ metric_index_transaction_outputs(&mut metric_utxos, transaction);
+ metric_index_blinded_fee_outputs(
+ &mut metric_utxos,
+ &revealed.commitment,
+ &revealed.included_by,
+ block,
+ transaction.fee(),
+ ledger
+ .burn_leader_ranks_for_block(block.height)
+ .map(|ranks| ranks.len())
+ .unwrap_or(REVEAL_COMMITTEE_SIZE),
+ );
fees_amount = fees_amount
.checked_add(transaction.fee())
.context("block metric fees overflow")?;
@@ -1031,11 +1046,20 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
let mut expired_blinded_fee_values = Vec::new();
active_blinded.retain(|commitment, transaction| {
if revealed_commitments.contains(commitment) {
+ metric_locked_blinded_inputs.remove(commitment);
return false;
}
if block.height >= transaction.expires_at_height {
if !transaction.inputs.is_empty() {
expired_blinded_fee_values.push(transaction.fee);
+ metric_index_expired_blinded_change(
+ &mut metric_utxos,
+ commitment,
+ transaction,
+ metric_locked_blinded_inputs
+ .remove(commitment)
+ .unwrap_or_default(),
+ );
}
return false;
}
@@ -1059,6 +1083,8 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
for input in &transaction.inputs {
known_wallet_addresses.insert(input.owner.clone());
}
+ let locked_total = metric_spend_blinded_inputs(transaction, &mut metric_utxos)?;
+ metric_locked_blinded_inputs.insert(transaction.commitment.clone(), locked_total);
active_blinded.insert(transaction.commitment.clone(), transaction.clone());
}
total_burned_amount = total_burned_amount
@@ -1071,7 +1097,10 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
.apply_preverified_block_at(block.clone(), u64::MAX)
.with_context(|| format!("failed to replay block {} for metrics", block.height))?;
}
- let circulating_supply = ledger_circulating_supply(&running_ledger)?;
+ metric_index_block_reward(&mut metric_utxos, block);
+ let circulating_supply = ledger_circulating_supply(&running_ledger)?
+ .checked_add(metric_locked_supply(&metric_locked_blinded_inputs)?)
+ .context("circulating supply metric overflows")?;
let block_time_ms =
previous_timestamp_ms.map(|previous| block.timestamp_ms.saturating_sub(previous));
previous_timestamp_ms = Some(block.timestamp_ms);
@@ -1110,6 +1139,224 @@ fn ledger_circulating_supply(ledger: &Ledger) -> Result<Amount> {
})
}
+fn metric_locked_supply(locked: &BTreeMap<String, Amount>) -> Result<Amount> {
+ locked.values().try_fold(0_u64, |total, amount| {
+ total
+ .checked_add(*amount)
+ .context("circulating supply metric overflows")
+ })
+}
+
+fn metric_genesis_utxos(snapshot: &ChainSnapshot) -> BTreeMap<OutPoint, TxOutput> {
+ snapshot
+ .genesis_allocations
+ .iter()
+ .filter(|(_, amount)| **amount > 0)
+ .map(|(address, amount)| {
+ (
+ metric_genesis_allocation_outpoint(address),
+ TxOutput {
+ address: address.clone(),
+ amount: *amount,
+ },
+ )
+ })
+ .collect()
+}
+
+fn metric_apply_public_transaction(
+ transaction: &Transaction,
+ utxos: &mut BTreeMap<OutPoint, TxOutput>,
+) -> Result<()> {
+ metric_spend_transaction_inputs(transaction, utxos)?;
+ metric_index_transaction_outputs(utxos, transaction);
+ Ok(())
+}
+
+fn metric_spend_transaction_inputs(
+ transaction: &Transaction,
+ utxos: &mut BTreeMap<OutPoint, TxOutput>,
+) -> Result<Amount> {
+ let inputs = match transaction {
+ Transaction::Transfer { inputs, .. } | Transaction::Burn { inputs, .. } => inputs,
+ Transaction::Mine { .. } => return Ok(0),
+ };
+ metric_spend_inputs(inputs, utxos)
+}
+
+fn metric_spend_blinded_inputs(
+ transaction: &BlindedTransaction,
+ utxos: &mut BTreeMap<OutPoint, TxOutput>,
+) -> Result<Amount> {
+ metric_spend_inputs(&transaction.inputs, utxos)
+}
+
+fn metric_spend_inputs(
+ inputs: &[TxInput],
+ utxos: &mut BTreeMap<OutPoint, TxOutput>,
+) -> Result<Amount> {
+ inputs.iter().try_fold(0_u64, |total, input| {
+ let output = utxos.remove(&input.outpoint).with_context(|| {
+ format!(
+ "metric replay spends missing output {}:{}",
+ input.outpoint.txid, input.outpoint.index
+ )
+ })?;
+ total
+ .checked_add(output.amount)
+ .context("metric replay input total overflows")
+ })
+}
+
+fn metric_index_transaction_outputs(
+ utxos: &mut BTreeMap<OutPoint, TxOutput>,
+ transaction: &Transaction,
+) {
+ let outputs = match transaction {
+ Transaction::Transfer { outputs, .. } => outputs.clone(),
+ Transaction::Burn { change, .. } => change.clone(),
+ Transaction::Mine { recipient, .. } => vec![TxOutput {
+ address: recipient.clone(),
+ amount: MINE_REWARD,
+ }],
+ };
+ for (index, output) in outputs.iter().enumerate() {
+ utxos.insert(
+ OutPoint {
+ txid: transaction.signature().to_string(),
+ index: index as u32,
+ },
+ output.clone(),
+ );
+ }
+}
+
+fn metric_index_blinded_fee_outputs(
+ utxos: &mut BTreeMap<OutPoint, TxOutput>,
+ commitment: &str,
+ included_by: &str,
+ block: &Block,
+ fee: Amount,
+ available_reveal_bundle_slots: usize,
+) {
+ if fee == 0 {
+ return;
+ }
+ let committer_fee = blinded_fee_share(fee, BLINDED_COMMITTER_FEE_BPS);
+ if committer_fee > 0 {
+ utxos.insert(
+ metric_blinded_committer_fee_outpoint(commitment),
+ TxOutput {
+ address: included_by.to_string(),
+ amount: committer_fee,
+ },
+ );
+ }
+ let reveal_finalizer_fee = blinded_reveal_finalizer_fee(
+ fee,
+ block.included_reveal_bundle_count(),
+ available_reveal_bundle_slots,
+ );
+ if reveal_finalizer_fee > 0 {
+ utxos.insert(
+ metric_blinded_executor_fee_outpoint(commitment),
+ TxOutput {
+ address: block.miner.clone(),
+ amount: reveal_finalizer_fee,
+ },
+ );
+ }
+ let reveal_bundle_signer_fee = blinded_fee_share(fee, BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS);
+ if reveal_bundle_signer_fee > 0 {
+ for signature in &block.reveal_bundle_section.signatures {
+ utxos.insert(
+ metric_blinded_reveal_bundle_signer_fee_outpoint(commitment, signature.slot),
+ TxOutput {
+ address: signature.member.clone(),
+ amount: reveal_bundle_signer_fee,
+ },
+ );
+ }
+ }
+}
+
+fn metric_index_expired_blinded_change(
+ utxos: &mut BTreeMap<OutPoint, TxOutput>,
+ commitment: &str,
+ transaction: &BlindedTransaction,
+ locked_total: Amount,
+) {
+ let Some(first_input) = transaction.inputs.first() else {
+ return;
+ };
+ let change = locked_total.saturating_sub(transaction.fee);
+ if change == 0 {
+ return;
+ }
+ utxos.insert(
+ metric_blinded_expiry_change_outpoint(commitment),
+ TxOutput {
+ address: first_input.owner.clone(),
+ amount: change,
+ },
+ );
+}
+
+fn metric_index_block_reward(utxos: &mut BTreeMap<OutPoint, TxOutput>, block: &Block) {
+ if block.reward == 0 {
+ return;
+ }
+ utxos.insert(
+ metric_reward_outpoint(&block.hash),
+ TxOutput {
+ address: block.miner.clone(),
+ amount: block.reward,
+ },
+ );
+}
+
+fn metric_genesis_allocation_outpoint(address: &str) -> OutPoint {
+ OutPoint {
+ txid: hex_hash(format!("iuna-genesis-allocation:{address}")),
+ index: 0,
+ }
+}
+
+fn metric_reward_outpoint(block_hash: &str) -> OutPoint {
+ OutPoint {
+ txid: block_hash.to_string(),
+ index: u32::MAX,
+ }
+}
+
+fn metric_blinded_committer_fee_outpoint(commitment: &str) -> OutPoint {
+ OutPoint {
+ txid: commitment.to_string(),
+ index: u32::MAX - 1,
+ }
+}
+
+fn metric_blinded_executor_fee_outpoint(commitment: &str) -> OutPoint {
+ OutPoint {
+ txid: commitment.to_string(),
+ index: u32::MAX - 2,
+ }
+}
+
+fn metric_blinded_reveal_bundle_signer_fee_outpoint(commitment: &str, slot: u8) -> OutPoint {
+ OutPoint {
+ txid: commitment.to_string(),
+ index: u32::MAX - 3 - u32::from(slot),
+ }
+}
+
+fn metric_blinded_expiry_change_outpoint(commitment: &str) -> OutPoint {
+ OutPoint {
+ txid: commitment.to_string(),
+ index: 0,
+ }
+}
+
fn collect_transaction_addresses(transaction: &Transaction, addresses: &mut BTreeSet<String>) {
match transaction {
Transaction::Transfer {
@@ -1471,6 +1718,7 @@ CREATE TABLE block_metrics (
ledger.submit_transaction(burn).unwrap();
let block = ledger.mine_next_block(wallet, 1).unwrap();
ledger.apply_locally_mined_block(block).unwrap();
+ let supply_after_commit = ledger.status().balances.values().copied().sum::<u64>();
ledger.submit_blinded_reveal(blinded.reveal).unwrap();
let leader = ledger.expected_leader_for_next_block().unwrap();
let wallet = wallets
@@ -1501,9 +1749,14 @@ CREATE TABLE block_metrics (
store.save_with_metrics(&ledger.snapshot(), true).unwrap();
let metrics = store.load_metrics().unwrap();
+ let commit = metrics
+ .iter()
+ .find(|metric| metric.height == 1)
+ .expect("commit block metrics should exist");
let last = metrics.last().unwrap();
let supply_from_balances = ledger.status().balances.values().copied().sum::<u64>();
+ assert_eq!(commit.circulating_supply, supply_after_commit + 10_000_000);
assert_eq!(last.burn_count, 2);
assert_eq!(last.burned_amount, 4);
assert_eq!(last.fees_amount, 7);