commit 8d36a4e47dcfa5319af45b1243a6dc9b61ddb68e
parent 5e022036ad212a0a710fd7e8f4d3da5c7f5cf5a1
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Thu, 20 Aug 2026 11:28:44 +0200
Configure local testnet mining roles
Diffstat:
5 files changed, 134 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
@@ -119,7 +119,10 @@ docker compose up --build
```
The compose file starts one bootstrap genesis node and two joining nodes on an
-isolated Docker network. Management UIs are exposed on:
+isolated Docker network. Bootstrap automatically finalizes with burns, node2
+starts automatic PoW mining, and node3 starts with automatic burn/finalization
+enabled. Node3 can only create burns once its wallet has spendable IUNA.
+Management UIs are exposed on:
- bootstrap: <http://127.0.0.1:18661/>
- node2: <http://127.0.0.1:18662/>
@@ -131,8 +134,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 can continue without UI
-login. Override the local defaults from your shell or a `.env` file:
+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
+`.env` file:
```sh
IUNA_BOOTSTRAP_WALLET_PASSWORD='change-this-bootstrap-password' \
@@ -143,6 +147,12 @@ docker compose up --build
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_AUTOMATIC_BURN_ENABLED=true|false`
+- `IUNA_POW_MINING_ENABLED=true|false`
+
Stop the network while keeping chain data:
```sh
diff --git a/docker-compose.yml b/docker-compose.yml
@@ -37,6 +37,7 @@ services:
hostname: iuna-node2
environment:
IUNA_WALLET_PASSWORD: ${IUNA_NODE2_WALLET_PASSWORD:-local-testnet-node2-password}
+ IUNA_POW_MINING_ENABLED: "true"
entrypoint: ["/bin/sh", "-c"]
command:
- exec iuna --join 172.28.0.10:9444 --data-dir /data --http 0.0.0.0:18661 --p2p 0.0.0.0:9444 --p2p-announce 172.28.0.11:9444 --debug
@@ -57,6 +58,7 @@ services:
hostname: iuna-node3
environment:
IUNA_WALLET_PASSWORD: ${IUNA_NODE3_WALLET_PASSWORD:-local-testnet-node3-password}
+ IUNA_AUTOMATIC_BURN_ENABLED: "true"
entrypoint: ["/bin/sh", "-c"]
command:
- exec iuna --join 172.28.0.10:9444 --data-dir /data --http 0.0.0.0:18661 --p2p 0.0.0.0:9444 --p2p-announce 172.28.0.12:9444 --stratum 0.0.0.0:3333 --debug
diff --git a/src/cli.rs b/src/cli.rs
@@ -286,6 +286,9 @@ pub(crate) fn help_text() -> &'static str {
--data-dir <path> Local wallet directory (default ~/.iuna)\n\
--debug Print verbose runtime logs\n\n\
Environment:\n\
+ IUNA_WALLET_PASSWORD=<password> Unlock/encrypt the startup wallet and configure UI auth\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
@@ -39,6 +39,8 @@ const VDF_MEASUREMENT_INITIAL_ROUNDS: u64 = 1_000;
const VDF_MEASUREMENT_MAX_ROUNDS: u64 = 10_000_000;
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 WALLET_PASSWORD_ENV: &str = "IUNA_WALLET_PASSWORD";
#[tokio::main]
@@ -65,6 +67,8 @@ async fn main() -> Result<()> {
}
let mut ui_config = config_store::load_or_create(&config_path)?;
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 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)?;
@@ -79,6 +83,11 @@ async fn main() -> Result<()> {
ui_config.burn_per_block = GENESIS_INITIAL_BURN_PER_BLOCK;
ui_config.burn_fee = GENESIS_INITIAL_BURN_FEE;
}
+ let mining_config_dirty = apply_startup_mining_config_overrides(
+ &mut ui_config,
+ startup_automatic_burn_enabled,
+ startup_pow_mining_enabled,
+ );
let auth_config_dirty = apply_startup_wallet_password_config(
&config_path,
&mut ui_config,
@@ -86,8 +95,10 @@ async fn main() -> Result<()> {
)?;
let wallet_load = load_startup_wallet(&wallet_path, startup_wallet_password.as_deref())?;
let wallet_address = wallet_load.address().to_string();
- let ui_config_dirty =
- opts.chain_mode == ChainMode::Genesis || p2p_config_dirty || stratum_config_dirty;
+ let ui_config_dirty = opts.chain_mode == ChainMode::Genesis
+ || p2p_config_dirty
+ || stratum_config_dirty
+ || mining_config_dirty;
if ui_config_dirty || auth_config_dirty {
config_store::save(&config_path, &ui_config)?;
}
@@ -297,6 +308,46 @@ fn startup_wallet_password_from_env() -> Result<Option<String>> {
Ok(Some(password))
}
+fn startup_bool_from_env(name: &str) -> Result<Option<bool>> {
+ let Some(value) = std::env::var_os(name) else {
+ return Ok(None);
+ };
+ let value = value
+ .into_string()
+ .map_err(|_| anyhow::anyhow!("{name} must be valid UTF-8"))?;
+ let normalized = value.trim().to_ascii_lowercase();
+ parse_startup_bool_env_value(name, &normalized).map(Some)
+}
+
+fn parse_startup_bool_env_value(name: &str, normalized: &str) -> Result<bool> {
+ match normalized {
+ "1" | "true" | "yes" | "on" => Ok(true),
+ "0" | "false" | "no" | "off" => Ok(false),
+ _ => bail!("{name} must be one of true, false, 1, 0, yes, no, on, or off"),
+ }
+}
+
+fn apply_startup_mining_config_overrides(
+ ui_config: &mut config_store::UiConfig,
+ automatic_burn_enabled: Option<bool>,
+ pow_mining_enabled: Option<bool>,
+) -> bool {
+ let mut dirty = false;
+ if let Some(enabled) = automatic_burn_enabled {
+ if ui_config.mining_enabled != enabled {
+ ui_config.mining_enabled = enabled;
+ dirty = true;
+ }
+ }
+ if let Some(enabled) = pow_mining_enabled {
+ if ui_config.pow_mining_enabled != enabled {
+ ui_config.pow_mining_enabled = enabled;
+ dirty = true;
+ }
+ }
+ dirty
+}
+
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,11 @@ 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_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,
- persist_chain_snapshot, project_ui_data_store, run_chain_persistence_with_interval,
- validate_wallet_for_mode,
+ 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,
};
fn parse(args: &[&str]) -> anyhow::Result<Option<CliOptions>> {
@@ -30,6 +30,9 @@ 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_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"));
assert!(help_text().contains("skip seed verification"));
assert!(help_text().contains("--stratum <addr:port>"));
@@ -204,6 +207,61 @@ fn startup_wallet_password_mismatch_does_not_encrypt_plaintext_wallet() {
}
#[test]
+fn startup_mining_env_overrides_persisted_config() {
+ let mut config = UiConfig {
+ mining_enabled: false,
+ pow_mining_enabled: true,
+ ..UiConfig::default()
+ };
+
+ assert!(apply_startup_mining_config_overrides(
+ &mut config,
+ Some(true),
+ Some(false),
+ ));
+ assert!(config.mining_enabled);
+ assert!(!config.pow_mining_enabled);
+
+ assert!(!apply_startup_mining_config_overrides(
+ &mut config,
+ Some(true),
+ Some(false),
+ ));
+}
+
+#[test]
+fn startup_mining_env_can_override_genesis_defaults() {
+ let mut config = UiConfig {
+ 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_mining_config_overrides(
+ &mut config,
+ Some(false),
+ Some(true),
+ ));
+ assert!(!config.mining_enabled);
+ assert!(config.pow_mining_enabled);
+ assert_eq!(config.burn_per_block, GENESIS_INITIAL_BURN_PER_BLOCK);
+ assert_eq!(config.burn_fee, GENESIS_INITIAL_BURN_FEE);
+}
+
+#[test]
+fn startup_bool_env_parser_accepts_common_boolean_values() {
+ for value in ["1", "true", "yes", "on"] {
+ assert!(parse_startup_bool_env_value("TEST_BOOL", value).unwrap());
+ }
+ for value in ["0", "false", "no", "off"] {
+ assert!(!parse_startup_bool_env_value("TEST_BOOL", value).unwrap());
+ }
+ assert!(parse_startup_bool_env_value("TEST_BOOL", "maybe").is_err());
+}
+
+#[test]
fn no_args_starts_setup_mode() {
let opts = parse(&[]).unwrap().unwrap();
assert_eq!(opts.chain_mode, ChainMode::Setup);