commit cbbfb4152d54dccc4f4d75fb6fcce85f5d0e42c9
parent 3b5fdf77e336525351313e465a0ae27af2478d86
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Sat, 8 Aug 2026 22:32:26 +0200
Add configurable PoW mining workers
Diffstat:
6 files changed, 188 insertions(+), 31 deletions(-)
diff --git a/src/adapters/config_store.rs b/src/adapters/config_store.rs
@@ -17,6 +17,8 @@ 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_RECOVERY_VDF_TOP_RANK_PERCENT: u8 = 50;
+pub const DEFAULT_POW_MINING_WORKERS: u8 = 1;
+pub const MAX_POW_MINING_WORKERS: u8 = 32;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct UiConfig {
@@ -25,6 +27,7 @@ pub struct UiConfig {
pub auth_password_hash: Option<String>,
pub mining_enabled: bool,
pub pow_mining_enabled: bool,
+ pub pow_mining_workers: u8,
pub burn_per_block: Amount,
pub burn_fee: Amount,
pub recovery_vdf_top_rank_percent: u8,
@@ -41,6 +44,7 @@ impl Default for UiConfig {
auth_password_hash: None,
mining_enabled: false,
pow_mining_enabled: false,
+ pow_mining_workers: DEFAULT_POW_MINING_WORKERS,
burn_per_block: DEFAULT_BURN_AMOUNT,
burn_fee: DEFAULT_BURN_FEE,
recovery_vdf_top_rank_percent: DEFAULT_RECOVERY_VDF_TOP_RANK_PERCENT,
@@ -64,6 +68,8 @@ struct ConfigFile {
mining_enabled: Option<bool>,
#[serde(default)]
pow_mining_enabled: bool,
+ #[serde(default = "default_pow_mining_workers")]
+ pow_mining_workers: u8,
#[serde(default)]
burn_per_block: Amount,
#[serde(default)]
@@ -103,6 +109,7 @@ pub fn save(path: &Path, config: &UiConfig) -> Result<()> {
auth_password_hash: config.auth_password_hash.clone(),
mining_enabled: Some(config.mining_enabled),
pow_mining_enabled: config.pow_mining_enabled,
+ pow_mining_workers: clamp_pow_mining_workers(config.pow_mining_workers),
burn_per_block: config.burn_per_block,
burn_fee: Some(config.burn_fee),
recovery_vdf_top_rank_percent: Some(config.recovery_vdf_top_rank_percent),
@@ -152,6 +159,7 @@ fn load(path: &Path) -> Result<UiConfig> {
auth_password_hash: stored.auth_password_hash,
mining_enabled: stored.mining_enabled.unwrap_or(stored.burn_per_block > 0),
pow_mining_enabled: stored.pow_mining_enabled,
+ pow_mining_workers: clamp_pow_mining_workers(stored.pow_mining_workers),
burn_per_block: stored.burn_per_block.saturating_mul(scale),
burn_fee: stored
.burn_fee
@@ -168,6 +176,14 @@ fn load(path: &Path) -> Result<UiConfig> {
})
}
+pub fn clamp_pow_mining_workers(workers: u8) -> u8 {
+ workers.clamp(1, MAX_POW_MINING_WORKERS)
+}
+
+fn default_pow_mining_workers() -> u8 {
+ DEFAULT_POW_MINING_WORKERS
+}
+
fn create_config_file(path: &Path) -> Result<File> {
let mut options = OpenOptions::new();
options.write(true).create(true).truncate(true);
@@ -203,6 +219,7 @@ mod tests {
assert!(stored.contains("\"auth_password_hash\": null"));
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_fee\": 100"));
assert!(!stored.contains("\"pow_mine_fee\""));
@@ -225,6 +242,7 @@ mod tests {
auth_password_hash: Some("auth-hash".to_string()),
mining_enabled: true,
pow_mining_enabled: true,
+ pow_mining_workers: 4,
burn_per_block: 50 * MICRO_IUNA,
burn_fee: 3 * MICRO_IUNA,
keep_track_of_metrics: true,
@@ -241,6 +259,7 @@ mod tests {
assert_eq!(config.auth_password_hash.as_deref(), Some("auth-hash"));
assert!(config.mining_enabled);
assert!(config.pow_mining_enabled);
+ assert_eq!(config.pow_mining_workers, 4);
assert_eq!(config.burn_per_block, 50 * MICRO_IUNA);
assert_eq!(config.burn_fee, 3 * MICRO_IUNA);
assert!(config.keep_track_of_metrics);
@@ -284,6 +303,7 @@ mod tests {
assert!(config.setup_complete);
assert!(!config.mining_enabled);
assert!(!config.pow_mining_enabled);
+ assert_eq!(config.pow_mining_workers, 1);
assert_eq!(config.burn_per_block, 0);
assert_eq!(config.burn_fee, DEFAULT_BURN_FEE);
assert!(!config.keep_track_of_metrics);
diff --git a/src/adapters/http.rs b/src/adapters/http.rs
@@ -153,6 +153,7 @@ struct RecoveryVdfSettingsForm {
#[derive(Debug, Deserialize)]
struct PowMiningForm {
enabled: bool,
+ workers: Option<u8>,
}
#[derive(Debug, Deserialize)]
@@ -1097,7 +1098,7 @@ async fn api_pow_mining_form(
State(state): State<HttpState>,
Form(form): Form<PowMiningForm>,
) -> Json<ActionResponse> {
- action_json(set_pow_mining(&state, form.enabled).await)
+ action_json(set_pow_mining(&state, form.enabled, form.workers).await)
}
async fn api_metrics_settings_form(
@@ -1227,21 +1228,28 @@ async fn persist_burn_settings_config(
config_store::save(config_path, &config)
}
-async fn set_pow_mining(state: &HttpState, enabled: bool) -> Result<()> {
+async fn set_pow_mining(state: &HttpState, enabled: bool, workers: Option<u8>) -> Result<()> {
+ let workers = match workers {
+ Some(workers) => workers,
+ None => state.ui_config.lock().await.pow_mining_workers,
+ };
{
let mut node = state.node.lock().await;
+ node.set_pow_mining_workers(workers);
node.set_pow_mining_enabled(enabled);
}
- persist_pow_mining_config(&state.ui_config, &state.config_path, enabled).await
+ persist_pow_mining_config(&state.ui_config, &state.config_path, enabled, workers).await
}
async fn persist_pow_mining_config(
ui_config: &Arc<Mutex<UiConfig>>,
config_path: &Path,
enabled: bool,
+ workers: u8,
) -> Result<()> {
let mut config = ui_config.lock().await;
config.pow_mining_enabled = enabled;
+ config.pow_mining_workers = config_store::clamp_pow_mining_workers(workers);
config_store::save(config_path, &config)
}
@@ -3228,7 +3236,7 @@ const INDEX_HTML: &str = r#"<!doctype html>
.panel-description { max-width: 760px; margin: -4px 0 12px; color: #9eb3bc; font-size: 13px; line-height: 1.45; }
.mining-form { width: 100%; display: flex; flex-wrap: wrap; gap: 10px; align-items: end; }
.burn-fields { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; }
- .mine-action-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center; }
+ .mine-action-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; align-items: center; }
.mine-settings-form { display: grid; gap: 10px; }
.mine-fee-fields { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; }
.fee-preview { flex-basis: 100%; color: #9eb3bc; font-size: 12px; font-weight: 700; }
@@ -3276,6 +3284,9 @@ const INDEX_HTML: &str = r#"<!doctype html>
.toggle-switch.active .toggle-thumb { transform: translateX(20px); background: #d5f55f; }
.toggle-switch:focus-within .toggle-track { outline: 2px solid #d5f55f; outline-offset: 2px; }
.toggle-text { min-width: 22px; text-align: right; }
+ .compact-number-field { display: inline-flex; align-items: center; gap: 8px; color: #9fa8ad; font-size: 12px; font-weight: 850; }
+ .compact-number-field input { width: 58px; min-width: 0; border: 1px solid #3a4248; border-radius: 8px; padding: 7px 8px; background: #101215; color: #dce4e7; font: inherit; font-variant-numeric: tabular-nums; }
+ .compact-number-field input:focus { border-color: #d5f55f; outline: 2px solid rgba(213,245,95,.2); outline-offset: 2px; }
.receive-address { display: grid; gap: 8px; }
.address-box { border: 1px solid #2f363c; border-radius: 8px; padding: 11px; background: #111316; }
.panel-head { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 12px; }
@@ -3680,6 +3691,10 @@ const INDEX_HTML: &str = r#"<!doctype html>
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-text" x-text="powMiningEnabled ? 'On' : 'Off'"></span>
</label>
+ <label class="compact-number-field" title="Local PoW worker count">
+ <span>Workers</span>
+ <input type="number" min="1" :max="maxPowMiningWorkers" :value="powMiningWorkers" @change="setPowMiningWorkers($event.target.value)">
+ </label>
</div>
<div class="fee-preview" x-text="autoPowStatusLabel()"></div>
</div>
@@ -5833,9 +5848,11 @@ mod tests {
assert!(super::INDEX_HTML.contains("aria-label=\"Local mining status\""));
assert!(super::INDEX_HTML.contains("PoB State"));
assert!(super::INDEX_HTML.contains("PoW State"));
+ assert!(super::INDEX_HTML.contains("Workers"));
assert!(super::INDEX_HTML.contains("Selected Finalizer"));
assert!(app_js.contains("pobStatusLabel()"));
assert!(app_js.contains("powStatusShortLabel()"));
+ assert!(app_js.contains("setPowMiningWorkers(workers)"));
assert!(app_js.contains("localMiningMempoolLabel()"));
assert!(!super::INDEX_HTML.contains("Needs burns"));
}
@@ -6051,12 +6068,13 @@ mod tests {
let initial_config = ui_config.lock().await.clone();
config_store::save(&config_path, &initial_config).expect("initial config should save");
- persist_pow_mining_config(&ui_config, &config_path, true)
+ persist_pow_mining_config(&ui_config, &config_path, true, 4)
.await
.unwrap();
let config = config_store::load_or_create(&config_path).unwrap();
assert!(config.pow_mining_enabled);
+ assert_eq!(config.pow_mining_workers, 4);
}
#[test]
diff --git a/src/app.rs b/src/app.rs
@@ -12,6 +12,9 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tokio::sync::Mutex;
+use crate::adapters::config_store::{
+ DEFAULT_POW_MINING_WORKERS, MAX_POW_MINING_WORKERS, clamp_pow_mining_workers,
+};
use crate::domain::{
Amount, BlindedReveal, BlindedTransaction, Block, BuiltBlindedTransaction, BurnLeaderRank,
ChainSnapshot, ChainStatus, DEFAULT_FEE_PER_BYTE, DEFAULT_TRANSACTION_FEE, Ledger,
@@ -34,7 +37,7 @@ pub const PEER_MISBEHAVIOR_BAN_SCORE: u32 = 3;
pub const PEER_MISBEHAVIOR_BAN_MS: u64 = 10 * 60 * 1_000;
pub const PEER_CLOCK_OFFSET_ACCEPTANCE_MS: i64 = 10 * 60 * 1_000;
const PEER_CLOCK_OFFSET_STALE_MS: u64 = 20 * 60 * 1_000;
-const AUTO_POW_NONCE_ATTEMPTS_PER_TICK: u64 = 8;
+const AUTO_POW_NONCE_ATTEMPTS_PER_WORKER_TICK: u64 = 100_000;
const AUTO_PLAINTEXT_BURN_BEFORE_RECOVERY_MS: u64 = 60_000;
const AUTO_BLOCK_ANCHOR_BURN_AMOUNT: Amount = 1;
const AUTO_BLOCK_ANCHOR_BURN_FEE: Amount = 0;
@@ -55,6 +58,7 @@ pub struct NodeConfig {
pub vdf_rounds: u64,
pub burn_per_block: Amount,
pub burn_fee: Amount,
+ pub pow_mining_workers: u8,
pub recovery_vdf_top_rank_percent: u8,
}
@@ -202,6 +206,8 @@ pub struct LaunchProfileStatus {
pub struct MiningStatus {
pub automatic: bool,
pub pow_mining_enabled: bool,
+ pub pow_mining_workers: u8,
+ pub max_pow_mining_workers: u8,
pub burn_per_block: Amount,
pub automatic_burn_fee: Amount,
pub automatic_pow_mine_fee: Amount,
@@ -251,6 +257,7 @@ pub struct NodeCore {
ledger: Ledger,
automatic_mining_enabled: bool,
pow_mining_enabled: bool,
+ pow_mining_workers: u8,
burn_per_block: Amount,
burn_fee: Amount,
recovery_vdf_top_rank_percent: u8,
@@ -278,6 +285,7 @@ impl NodeCore {
config.burn_per_block,
config.burn_fee,
);
+ node.set_pow_mining_workers(config.pow_mining_workers);
node.set_recovery_vdf_top_rank_percent(config.recovery_vdf_top_rank_percent);
node
}
@@ -301,6 +309,7 @@ impl NodeCore {
automatic_mining_enabled,
burn_per_block,
burn_fee,
+ DEFAULT_POW_MINING_WORKERS,
100,
)
}
@@ -333,6 +342,7 @@ impl NodeCore {
automatic_mining_enabled,
burn_per_block,
burn_fee,
+ DEFAULT_POW_MINING_WORKERS,
100,
)
}
@@ -343,6 +353,7 @@ impl NodeCore {
automatic_mining_enabled: bool,
burn_per_block: Amount,
burn_fee: Amount,
+ pow_mining_workers: u8,
recovery_vdf_top_rank_percent: u8,
) -> Self {
Self {
@@ -350,6 +361,7 @@ impl NodeCore {
ledger,
automatic_mining_enabled,
pow_mining_enabled: false,
+ pow_mining_workers: clamp_pow_mining_workers(pow_mining_workers),
burn_per_block,
burn_fee,
recovery_vdf_top_rank_percent: recovery_vdf_top_rank_percent.min(100),
@@ -656,6 +668,8 @@ impl NodeCore {
mining: MiningStatus {
automatic: self.automatic_mining_enabled,
pow_mining_enabled: self.pow_mining_enabled,
+ pow_mining_workers: self.pow_mining_workers,
+ max_pow_mining_workers: MAX_POW_MINING_WORKERS,
burn_per_block: self.burn_per_block,
automatic_burn_fee: self.burn_fee,
automatic_pow_mine_fee: MINE_FINALIZER_FEE,
@@ -765,6 +779,22 @@ impl NodeCore {
self.pow_mining_enabled
}
+ pub fn set_pow_mining_workers(&mut self, workers: u8) {
+ let workers = clamp_pow_mining_workers(workers);
+ if self.pow_mining_workers != workers {
+ self.pow_mining_workers = workers;
+ self.auto_pow_mine_cursor = None;
+ if self.pow_mining_enabled {
+ self.last_auto_pow_mine_status =
+ Some("waiting for next automatic PoW mining tick".to_string());
+ }
+ }
+ }
+
+ pub fn pow_mining_workers(&self) -> u8 {
+ self.pow_mining_workers
+ }
+
pub fn set_recovery_vdf_top_rank_percent(&mut self, percent: u8) {
self.recovery_vdf_top_rank_percent = percent.min(100);
}
@@ -1562,30 +1592,49 @@ impl NodeCore {
.as_ref()
.context("automatic PoW cursor was not initialized")?
.clone();
- let outcome = self.wallet_build_ledger()?.search_mine(
- wallet_address,
- cursor.salt,
- cursor.next_nonce,
- AUTO_POW_NONCE_ATTEMPTS_PER_TICK,
- )?;
- let mut searched = outcome.attempts;
+ let budget = AUTO_POW_NONCE_ATTEMPTS_PER_WORKER_TICK
+ .saturating_mul(u64::from(self.pow_mining_workers));
+ let mut remaining = budget;
+ let mut next_nonce = cursor.next_nonce;
+ let mut tick_attempts = 0_u64;
+ let mut queued = 0_u64;
+ let mut first_tx = None;
+ while remaining > 0 {
+ let outcome = self.wallet_build_ledger()?.search_mine(
+ wallet_address.clone(),
+ cursor.salt,
+ next_nonce,
+ remaining,
+ )?;
+ remaining = remaining.saturating_sub(outcome.attempts);
+ tick_attempts = tick_attempts.saturating_add(outcome.attempts);
+ next_nonce = outcome.next_nonce;
+ let Some(tx) = outcome.transaction else {
+ break;
+ };
+ self.submit_public_mine_action(tx.clone())?;
+ queued = queued.saturating_add(1);
+ first_tx.get_or_insert(tx);
+ }
+
+ let mut searched = tick_attempts;
if let Some(cursor) = &mut self.auto_pow_mine_cursor {
if cursor.anchor == anchor {
- cursor.next_nonce = outcome.next_nonce;
- cursor.searched = cursor.searched.saturating_add(outcome.attempts);
+ cursor.next_nonce = next_nonce;
+ cursor.searched = cursor.searched.saturating_add(tick_attempts);
searched = cursor.searched;
}
}
- let Some(tx) = outcome.transaction else {
+ let Some(tx) = first_tx else {
self.last_auto_pow_mine_status = Some(format!(
"searched {searched} PoW nonces for the current tip; no proof yet"
));
return Ok(None);
};
- self.submit_public_mine_action(tx.clone())?;
self.last_auto_pow_mine_anchor = Some(anchor);
self.last_auto_pow_mine_status = Some(format!(
- "queued mine action after {searched} PoW nonce attempts for the current tip"
+ "queued {queued} mine action{} after {searched} PoW nonce attempts for the current tip",
+ if queued == 1 { "" } else { "s" }
));
Ok(Some(tx))
}
@@ -2496,7 +2545,10 @@ mod tests {
RECOVERY_BLOCK_DELAY_MS, Transaction, VDF_TARGET_BLOCK_MS, Wallet, run_vdf,
};
- use super::{GossipEnvelope, InMemoryNetwork, NodeConfig, NodeCore};
+ use super::{
+ DEFAULT_POW_MINING_WORKERS, GossipEnvelope, InMemoryNetwork, MAX_POW_MINING_WORKERS,
+ NodeConfig, NodeCore,
+ };
fn wallet_for_address<'a>(wallets: &'a [Wallet], address: &str) -> &'a Wallet {
wallets
@@ -2534,6 +2586,7 @@ mod tests {
vdf_rounds: 10,
burn_per_block: 1,
burn_fee: 1,
+ pow_mining_workers: 1,
recovery_vdf_top_rank_percent: 100,
});
@@ -2569,7 +2622,6 @@ mod tests {
node.set_pow_mining_enabled(true);
let first = node.prepare_automatic_mining(2);
- assert!(node.ledger().pending().is_empty());
assert!(node.ledger().pending_blinded_transactions().len() <= 1);
let first = std::iter::once(first)
.chain((3..10_000).map(|timestamp| node.prepare_automatic_mining(timestamp)))
@@ -2591,7 +2643,8 @@ mod tests {
*difficulty_bits,
node.ledger().current_mine_difficulty_bits()
);
- assert_eq!(node.ledger().pending().len(), 1);
+ let first_pending = node.ledger().pending().len();
+ assert!(first_pending >= 1);
assert!(node.ledger().pending_blinded_transactions().is_empty());
assert!(node
.drain_outbox()
@@ -2603,7 +2656,7 @@ mod tests {
.last_auto_pow_mine_status
.as_deref()
.unwrap_or_default()
- .contains("queued mine action after")
+ .contains("queued")
);
let second = (10_000..20_000)
@@ -2614,7 +2667,7 @@ mod tests {
second.pow_mined.as_ref().unwrap().signature(),
first_mine.signature()
);
- assert_eq!(node.ledger().pending().len(), 2);
+ assert!(node.ledger().pending().len() > first_pending);
assert!(node.ledger().pending_blinded_transactions().is_empty());
}
@@ -2629,6 +2682,7 @@ mod tests {
vdf_rounds: 10,
burn_per_block: 0,
burn_fee: 0,
+ pow_mining_workers: 1,
recovery_vdf_top_rank_percent: 100,
});
@@ -2661,6 +2715,7 @@ mod tests {
vdf_rounds: 10,
burn_per_block: 0,
burn_fee: 0,
+ pow_mining_workers: 1,
recovery_vdf_top_rank_percent: 100,
});
@@ -2678,6 +2733,34 @@ mod tests {
}
#[test]
+ fn automatic_pow_mining_workers_are_clamped_and_reported() {
+ let wallet = Wallet::from_seed("automatic-pow-workers-wallet");
+ let mut node = NodeCore::new(NodeConfig {
+ wallet,
+ genesis_allocations: BTreeMap::new(),
+ vdf_rounds: 10,
+ burn_per_block: 0,
+ burn_fee: 0,
+ pow_mining_workers: 99,
+ recovery_vdf_top_rank_percent: 100,
+ });
+
+ assert_eq!(node.pow_mining_workers(), MAX_POW_MINING_WORKERS);
+ assert_eq!(
+ node.status().mining.max_pow_mining_workers,
+ MAX_POW_MINING_WORKERS
+ );
+
+ node.set_pow_mining_workers(0);
+
+ assert_eq!(node.pow_mining_workers(), DEFAULT_POW_MINING_WORKERS);
+ assert_eq!(
+ node.status().mining.pow_mining_workers,
+ DEFAULT_POW_MINING_WORKERS
+ );
+ }
+
+ #[test]
fn automatic_pow_mining_skips_unspendable_owned_blinded_payloads() {
let alice = Wallet::from_seed("automatic-pow-stale-owned-blind-alice");
let bob = Wallet::from_seed("automatic-pow-stale-owned-blind-bob");
@@ -2741,6 +2824,7 @@ mod tests {
vdf_rounds: 10,
burn_per_block: 0,
burn_fee: 0,
+ pow_mining_workers: 1,
recovery_vdf_top_rank_percent: 100,
});
@@ -2816,6 +2900,7 @@ mod tests {
vdf_rounds: 10,
burn_per_block: 0,
burn_fee: 0,
+ pow_mining_workers: 1,
recovery_vdf_top_rank_percent: 100,
});
@@ -2843,6 +2928,7 @@ mod tests {
vdf_rounds: 1,
burn_per_block: 0,
burn_fee: 0,
+ pow_mining_workers: 1,
recovery_vdf_top_rank_percent: 100,
});
diff --git a/src/main.rs b/src/main.rs
@@ -94,6 +94,7 @@ async fn main() -> Result<()> {
initial_burn_fee,
),
};
+ node_core.set_pow_mining_workers(ui_config.pow_mining_workers);
node_core.set_pow_mining_enabled(ui_config.pow_mining_enabled);
node_core.set_recovery_vdf_top_rank_percent(ui_config.recovery_vdf_top_rank_percent);
let node: SharedNode = Arc::new(Mutex::new(node_core));
diff --git a/tests/iuna.rs b/tests/iuna.rs
@@ -29,6 +29,7 @@ fn node(_network_key: &str, wallet: Wallet, allocations: BTreeMap<String, Amount
vdf_rounds: 25,
burn_per_block: DEFAULT_BURN_PER_BLOCK,
burn_fee: 1,
+ pow_mining_workers: 1,
recovery_vdf_top_rank_percent: 100,
})
}
@@ -946,16 +947,19 @@ fn pow_only_node_gossips_mine_action_to_pob_only_finalizer() {
alice_node.receive(envelope).unwrap();
}
- assert_eq!(alice_node.ledger().pending().len(), 1);
+ assert!(!alice_node.ledger().pending().is_empty());
assert!(
alice_node
.ledger()
.pending_blinded_transactions()
.is_empty()
);
- assert_eq!(
- alice_node.ledger().pending()[0].signature(),
- mine.signature()
+ assert!(
+ alice_node
+ .ledger()
+ .pending()
+ .iter()
+ .any(|tx| tx.signature() == mine.signature())
);
}
@@ -1828,6 +1832,7 @@ fn mined_block_gossip_does_not_include_full_chain_snapshot() {
vdf_rounds: 10,
burn_per_block: 1,
burn_fee: 1,
+ pow_mining_workers: 1,
recovery_vdf_top_rank_percent: 100,
});
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -68,6 +68,8 @@ window.iunaApp = function iunaApp() {
burnFeeDraft: "0.0001",
miningEnabled: false,
powMiningEnabled: false,
+ powMiningWorkers: 1,
+ maxPowMiningWorkers: 32,
recoveryVdfTopRankPercent: 50,
burnAmountDirty: false,
transferTo: "",
@@ -605,6 +607,9 @@ window.iunaApp = function iunaApp() {
this.burnFee = status.mining?.automatic_burn_fee ?? this.burnFee;
this.miningEnabled = status.mining?.automatic ?? this.miningEnabled;
this.powMiningEnabled = status.mining?.pow_mining_enabled ?? this.powMiningEnabled;
+ this.powMiningWorkers = status.mining?.pow_mining_workers ?? this.powMiningWorkers;
+ this.maxPowMiningWorkers =
+ status.mining?.max_pow_mining_workers ?? this.maxPowMiningWorkers;
if (!this.burnAmountDirty) {
this.burnAmountDraft = this.amountLabel(this.burnAmount);
this.burnFeeDraft = this.amountLabel(this.burnFee);
@@ -1223,7 +1228,7 @@ window.iunaApp = function iunaApp() {
this.powMiningEnabled = enabled;
await this.postForm(
"/api/settings/pow-mining",
- { enabled },
+ { enabled, workers: this.powMiningWorkers },
enabled ? "PoW mining turned on" : "PoW mining turned off"
);
} catch (error) {
@@ -1232,6 +1237,26 @@ window.iunaApp = function iunaApp() {
}
},
+ async setPowMiningWorkers(workers) {
+ const previous = this.powMiningWorkers;
+ const parsed = Number.parseInt(workers, 10);
+ const clamped = Math.min(
+ this.maxPowMiningWorkers,
+ Math.max(1, Number.isFinite(parsed) ? parsed : 1)
+ );
+ try {
+ this.powMiningWorkers = clamped;
+ await this.postForm(
+ "/api/settings/pow-mining",
+ { enabled: this.powMiningEnabled, workers: clamped },
+ `PoW workers set to ${clamped}`
+ );
+ } catch (error) {
+ this.powMiningWorkers = previous;
+ this.showFlash(error.message, "error");
+ }
+ },
+
async setKeepTrackOfMetrics(enabled) {
const previous = this.keepTrackOfMetrics;
try {
@@ -1326,16 +1351,18 @@ window.iunaApp = function iunaApp() {
if (this.status.wallet_locked) return "Wallet locked";
if (!this.powMiningEnabled) return "Off";
const status = this.status.mining?.last_auto_pow_mine_status || "";
- if (status.includes("queued mine action")) return "Queued";
+ if (status.includes("queued")) return "Queued";
if (status.includes("searched")) return "Searching";
if (status.includes("waiting")) return "Waiting";
if (status.includes("failed")) return "Error";
- return "On";
+ return `${this.powMiningWorkers} worker${this.powMiningWorkers === 1 ? "" : "s"}`;
},
autoPowStatusLabel() {
if (!this.powMiningEnabled) return "PoW mining is off";
- return this.status.mining?.last_auto_pow_mine_status || "Waiting for next automatic PoW mining tick";
+ const status =
+ this.status.mining?.last_auto_pow_mine_status || "Waiting for next automatic PoW mining tick";
+ return `${status} (${this.powMiningWorkers} worker${this.powMiningWorkers === 1 ? "" : "s"})`;
},
currentFinalizerLabel() {