iuna

iuna

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

commit 71e5830ed770e98af007789fd7d9e0729e6271a2
parent c04eca7dd9e3333bd44be5b6f89a103a06623b63
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Sun, 30 Aug 2026 07:49:10 +0200

Fix automatic burn fee rate handling

Diffstat:
Msrc/app.rs | 1-
Msrc/app/automatic_mining.rs | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
2 files changed, 77 insertions(+), 30 deletions(-)

diff --git a/src/app.rs b/src/app.rs @@ -54,7 +54,6 @@ 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 MIN_AUTO_BLOCK_ANCHOR_BURN_AMOUNT: Amount = 1; -const AUTO_BLOCK_ANCHOR_BURN_FEE: Amount = 1; static DEBUG_LOGGING: AtomicBool = AtomicBool::new(false); #[cfg(test)] diff --git a/src/app/automatic_mining.rs b/src/app/automatic_mining.rs @@ -1,10 +1,12 @@ use anyhow::{Context, Result}; -use super::helpers::{allowed_recovery_vdf_rank_count, recovery_vdf_sample_percent}; +use super::helpers::{ + allowed_recovery_vdf_rank_count, converge_fee_by_byte, recovery_vdf_sample_percent, +}; use super::{ - AUTO_BLOCK_ANCHOR_BURN_FEE, AUTO_PLAINTEXT_BURN_BEFORE_RECOVERY_MS, AutoMineOutcome, - AutoMinePlan, BURN_BUNDLE_COLLECTION_MS, GossipEnvelope, Ledger, - MIN_AUTO_BLOCK_ANCHOR_BURN_AMOUNT, NodeCore, PreparedBlock, Transaction, run_vdf, + AUTO_PLAINTEXT_BURN_BEFORE_RECOVERY_MS, AutoMineOutcome, AutoMinePlan, + BURN_BUNDLE_COLLECTION_MS, GossipEnvelope, Ledger, MIN_AUTO_BLOCK_ANCHOR_BURN_AMOUNT, NodeCore, + PreparedBlock, Transaction, run_vdf, }; use crate::domain::{Amount, BurnCommitteeMember, FinalizerMode}; @@ -318,30 +320,31 @@ impl NodeCore { let ledger = self.wallet_anchor_build_ledger()?; let wallet = self.wallet.unlocked()?; + let available_utxos = ledger.available_utxos_for_address(wallet.address())?; // The plaintext anchor is the block's automatic burn when one is configured. // Keep a one-micro-IUNA anchor when automatic finalization is enabled with a // zero target, because the finalizer still needs a local burn to anchor. let anchor_burn_amount = self.burn_per_block.max(MIN_AUTO_BLOCK_ANCHOR_BURN_AMOUNT); - let required = anchor_burn_amount - .checked_add(AUTO_BLOCK_ANCHOR_BURN_FEE) - .context("automatic finalizer anchor burn amount plus fee overflows")?; - let outpoint = ledger - .available_utxos_for_address(wallet.address())? - .into_iter() - .filter(|(_, output)| output.amount >= required) - .min_by_key(|(_, output)| output.amount) - .map(|(outpoint, _)| outpoint); - let burn = match outpoint { - Some(outpoint) => ledger.build_burn_with_inputs( - wallet, - anchor_burn_amount, - AUTO_BLOCK_ANCHOR_BURN_FEE, - &[outpoint], - ), - None => ledger.build_burn(wallet, anchor_burn_amount, AUTO_BLOCK_ANCHOR_BURN_FEE), - }; - let burn = match burn { - Ok(burn) => burn, + let burn = match converge_fee_by_byte(self.burn_fee, |fee| { + let required = anchor_burn_amount + .checked_add(fee) + .context("automatic finalizer anchor burn amount plus fee overflows")?; + let outpoint = available_utxos + .iter() + .filter(|(_, output)| output.amount >= required) + .min_by_key(|(_, output)| output.amount) + .map(|(outpoint, _)| outpoint); + match outpoint { + Some(outpoint) => ledger.build_burn_with_inputs( + wallet, + anchor_burn_amount, + fee, + std::slice::from_ref(outpoint), + ), + None => ledger.build_burn(wallet, anchor_burn_amount, fee), + } + }) { + Ok((burn, _)) => burn, Err(error) => { self.last_auto_anchor_burn_height = Some(current_height); return Err(error).context("automatic finalizer anchor burn failed"); @@ -647,7 +650,7 @@ mod tests { app::{GossipEnvelope, InMemoryNetwork}, domain::{ BurnBundle, BurnCommitteeMember, FinalizerMode, GenesisBurn, Ledger, MICRO_IUNA, - Wallet, run_vdf, + Transaction, Wallet, run_vdf, }, }; use tempfile::tempdir; @@ -958,14 +961,59 @@ mod tests { node.publish_burn_bundle_for_next_block().unwrap(); - assert!( - node.usable_burn_bundles() - .iter() - .any(|bundle| bundle.member == wallet.address() && !bundle.burns.is_empty()) + let bundles = node.usable_burn_bundles(); + let bundled_anchor = bundles + .iter() + .find(|bundle| bundle.member == wallet.address()) + .and_then(|bundle| bundle.burns.first()) + .expect("the local anchor should be exposed through the committee bundle"); + assert_eq!( + bundled_anchor.fee(), + bundled_anchor.economic_size_bytes() as u64 ); } #[test] + fn local_anchor_burn_stores_total_fee_derived_from_configured_fee_rate() { + let wallet = Wallet::from_seed("local-anchor-fee-rate-wallet"); + let ledger = funded_ledger(std::slice::from_ref(&wallet)); + let fee_per_byte = 100; + let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( + wallet, + ledger, + true, + 3_000, + fee_per_byte, + ); + + node.prepare_automatic_burn(1).unwrap(); + + let burn = node + .local_block_anchor_burn + .as_ref() + .map(|(_, burn)| burn) + .expect("selected finalizer should prepare a local anchor"); + assert_eq!(burn.amount(), 3_000); + assert_eq!(burn.fee(), fee_per_byte * burn.economic_size_bytes() as u64); + assert_ne!(burn.fee(), fee_per_byte); + let required = burn.amount().checked_add(burn.fee()).unwrap(); + let expected_outpoint = node + .ledger() + .available_utxos_for_address(node.wallet_address()) + .unwrap() + .into_iter() + .filter(|(_, output)| output.amount >= required) + .min_by_key(|(_, output)| output.amount) + .map(|(outpoint, _)| outpoint) + .expect("fixture should contain a single UTXO large enough for the burn"); + let Transaction::Burn { inputs, .. } = burn else { + panic!("local anchor should be a burn transaction"); + }; + assert_eq!(inputs.len(), 1); + assert_eq!(inputs[0].outpoint, expected_outpoint); + } + + #[test] fn conflicting_local_anchor_falls_back_to_pending_wallet_burn() { let wallet = Wallet::from_seed("conflicting-local-anchor-wallet"); let ledger = funded_ledger(std::slice::from_ref(&wallet));