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