iuna

iuna

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

commit 3fff0452022fc2802fe7befd12b2741fb2eeec9b
parent ff48f2f9df644012356b27601362cd017285c496
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Fri, 21 Aug 2026 22:54:59 +0200

Fix testnet burn defaults and bundle quorum UI

Diffstat:
Msrc/adapters/config_store.rs | 6+++---
Msrc/adapters/http/ui.rs | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/app.rs | 2+-
Msrc/app/automatic_mining.rs | 7+++++++
Msrc/main.rs | 3++-
Msrc/main_tests.rs | 20+++++++++++++++-----
6 files changed, 205 insertions(+), 23 deletions(-)

diff --git a/src/adapters/config_store.rs b/src/adapters/config_store.rs @@ -16,8 +16,8 @@ use crate::domain::{Amount, MICRO_IUNA}; const CONFIG_FILE_VERSION: u32 = 1; const AMOUNT_UNIT_MICROIUNA: &str = "microiuna"; const LEGACY_AMOUNT_UNIT_PRE_RENAME: &str = concat!("micro", "l", "uun"); -pub const DEFAULT_BURN_AMOUNT: Amount = MICRO_IUNA / 10_000; -pub const DEFAULT_BURN_FEE: Amount = DEFAULT_BURN_AMOUNT; +pub const DEFAULT_BURN_AMOUNT: Amount = MICRO_IUNA; +pub const DEFAULT_BURN_FEE: Amount = MICRO_IUNA / 10_000; pub const DEFAULT_RECOVERY_VDF_TOP_RANK_PERCENT: u8 = 50; pub const DEFAULT_POW_MINING_WORKERS: u8 = 1; pub const MAX_POW_MINING_WORKERS: u8 = 32; @@ -334,7 +334,7 @@ mod tests { assert!(stored.contains("\"mining_enabled\": false")); assert!(stored.contains("\"pow_mining_enabled\": false")); assert!(stored.contains("\"pow_mining_workers\": 1")); - assert!(stored.contains("\"burn_per_block\": 100")); + assert!(stored.contains("\"burn_per_block\": 1000000")); assert!(stored.contains("\"burn_fee\": 100")); assert!(!stored.contains("\"pow_mine_fee\"")); assert!(!stored.contains("required_burn")); diff --git a/src/adapters/http/ui.rs b/src/adapters/http/ui.rs @@ -1,8 +1,8 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use crate::domain::{ - Amount, Block, BurnLeaderRank, ChainSnapshot, FinalizerMode, MINE_REWARD, OutPoint, - Transaction, TxInput, TxOutput, + Amount, BURN_COMMITTEE_SIZE, Block, BurnLeaderRank, ChainSnapshot, FinalizerMode, MINE_REWARD, + OutPoint, Transaction, TxInput, TxOutput, }; use crate::adapters::ui_index::build_ui_chain_index; @@ -175,9 +175,7 @@ pub(super) fn ui_block( .get(&block.hash) .cloned() .unwrap_or_default(); - let burn_bundles_included = block.included_burn_bundle_count(); - let burn_bundles_required = - explicit_burn_bundle_signatures_required(&block, burn_bundles_included); + let (burn_bundles_included, burn_bundles_required) = burn_bundle_wallet_quorum(&block, &ranks); let public_fees = block .transactions .iter() @@ -245,11 +243,33 @@ 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 burn_bundle_wallet_quorum(block: &Block, ranks: &[BurnLeaderRank]) -> (usize, usize) { + if block.finalizer_mode != FinalizerMode::Ticket { + return (0, 0); } + + let owner_count = ranks + .iter() + .filter(|rank| rank.rank >= block.finalizer_rank) + .map(|rank| rank.owner.as_str()) + .chain(std::iter::once(block.miner.as_str())) + .collect::<BTreeSet<_>>() + .len(); + let rank_committee_cap = BURN_COMMITTEE_SIZE + .saturating_sub(block.finalizer_rank as usize) + .max(1); + let committee_size = owner_count.min(rank_committee_cap); + let included = block + .burn_bundle_section + .signatures + .iter() + .map(|signature| signature.member.as_str()) + .chain(std::iter::once(block.miner.as_str())) + .collect::<BTreeSet<_>>() + .len() + .min(committee_size); + + (included, committee_size) } fn block_lost_iuna(transactions: &[Transaction], reward: Amount) -> Amount { @@ -646,7 +666,7 @@ mod tests { } #[test] - fn ui_block_does_not_count_implicit_finalizer_attestation_as_missing_bundle() { + fn ui_block_counts_implicit_finalizer_attestation_in_wallet_quorum() { let block = Block { height: 1, prev_hash: "parent".to_string(), @@ -676,7 +696,151 @@ mod tests { 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); + assert_eq!(ui.burn_bundle_quorum.burn_bundles_included, 1); + assert_eq!(ui.burn_bundle_quorum.committee_size, 1); + } + + #[test] + fn ui_block_burn_bundle_quorum_counts_unique_ticket_wallets() { + 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 { + signatures: vec![crate::domain::BurnBundleSignature { + slot: 1, + member: "member-1".to_string(), + signature: "sig-1".to_string(), + }], + burns: Vec::new(), + }, + transactions: vec![burn("burn-a")], + hash: "hash".to_string(), + }; + let ranks = BTreeMap::from([( + "hash".to_string(), + vec![ + BurnLeaderRank { + rank: 0, + ticket_id: "ticket-0".to_string(), + owner: "finalizer".to_string(), + amount: 1, + eligible_from_height: 1, + eligible_until_height: 1, + }, + BurnLeaderRank { + rank: 1, + ticket_id: "ticket-1".to_string(), + owner: "member-1".to_string(), + amount: 1, + eligible_from_height: 1, + eligible_until_height: 1, + }, + BurnLeaderRank { + rank: 2, + ticket_id: "ticket-2".to_string(), + owner: "finalizer".to_string(), + amount: 1, + eligible_from_height: 1, + eligible_until_height: 1, + }, + BurnLeaderRank { + rank: 3, + ticket_id: "ticket-3".to_string(), + owner: "member-1".to_string(), + amount: 1, + eligible_from_height: 1, + eligible_until_height: 1, + }, + BurnLeaderRank { + rank: 4, + ticket_id: "ticket-4".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, 2); + assert_eq!(ui.burn_bundle_quorum.committee_size, 2); + } + + #[test] + fn ui_block_burn_bundle_quorum_uses_fallback_rank_window() { + let block = Block { + height: 1, + prev_hash: "parent".to_string(), + timestamp_ms: 1, + miner: "fallback".to_string(), + finalizer_mode: FinalizerMode::Ticket, + finalizer_rank: 1, + reward: 1, + vdf_rounds: 1, + vdf_output: "vdf".to_string(), + leader_proof: None, + burn_bundle_section: BurnBundleSection { + signatures: vec![crate::domain::BurnBundleSignature { + slot: 1, + member: "member-1".to_string(), + signature: "sig-1".to_string(), + }], + burns: Vec::new(), + }, + transactions: vec![burn("burn-a")], + hash: "hash".to_string(), + }; + let ranks = BTreeMap::from([( + "hash".to_string(), + vec![ + BurnLeaderRank { + rank: 0, + ticket_id: "ticket-0".to_string(), + owner: "missed-primary".to_string(), + amount: 1, + eligible_from_height: 1, + eligible_until_height: 1, + }, + BurnLeaderRank { + rank: 1, + ticket_id: "ticket-1".to_string(), + owner: "fallback".to_string(), + amount: 1, + eligible_from_height: 1, + eligible_until_height: 1, + }, + BurnLeaderRank { + rank: 2, + ticket_id: "ticket-2".to_string(), + owner: "member-1".to_string(), + amount: 1, + eligible_from_height: 1, + eligible_until_height: 1, + }, + BurnLeaderRank { + rank: 3, + ticket_id: "ticket-3".to_string(), + owner: "member-2".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, 2); + assert_eq!(ui.burn_bundle_quorum.committee_size, 2); } } diff --git a/src/app.rs b/src/app.rs @@ -52,7 +52,7 @@ const PEER_CLOCK_OFFSET_STALE_MS: u64 = 20 * 60 * 1_000; const AUTO_POW_NONCE_ATTEMPTS_PER_WORKER_TICK: u64 = 100_000; const AUTO_PLAINTEXT_BURN_BEFORE_RECOVERY_MS: u64 = 60_000; const BURN_BUNDLE_COLLECTION_MS: u64 = 30_000; -const AUTO_BLOCK_ANCHOR_BURN_AMOUNT: Amount = 1; +const AUTO_BLOCK_ANCHOR_BURN_AMOUNT: Amount = crate::domain::MICRO_IUNA; const AUTO_BLOCK_ANCHOR_BURN_FEE: Amount = 1; static DEBUG_LOGGING: AtomicBool = AtomicBool::new(false); diff --git a/src/app/automatic_mining.rs b/src/app/automatic_mining.rs @@ -271,6 +271,13 @@ impl NodeCore { self.last_auto_burn_height = Some(current_height); return Ok(anchor_burn); } + if anchor_burn + .as_ref() + .is_some_and(|burn| burn.amount() >= self.burn_per_block) + { + self.last_auto_burn_height = Some(current_height); + return Ok(anchor_burn); + } if self.last_auto_burn_height == Some(current_height) { return Ok(anchor_burn); } diff --git a/src/main.rs b/src/main.rs @@ -32,6 +32,7 @@ use cli::{ #[cfg(test)] use cli::{default_data_dir, help_text}; +const GENESIS_BOOTSTRAP_BALANCE: Amount = 100 * MICRO_IUNA; const GENESIS_BOOTSTRAP_BURN_AMOUNT: Amount = MICRO_IUNA; const GENESIS_INITIAL_BURN_PER_BLOCK: Amount = config_store::DEFAULT_BURN_AMOUNT; const GENESIS_INITIAL_BURN_FEE: Amount = config_store::DEFAULT_BURN_FEE; @@ -470,7 +471,7 @@ fn setup_ledger() -> Ledger { fn start_genesis_ledger(wallet_address: &str) -> Result<Ledger> { let vdf_rounds = measure_initial_vdf_rounds(); let mut genesis = BTreeMap::new(); - genesis.insert(wallet_address.to_string(), GENESIS_BOOTSTRAP_BURN_AMOUNT); + genesis.insert(wallet_address.to_string(), GENESIS_BOOTSTRAP_BALANCE); Ledger::new_with_genesis_burns( genesis, vec![GenesisBurn::new( diff --git a/src/main_tests.rs b/src/main_tests.rs @@ -422,7 +422,7 @@ fn genesis_mode_starts_with_default_burn_rate_and_fee() { fn genesis_default_auto_mining_keeps_burning_after_first_block() { let wallet = Wallet::from_seed("genesis-auto-burn-wallet"); let mut genesis = BTreeMap::new(); - genesis.insert(wallet.address().to_string(), MICRO_IUNA); + genesis.insert(wallet.address().to_string(), 10 * MICRO_IUNA); let ledger = Ledger::new_with_genesis_burns( genesis, vec![GenesisBurn::new(wallet.address(), MICRO_IUNA)], @@ -440,11 +440,20 @@ fn genesis_default_auto_mining_keeps_burning_after_first_block() { let second = node.automatic_mine_once(2_000); let third = node.automatic_mine_once(3_000); - assert_eq!(first.burned.as_ref().map(|tx| tx.amount()), Some(1)); + assert_eq!( + first.burned.as_ref().map(|tx| tx.amount()), + Some(GENESIS_INITIAL_BURN_PER_BLOCK) + ); assert!(first.block.is_some(), "{first:?}"); - assert_eq!(second.burned.as_ref().map(|tx| tx.amount()), Some(1)); + assert_eq!( + second.burned.as_ref().map(|tx| tx.amount()), + Some(GENESIS_INITIAL_BURN_PER_BLOCK) + ); assert!(second.block.is_some(), "{second:?}"); - assert_eq!(third.burned.as_ref().map(|tx| tx.amount()), Some(1)); + assert_eq!( + third.burned.as_ref().map(|tx| tx.amount()), + Some(GENESIS_INITIAL_BURN_PER_BLOCK) + ); assert!(third.block.is_some(), "{third:?}"); assert!( second.skipped_reason.as_deref().is_none_or(|reason| { @@ -454,7 +463,8 @@ fn genesis_default_auto_mining_keeps_burning_after_first_block() { ); assert!( node.ledger().balance_of(wallet.address()) - >= BLOCK_REWARD - 3 * (GENESIS_INITIAL_BURN_PER_BLOCK + GENESIS_INITIAL_BURN_FEE) + >= BLOCK_REWARD + .saturating_sub(3 * (GENESIS_INITIAL_BURN_PER_BLOCK + GENESIS_INITIAL_BURN_FEE)) ); }