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:
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))
);
}