iuna

iuna

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

commit d5c7bccf068ee3c79c0f845539aa5b41a95f3d97
parent 0d31c97e55daaffdf460aed41ecf1d35c995d050
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Fri, 21 Aug 2026 20:28:16 +0200

Fix genesis finalization display

Diffstat:
Msrc/adapters/http/ui.rs | 56++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/app/automatic_mining.rs | 60+++++++++++++++++++++++++++++++++++++++++-------------------
Mwww/assets/iuna-ui.js | 3++-
3 files changed, 93 insertions(+), 26 deletions(-)

diff --git a/src/adapters/http/ui.rs b/src/adapters/http/ui.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; use crate::domain::{ - Amount, Block, BurnLeaderRank, ChainSnapshot, MINE_REWARD, OutPoint, Transaction, TxInput, - TxOutput, + Amount, Block, BurnLeaderRank, ChainSnapshot, FinalizerMode, MINE_REWARD, OutPoint, + Transaction, TxInput, TxOutput, }; use crate::adapters::ui_index::build_ui_chain_index; @@ -176,7 +176,8 @@ pub(super) fn ui_block( .cloned() .unwrap_or_default(); let burn_bundles_included = block.included_burn_bundle_count(); - let committee_size = ranks.len(); + let burn_bundles_required = + explicit_burn_bundle_signatures_required(&block, burn_bundles_included); let public_fees = block .transactions .iter() @@ -232,7 +233,7 @@ pub(super) fn ui_block( burn_bundle_bytes, burn_bundle_quorum: UiBurnBundleQuorum { burn_bundles_included, - committee_size, + committee_size: burn_bundles_required, }, vdf_rounds: block.vdf_rounds, vdf_output: block.vdf_output, @@ -244,6 +245,13 @@ pub(super) fn ui_block( } } +fn explicit_burn_bundle_signatures_required(block: &Block, burn_bundles_included: usize) -> usize { + match block.finalizer_mode { + FinalizerMode::Ticket if block.finalizer_rank <= 1 => burn_bundles_included, + FinalizerMode::Ticket | FinalizerMode::Recovery => 0, + } +} + fn block_lost_iuna(transactions: &[Transaction], reward: Amount) -> Amount { let mut burned = 0_u64; let mut existing_supply_fees = 0_u64; @@ -500,8 +508,8 @@ mod tests { use std::collections::BTreeMap; use crate::domain::{ - Amount, Block, BurnBundleSection, FinalizerMode, MaskedBurn, OutPoint, Transaction, - TxInput, TxOutput, + Amount, Block, BurnBundleSection, BurnLeaderRank, FinalizerMode, MaskedBurn, OutPoint, + Transaction, TxInput, TxOutput, }; use super::{block_lost_iuna, ui_block}; @@ -629,10 +637,46 @@ mod tests { let ui = ui_block(block, &BTreeMap::new(), &BTreeMap::new()); assert_eq!(ui.burn_bundle_quorum.burn_bundles_included, 1); + assert_eq!(ui.burn_bundle_quorum.committee_size, 1); assert_eq!(ui.lost_iuna, 1); 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); } + + #[test] + fn ui_block_does_not_count_implicit_finalizer_attestation_as_missing_bundle() { + let block = Block { + height: 1, + prev_hash: "parent".to_string(), + timestamp_ms: 1, + miner: "finalizer".to_string(), + finalizer_mode: FinalizerMode::Ticket, + finalizer_rank: 0, + reward: 1, + vdf_rounds: 1, + vdf_output: "vdf".to_string(), + leader_proof: None, + burn_bundle_section: BurnBundleSection::default(), + transactions: vec![burn("burn-a")], + hash: "hash".to_string(), + }; + let ranks = BTreeMap::from([( + "hash".to_string(), + vec![BurnLeaderRank { + rank: 0, + ticket_id: "ticket".to_string(), + owner: "finalizer".to_string(), + amount: 1, + eligible_from_height: 1, + eligible_until_height: 1, + }], + )]); + + let ui = ui_block(block, &BTreeMap::new(), &ranks); + + assert_eq!(ui.burn_bundle_quorum.burn_bundles_included, 0); + assert_eq!(ui.burn_bundle_quorum.committee_size, 0); + } } diff --git a/src/app/automatic_mining.rs b/src/app/automatic_mining.rs @@ -92,9 +92,9 @@ impl NodeCore { let wallet_rank = self .ledger .finalizer_rank_for_next_block(self.wallet.address()); - let will_run_recovery_vdf = self.should_prepare_recovery_vdf(timestamp_ms); - let will_run_ticket_vdf = !will_run_recovery_vdf - && wallet_rank.is_some_and(|rank| self.wallet_rank_runs_vdf(rank)); + let will_run_ticket_vdf = wallet_rank.is_some_and(|rank| self.wallet_rank_runs_vdf(rank)); + let will_run_recovery_vdf = + !will_run_ticket_vdf && self.should_prepare_recovery_vdf(timestamp_ms); if let Some(wait_ms) = self.burn_bundle_collection_wait_ms( timestamp_ms, will_run_ticket_vdf || will_run_recovery_vdf, @@ -183,9 +183,9 @@ impl NodeCore { let wallet_rank = self .ledger .finalizer_rank_for_next_block(self.wallet.address()); - let will_run_recovery_vdf = self.should_prepare_recovery_vdf(timestamp_ms); - let will_run_ticket_vdf = !will_run_recovery_vdf - && wallet_rank.is_some_and(|rank| self.wallet_rank_runs_vdf(rank)); + let will_run_ticket_vdf = wallet_rank.is_some_and(|rank| self.wallet_rank_runs_vdf(rank)); + let will_run_recovery_vdf = + !will_run_ticket_vdf && self.should_prepare_recovery_vdf(timestamp_ms); if let Some(wait_ms) = self.burn_bundle_collection_wait_ms( timestamp_ms, will_run_ticket_vdf || will_run_recovery_vdf, @@ -439,15 +439,18 @@ impl NodeCore { let next_height = self.ledger.height().saturating_add(1); let (attestation_ledger, _) = self.ledger_with_local_block_anchor(); let explicit_signatures_required = if will_run_vdf { - let finalizer_mode = if self.should_prepare_recovery_vdf(timestamp_ms) { - FinalizerMode::Recovery - } else { - FinalizerMode::Ticket - }; + let wallet_rank = + attestation_ledger.finalizer_rank_for_next_block(self.wallet.address()); + let will_run_ticket_vdf = + wallet_rank.is_some_and(|rank| self.wallet_rank_runs_vdf(rank)); + let finalizer_mode = + if !will_run_ticket_vdf && self.should_prepare_recovery_vdf(timestamp_ms) { + FinalizerMode::Recovery + } else { + FinalizerMode::Ticket + }; let finalizer_rank = if matches!(finalizer_mode, FinalizerMode::Ticket) { - attestation_ledger - .finalizer_rank_for_next_block(self.wallet.address()) - .unwrap_or(0) + wallet_rank.unwrap_or(0) } else { 0 }; @@ -482,10 +485,7 @@ impl NodeCore { .then(|| BURN_BUNDLE_COLLECTION_MS.saturating_sub(elapsed)) } - fn request_missing_burn_bundles_for_next_block(&mut self, timestamp_ms: u64) { - if self.should_prepare_recovery_vdf(timestamp_ms) { - return; - } + fn request_missing_burn_bundles_for_next_block(&mut self, _timestamp_ms: u64) { let (attestation_ledger, _) = self.ledger_with_local_block_anchor(); let Some(finalizer_rank) = attestation_ledger.finalizer_rank_for_next_block(self.wallet.address()) @@ -618,7 +618,8 @@ mod tests { adapters::chain_store::SqliteChainStore, app::{GossipEnvelope, InMemoryNetwork}, domain::{ - BurnBundle, BurnCommitteeMember, GenesisBurn, Ledger, MICRO_IUNA, Wallet, run_vdf, + BurnBundle, BurnCommitteeMember, FinalizerMode, GenesisBurn, Ledger, MICRO_IUNA, + Wallet, run_vdf, }, }; use tempfile::tempdir; @@ -656,6 +657,27 @@ mod tests { } #[test] + fn automatic_mining_prefers_runnable_ticket_over_available_recovery() { + let wallet = Wallet::from_seed("ticket-before-recovery-wallet"); + let ledger = funded_ledger(std::slice::from_ref(&wallet)); + assert_eq!( + ledger.finalizer_rank_for_next_block(wallet.address()), + Some(0) + ); + let timestamp_ms = ledger.recovery_block_min_timestamp(); + let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled(wallet, ledger, true, 0, 1); + + let outcome = node.automatic_mine_once(timestamp_ms); + let block = outcome + .block + .expect("rank 0 ticket finalizer should produce a block"); + + assert_eq!(block.finalizer_mode, FinalizerMode::Ticket); + assert_eq!(block.finalizer_rank, 0); + assert!(block.leader_proof.is_some()); + } + + #[test] fn same_slot_bundles_for_different_members_do_not_conflict_locally() { let wallet = Wallet::from_seed("rank-bundle-cache-node"); let ledger = funded_ledger(std::slice::from_ref(&wallet)); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -2781,7 +2781,8 @@ window.iunaApp = function iunaApp() { blockTimestampLabel(block) { const timestamp = Number(block?.timestamp_ms ?? block?.timestampMs); - if (!Number.isFinite(timestamp)) return "-"; + if (Number(block?.height) === 0) return "Genesis"; + if (!Number.isFinite(timestamp) || timestamp <= 0) return "-"; return new Date(timestamp).toLocaleString(); },