commit d5c7bccf068ee3c79c0f845539aa5b41a95f3d97
parent 0d31c97e55daaffdf460aed41ecf1d35c995d050
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Fri, 21 Aug 2026 20:28:16 +0200
Fix genesis finalization display
Diffstat:
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();
},