iuna

iuna

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

commit 44d2006d204c828729a77c4859eb94f7ea96d9c3
parent 0bd141f305d52857c393442dcc188854559f145c
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Thu, 20 Aug 2026 13:57:57 +0200

Improve Docker setup and block loss accounting

Diffstat:
MREADME.md | 5++++-
Mdocker-compose.yml | 9++++++---
Msrc/adapters/http/index_html.rs | 2+-
Msrc/adapters/http/types.rs | 1+
Msrc/adapters/http/ui.rs | 106++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/cli.rs | 1+
Msrc/main.rs | 36+++++++++++++++++++++++++++++++++++-
Msrc/main_tests.rs | 48+++++++++++++++++++++++++++++++++++++++++++-----
Mwww/assets/iuna-ui.js | 4+++-
9 files changed, 196 insertions(+), 16 deletions(-)

diff --git a/README.md b/README.md @@ -135,7 +135,9 @@ stable Docker-network addresses to each other. The local compose file sets `IUNA_WALLET_PASSWORD` for every container. On first start this configures the management UI password and encrypts the wallet; on restart it unlocks the encrypted wallet so finalization and automatic mining can -continue without UI login. Override the local defaults from your shell or a +continue without UI login. Compose also sets `IUNA_SETUP_COMPLETE=true`, so +after unlocking the management UI you land directly in the node instead of the +initial setup wizard. Override the local password defaults from your shell or a `.env` file: ```sh @@ -150,6 +152,7 @@ Do not use compose-file default passwords for public nodes or valuable wallets. For unattended local nodes, startup environment flags can also persist mining settings: +- `IUNA_SETUP_COMPLETE=true|false` - `IUNA_AUTOMATIC_BURN_ENABLED=true|false` - `IUNA_POW_MINING_ENABLED=true|false` diff --git a/docker-compose.yml b/docker-compose.yml @@ -20,11 +20,12 @@ services: hostname: iuna-bootstrap environment: IUNA_WALLET_PASSWORD: ${IUNA_BOOTSTRAP_WALLET_PASSWORD:-local-testnet-bootstrap-password} + IUNA_SETUP_COMPLETE: "true" entrypoint: ["/bin/sh", "-c"] command: - if [ -s /data/chain.sqlite3 ]; then exec iuna --data-dir /data --http 0.0.0.0:18661 --p2p 0.0.0.0:9444 --p2p-announce 172.28.0.10:9444 --debug; else exec iuna --genesis --data-dir /data --http 0.0.0.0:18661 --p2p 0.0.0.0:9444 --p2p-announce 172.28.0.10:9444 --debug; fi ports: - - "18661:18661" + - "8661:18661" - "19444:9444" volumes: - bootstrap-data:/data @@ -37,6 +38,7 @@ services: hostname: iuna-node2 environment: IUNA_WALLET_PASSWORD: ${IUNA_NODE2_WALLET_PASSWORD:-local-testnet-node2-password} + IUNA_SETUP_COMPLETE: "true" IUNA_POW_MINING_ENABLED: "true" entrypoint: ["/bin/sh", "-c"] command: @@ -45,7 +47,7 @@ services: bootstrap: condition: service_healthy ports: - - "18662:18661" + - "8662:18661" - "19445:9444" volumes: - node2-data:/data @@ -58,6 +60,7 @@ services: hostname: iuna-node3 environment: IUNA_WALLET_PASSWORD: ${IUNA_NODE3_WALLET_PASSWORD:-local-testnet-node3-password} + IUNA_SETUP_COMPLETE: "true" IUNA_AUTOMATIC_BURN_ENABLED: "true" entrypoint: ["/bin/sh", "-c"] command: @@ -66,7 +69,7 @@ services: bootstrap: condition: service_healthy ports: - - "18663:18661" + - "8663:18661" - "19446:9444" - "3333:3333" volumes: diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs @@ -965,7 +965,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> <div class="detail-kv"><div class="key">Burn Bundles</div><div x-text="blockBurnBundleRatio(selectedBlock)"></div></div> <div class="detail-kv"><div class="key">Burns</div><div x-text="blockBurnCount(selectedBlock)"></div></div> <div class="detail-kv"><div class="key">Transfers</div><div x-text="blockTransferCount(selectedBlock)"></div></div> - <div class="detail-kv"><div class="key">Total Burned</div><div>IUNA <span x-text="amountLabel(blockBurned(selectedBlock))"></span></div></div> + <div class="detail-kv"><div class="key">Total Lost</div><div>IUNA <span x-text="amountLabel(blockLostIuna(selectedBlock))"></span></div></div> <div class="detail-kv"> <div class="key">Bytes</div> <button class="detail-link" type="button" @click="openBlockBytesModal(selectedBlock)" title="Block byte breakdown"> diff --git a/src/adapters/http/types.rs b/src/adapters/http/types.rs @@ -387,6 +387,7 @@ pub(super) struct UiBlock { pub(super) finalizer_rank: u32, pub(super) reward: Amount, pub(super) total_fees: Amount, + pub(super) lost_iuna: Amount, pub(super) total_bytes: usize, pub(super) transaction_bytes: usize, pub(super) transaction_byte_breakdown: Vec<UiByteBreakdown>, diff --git a/src/adapters/http/ui.rs b/src/adapters/http/ui.rs @@ -1,7 +1,8 @@ use std::collections::BTreeMap; use crate::domain::{ - Block, BurnLeaderRank, ChainSnapshot, MINE_REWARD, OutPoint, Transaction, TxInput, TxOutput, + Amount, Block, BurnLeaderRank, ChainSnapshot, MINE_REWARD, OutPoint, Transaction, TxInput, + TxOutput, }; use crate::adapters::ui_index::build_ui_chain_index; @@ -224,6 +225,7 @@ pub(super) fn ui_block( finalizer_rank: block.finalizer_rank, reward: block.reward, total_fees, + lost_iuna: block_lost_iuna(&block.transactions, block.reward), total_bytes, transaction_bytes, transaction_byte_breakdown, @@ -242,6 +244,30 @@ pub(super) fn ui_block( } } +fn block_lost_iuna(transactions: &[Transaction], reward: Amount) -> Amount { + let mut burned = 0_u64; + let mut existing_supply_fees = 0_u64; + let mut minted_finalizer_fees = 0_u64; + for transaction in transactions { + match transaction { + Transaction::Burn { amount, fee, .. } => { + burned = burned.saturating_add(*amount); + existing_supply_fees = existing_supply_fees.saturating_add(*fee); + } + Transaction::Transfer { fee, .. } => { + existing_supply_fees = existing_supply_fees.saturating_add(*fee); + } + Transaction::Mine { .. } => { + minted_finalizer_fees = minted_finalizer_fees.saturating_add(transaction.fee()); + } + } + } + let returned_fees = reward + .saturating_sub(minted_finalizer_fees) + .min(existing_supply_fees); + burned.saturating_add(existing_supply_fees.saturating_sub(returned_fees)) +} + fn transaction_byte_breakdown(transactions: &[Transaction]) -> Vec<UiByteBreakdown> { let mut transfer_bytes = 0_usize; let mut burn_bytes = 0_usize; @@ -474,11 +500,11 @@ mod tests { use std::collections::BTreeMap; use crate::domain::{ - Block, BurnBundleSection, FinalizerMode, MaskedBurn, OutPoint, Transaction, TxInput, - TxOutput, + Amount, Block, BurnBundleSection, FinalizerMode, MaskedBurn, OutPoint, Transaction, + TxInput, TxOutput, }; - use super::ui_block; + use super::{block_lost_iuna, ui_block}; fn burn(signature: &str) -> Transaction { Transaction::Burn { @@ -500,6 +526,77 @@ mod tests { } } + fn transfer(signature: &str, fee: Amount) -> Transaction { + Transaction::Transfer { + inputs: vec![TxInput { + outpoint: OutPoint { + txid: format!("{signature:0<64}"), + index: 0, + }, + owner: "owner".to_string(), + signature: signature.to_string(), + }], + outputs: vec![TxOutput { + address: "recipient".to_string(), + amount: 1, + }], + fee, + signature: signature.to_string(), + } + } + + #[test] + fn block_lost_iuna_counts_burns_without_rewarded_fees() { + let burn = Transaction::Burn { + inputs: Vec::new(), + change: Vec::new(), + amount: 7, + fee: 3, + signature: "burn".to_string(), + }; + + assert_eq!(block_lost_iuna(&[burn], 3), 7); + } + + #[test] + fn block_lost_iuna_counts_unreturned_existing_supply_fees() { + let transfer = transfer("transfer-a", 5); + + assert_eq!(block_lost_iuna(&[transfer], 2), 3); + } + + #[test] + fn block_lost_iuna_ignores_minted_mine_finalizer_fees() { + let mine = Transaction::Mine { + recipient: "miner".to_string(), + anchor: "anchor".to_string(), + salt: 0, + nonce: 0, + difficulty_bits: 1, + proof_header: None, + signature: "mine".to_string(), + }; + + assert_eq!(block_lost_iuna(&[mine], 1), 0); + } + + #[test] + fn block_lost_iuna_does_not_count_mine_fees_as_returned_existing_supply() { + let transfer = transfer("transfer-a", 5); + let mine = Transaction::Mine { + recipient: "miner".to_string(), + anchor: "anchor".to_string(), + salt: 0, + nonce: 0, + difficulty_bits: 1, + proof_header: None, + signature: "mine".to_string(), + }; + let reward = mine.fee().saturating_add(2); + + assert_eq!(block_lost_iuna(&[transfer, mine], reward), 3); + } + #[test] fn ui_block_exposes_burn_bundles() { let burn = burn("burn-a"); @@ -532,6 +629,7 @@ mod tests { let ui = ui_block(block, &BTreeMap::new(), &BTreeMap::new()); assert_eq!(ui.burn_bundle_quorum.burn_bundles_included, 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); diff --git a/src/cli.rs b/src/cli.rs @@ -287,6 +287,7 @@ pub(crate) fn help_text() -> &'static str { --debug Print verbose runtime logs\n\n\ Environment:\n\ IUNA_WALLET_PASSWORD=<password> Unlock/encrypt the startup wallet and configure UI auth\n\ + IUNA_SETUP_COMPLETE=true|false Persist initial setup completion at startup\n\ IUNA_AUTOMATIC_BURN_ENABLED=true|false Persist automatic burn/finalization at startup\n\ IUNA_POW_MINING_ENABLED=true|false Persist automatic PoW mining at startup\n\ IUNA_DEV_SKIP_SEED_VERIFY=1 Show a setup button to skip seed verification\n" diff --git a/src/main.rs b/src/main.rs @@ -41,6 +41,7 @@ const VDF_MEASUREMENT_MIN_ELAPSED: Duration = Duration::from_millis(150); const VDF_PROGRESS_LOG_INTERVAL: Duration = Duration::from_secs(10); const AUTOMATIC_BURN_ENABLED_ENV: &str = "IUNA_AUTOMATIC_BURN_ENABLED"; const POW_MINING_ENABLED_ENV: &str = "IUNA_POW_MINING_ENABLED"; +const SETUP_COMPLETE_ENV: &str = "IUNA_SETUP_COMPLETE"; const WALLET_PASSWORD_ENV: &str = "IUNA_WALLET_PASSWORD"; #[tokio::main] @@ -69,6 +70,7 @@ async fn main() -> Result<()> { let startup_wallet_password = startup_wallet_password_from_env()?; let startup_automatic_burn_enabled = startup_bool_from_env(AUTOMATIC_BURN_ENABLED_ENV)?; let startup_pow_mining_enabled = startup_bool_from_env(POW_MINING_ENABLED_ENV)?; + let startup_setup_complete = startup_bool_from_env(SETUP_COMPLETE_ENV)?; let p2p_config_dirty = apply_cli_p2p_config_overrides(&opts, &mut ui_config); let stratum_config_dirty = apply_cli_stratum_config_overrides(&opts, &mut ui_config); let p2p_announce_addr = configured_p2p_announce_addr(&opts, &ui_config)?; @@ -88,6 +90,12 @@ async fn main() -> Result<()> { startup_automatic_burn_enabled, startup_pow_mining_enabled, ); + let setup_config_dirty = apply_startup_setup_config_override( + &opts, + persisted_chain_exists, + &mut ui_config, + startup_setup_complete, + )?; let auth_config_dirty = apply_startup_wallet_password_config( &config_path, &mut ui_config, @@ -98,7 +106,8 @@ async fn main() -> Result<()> { let ui_config_dirty = opts.chain_mode == ChainMode::Genesis || p2p_config_dirty || stratum_config_dirty - || mining_config_dirty; + || mining_config_dirty + || setup_config_dirty; if ui_config_dirty || auth_config_dirty { config_store::save(&config_path, &ui_config)?; } @@ -348,6 +357,31 @@ fn apply_startup_mining_config_overrides( dirty } +fn apply_startup_setup_config_override( + opts: &CliOptions, + persisted_chain_exists: bool, + ui_config: &mut config_store::UiConfig, + setup_complete: Option<bool>, +) -> Result<bool> { + let Some(setup_complete) = setup_complete else { + return Ok(false); + }; + if setup_complete + && opts.chain_mode == ChainMode::Setup + && !persisted_chain_exists + && opts.join_peers.is_empty() + { + bail!( + "{SETUP_COMPLETE_ENV}=true requires --genesis, --join, or an existing chain database" + ); + } + if ui_config.setup_complete == setup_complete { + return Ok(false); + } + ui_config.setup_complete = setup_complete; + Ok(true) +} + fn apply_startup_wallet_password_config( config_path: &Path, ui_config: &mut config_store::UiConfig, diff --git a/src/main_tests.rs b/src/main_tests.rs @@ -17,11 +17,12 @@ use tokio::sync::Mutex; use super::{ ChainMode, CliOptions, GENESIS_INITIAL_BURN_FEE, GENESIS_INITIAL_BURN_PER_BLOCK, StartupWallet, apply_cli_p2p_config_overrides, apply_cli_stratum_config_overrides, - apply_startup_mining_config_overrides, apply_startup_wallet_password_config, - configured_p2p_announce_addr, configured_p2p_bind_addr, configured_stratum_addr, - extrapolate_vdf_rounds, help_text, initial_burn_fee, initial_burn_per_block, initialize_ledger, - load_startup_wallet, measure_vdf_rounds, parse_startup_bool_env_value, persist_chain_snapshot, - project_ui_data_store, run_chain_persistence_with_interval, validate_wallet_for_mode, + apply_startup_mining_config_overrides, apply_startup_setup_config_override, + apply_startup_wallet_password_config, configured_p2p_announce_addr, configured_p2p_bind_addr, + configured_stratum_addr, extrapolate_vdf_rounds, help_text, initial_burn_fee, + initial_burn_per_block, initialize_ledger, load_startup_wallet, measure_vdf_rounds, + parse_startup_bool_env_value, persist_chain_snapshot, project_ui_data_store, + run_chain_persistence_with_interval, validate_wallet_for_mode, }; fn parse(args: &[&str]) -> anyhow::Result<Option<CliOptions>> { @@ -31,6 +32,7 @@ fn parse(args: &[&str]) -> anyhow::Result<Option<CliOptions>> { #[test] fn help_mentions_dev_seed_verify_bypass_env() { assert!(help_text().contains("IUNA_WALLET_PASSWORD=<password>")); + assert!(help_text().contains("IUNA_SETUP_COMPLETE=true|false")); assert!(help_text().contains("IUNA_AUTOMATIC_BURN_ENABLED=true|false")); assert!(help_text().contains("IUNA_POW_MINING_ENABLED=true|false")); assert!(help_text().contains("IUNA_DEV_SKIP_SEED_VERIFY=1")); @@ -262,6 +264,42 @@ fn startup_bool_env_parser_accepts_common_boolean_values() { } #[test] +fn startup_setup_complete_env_marks_join_and_genesis_nodes_ready() { + let join = parse(&["--join", "127.0.0.1:9444"]).unwrap().unwrap(); + let mut join_config = UiConfig::default(); + assert!( + apply_startup_setup_config_override(&join, false, &mut join_config, Some(true)).unwrap() + ); + assert!(join_config.setup_complete); + + let genesis = parse(&["--genesis"]).unwrap().unwrap(); + let mut genesis_config = UiConfig { + setup_complete: false, + mining_enabled: true, + pow_mining_enabled: false, + burn_per_block: GENESIS_INITIAL_BURN_PER_BLOCK, + burn_fee: GENESIS_INITIAL_BURN_FEE, + ..UiConfig::default() + }; + assert!( + apply_startup_setup_config_override(&genesis, false, &mut genesis_config, Some(true)) + .unwrap() + ); + assert!(genesis_config.setup_complete); +} + +#[test] +fn startup_setup_complete_env_rejects_empty_setup_node() { + let setup = parse(&[]).unwrap().unwrap(); + let mut config = UiConfig::default(); + let error = + apply_startup_setup_config_override(&setup, false, &mut config, Some(true)).unwrap_err(); + + assert!(error.to_string().contains("requires --genesis, --join")); + assert!(!config.setup_complete); +} + +#[test] fn no_args_starts_setup_mode() { let opts = parse(&[]).unwrap().unwrap(); assert_eq!(opts.chain_mode, ChainMode::Setup); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -2765,7 +2765,9 @@ window.iunaApp = function iunaApp() { return context.source || "-"; }, - blockBurned(block) { + blockLostIuna(block) { + const explicitTotal = block?.lostIuna ?? block?.lost_iuna; + if (explicitTotal !== null && explicitTotal !== undefined) return Number(explicitTotal) || 0; return this.blockTransactions(block) .filter((tx) => tx.kind === "burn") .reduce((sum, tx) => sum + this.txAmount(tx), 0);