commit fe972a32a637661060b72f77f17655a98c1a404b
parent 8d9b52afef31ba8af1bf479298dd5fe4e2e1d8fb
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Sat, 8 Aug 2026 21:12:10 +0200
Improve local mining status and PoW resilience
Diffstat:
4 files changed, 150 insertions(+), 37 deletions(-)
diff --git a/src/adapters/http.rs b/src/adapters/http.rs
@@ -3233,6 +3233,7 @@ const INDEX_HTML: &str = r#"<!doctype html>
.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; }
.mine-stats { display: grid; grid-template-columns: repeat(4, minmax(112px, 1fr)); gap: 8px; min-width: 0; }
+ .local-mining-stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.fee-history { grid-template-columns: repeat(3, minmax(112px, 1fr)); margin-top: 12px; }
.mine-stat { min-width: 0; border: 1px solid #2f363c; border-radius: 8px; padding: 9px 10px; background: #111316; }
.mine-stat-label { display: flex; gap: 5px; align-items: center; color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; }
@@ -3603,11 +3604,23 @@ const INDEX_HTML: &str = r#"<!doctype html>
<div class="mining-grid">
<div class="panel">
<h3>Status</h3>
- <div class="grid">
- <div class="metric"><div class="label">Current Leader</div><div class="value" x-text="isLeaderLabel()"></div></div>
- <div class="metric"><div class="label">Last Burn Height</div><div class="value" x-text="status.mining?.last_auto_burn_height ?? '-'"></div></div>
- <div class="metric"><div class="label">VDF Rounds</div><div class="value" x-text="status.mining?.vdf_rounds ?? '-'"></div></div>
- <div class="metric"><div class="label">Target</div><div class="value" x-text="targetSecondsLabel()"></div></div>
+ <div class="mine-stats local-mining-stats" aria-label="Local mining status">
+ <div class="mine-stat">
+ <div class="mine-stat-label">PoB State</div>
+ <div class="mine-stat-value" x-text="pobStatusLabel()"></div>
+ </div>
+ <div class="mine-stat">
+ <div class="mine-stat-label">PoW State</div>
+ <div class="mine-stat-value" x-text="powStatusShortLabel()"></div>
+ </div>
+ <div class="mine-stat">
+ <div class="mine-stat-label">Selected Finalizer</div>
+ <code class="mine-stat-value" x-text="currentFinalizerLabel()"></code>
+ </div>
+ <div class="mine-stat">
+ <div class="mine-stat-label">Mempool</div>
+ <div class="mine-stat-value" x-text="localMiningMempoolLabel()"></div>
+ </div>
</div>
</div>
<div class="panel">
@@ -3659,7 +3672,7 @@ const INDEX_HTML: &str = r#"<!doctype html>
</div>
<div class="mine-stat">
<div class="mine-stat-label">Difficulty <button class="info-button" type="button" @click="openPowDifficultyInfo" title="How difficulty is adjusted" aria-label="How PoW difficulty is adjusted">i</button></div>
- <div class="mine-stat-value"><span x-text="status.chain?.current_mine_difficulty_bits ?? status.launch_profile?.mine_difficulty_bits ?? '-'"></span> bits</div>
+ <div class="mine-stat-value"><span x-text="powDifficultyLabel()"></span> bits</div>
</div>
</div>
<label class="toggle-switch" :class="{ active: powMiningEnabled }" title="Continuously search for PoW mine actions with a small local work budget">
@@ -5817,6 +5830,13 @@ mod tests {
super::INDEX_HTML.contains("Search for PoW actions that mint a fixed IUNA reward.")
);
assert!(super::INDEX_HTML.contains("amountLabel(powMineReward())"));
+ 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("Selected Finalizer"));
+ assert!(app_js.contains("pobStatusLabel()"));
+ assert!(app_js.contains("powStatusShortLabel()"));
+ assert!(app_js.contains("localMiningMempoolLabel()"));
assert!(!super::INDEX_HTML.contains("Needs burns"));
}
diff --git a/src/app.rs b/src/app.rs
@@ -1280,7 +1280,7 @@ impl NodeCore {
if self.ledger.has_unrevealed_blinded_transaction(commitment)
&& !ledger.has_transaction(payload.signature())
{
- let _ = ledger.submit_transaction(payload.clone())?;
+ let _ = ledger.submit_transaction(payload.clone());
}
}
Ok(())
@@ -1301,7 +1301,7 @@ impl NodeCore {
return Ok(());
};
if *height == ledger.height() && !ledger.has_transaction(burn.signature()) {
- ledger.reserve_transaction_inputs(burn)?;
+ let _ = ledger.reserve_transaction_inputs(burn);
}
Ok(())
}
@@ -1758,46 +1758,37 @@ impl NodeCore {
}
fn prepare_next_block_with_local_anchor(&self, timestamp_ms: u64) -> Result<PreparedBlock> {
- let required_burn_signature = self.current_local_block_anchor_signature();
- self.ledger_with_local_block_anchor()?
- .prepare_next_block_with_required_burn_and_reveal_bundles(
- self.wallet.address(),
- timestamp_ms,
- self.usable_reveal_bundles(),
- required_burn_signature.as_deref(),
- )
+ let (ledger, required_burn_signature) = self.ledger_with_local_block_anchor();
+ ledger.prepare_next_block_with_required_burn_and_reveal_bundles(
+ self.wallet.address(),
+ timestamp_ms,
+ self.usable_reveal_bundles(),
+ required_burn_signature.as_deref(),
+ )
}
fn prepare_recovery_block_with_local_anchor(&self, timestamp_ms: u64) -> Result<PreparedBlock> {
- let required_burn_signature = self.current_local_block_anchor_signature();
- self.ledger_with_local_block_anchor()?
- .prepare_recovery_block_with_required_burn_and_reveal_bundles(
- self.wallet.address(),
- timestamp_ms,
- self.usable_reveal_bundles(),
- required_burn_signature.as_deref(),
- )
- }
-
- fn current_local_block_anchor_signature(&self) -> Option<String> {
- let (height, burn) = self.local_block_anchor_burn.as_ref()?;
- if *height == self.ledger.height() && !self.ledger.has_transaction(burn.signature()) {
- Some(burn.signature().to_string())
- } else {
- None
- }
+ let (ledger, required_burn_signature) = self.ledger_with_local_block_anchor();
+ ledger.prepare_recovery_block_with_required_burn_and_reveal_bundles(
+ self.wallet.address(),
+ timestamp_ms,
+ self.usable_reveal_bundles(),
+ required_burn_signature.as_deref(),
+ )
}
- fn ledger_with_local_block_anchor(&self) -> Result<Ledger> {
+ fn ledger_with_local_block_anchor(&self) -> (Ledger, Option<String>) {
let mut ledger = self.ledger.clone();
let Some((height, burn)) = &self.local_block_anchor_burn else {
- return Ok(ledger);
+ return (ledger, None);
};
if *height == ledger.height() && !ledger.has_transaction(burn.signature()) {
ledger.drop_pending_blinded_conflicting_with_transaction(burn);
- let _ = ledger.submit_transaction(burn.clone())?;
+ if ledger.submit_transaction(burn.clone()).is_ok() {
+ return (ledger, Some(burn.signature().to_string()));
+ }
}
- Ok(ledger)
+ (ledger, None)
}
fn clear_stale_local_block_anchor(&mut self) {
@@ -2656,6 +2647,61 @@ mod tests {
}
#[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");
+ let mut allocations = BTreeMap::new();
+ allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA);
+ allocations.insert(bob.address().to_string(), MICRO_IUNA);
+ let ledger = Ledger::new_with_genesis_burns(
+ allocations,
+ vec![GenesisBurn::new(bob.address(), MICRO_IUNA)],
+ 10,
+ )
+ .unwrap();
+ let mut node = NodeCore::from_ledger(alice.clone(), ledger, 0);
+
+ let blinded = node
+ .blinded_burn_with_fee(MICRO_IUNA / 10, 7, node.chain_height() + 4)
+ .unwrap();
+ let mut finalizer_ledger = node.ledger().clone();
+ let leader_burn = finalizer_ledger.build_burn(&bob, 1, 0).unwrap();
+ finalizer_ledger.submit_transaction(leader_burn).unwrap();
+ let commit_block = finalizer_ledger.mine_next_block(&bob, 1).unwrap();
+ assert!(
+ commit_block
+ .blinded_transactions
+ .iter()
+ .any(|tx| tx.commitment == blinded.commitment)
+ );
+ finalizer_ledger.apply_block(commit_block).unwrap();
+ assert!(node.import_verified_ledger(finalizer_ledger).unwrap());
+ assert!(
+ node.ledger()
+ .has_unrevealed_blinded_transaction(&blinded.commitment)
+ );
+
+ node.set_pow_mining_enabled(true);
+
+ assert!(node.prepare_automatic_pow_mining().is_ok());
+ }
+
+ #[test]
+ fn automatic_pow_mining_skips_stale_local_anchor_reservation() {
+ let wallet = Wallet::from_seed("automatic-pow-stale-anchor-wallet");
+ let mut stale_allocations = BTreeMap::new();
+ stale_allocations.insert(wallet.address().to_string(), MICRO_IUNA);
+ let stale_ledger = Ledger::new(stale_allocations, 10);
+ let stale_anchor = stale_ledger.build_burn(&wallet, 1, 0).unwrap();
+ let live_ledger = Ledger::new(BTreeMap::new(), 10);
+ let mut node = NodeCore::from_ledger(wallet.clone(), live_ledger, 0);
+ node.local_block_anchor_burn = Some((node.chain_height(), stale_anchor));
+ node.set_pow_mining_enabled(true);
+
+ assert!(node.prepare_automatic_pow_mining().is_ok());
+ }
+
+ #[test]
fn automatic_finalization_does_not_tick_pow_mining() {
let wallet = Wallet::from_seed("automatic-pow-separated-finalizer-wallet");
let mut node = NodeCore::new(NodeConfig {
diff --git a/tests/properties.rs b/tests/properties.rs
@@ -705,6 +705,7 @@ fn in_memory_network_converges_under_generated_node_actions() {
assert!(
reason.contains("at least one burn")
|| reason.contains("selected finalizer")
+ || reason.contains("required burn")
|| reason.contains("could not")
|| reason.contains("automatic"),
"unexpected mining skip reason: {reason}"
@@ -788,6 +789,7 @@ fn receive_chaotic_envelope(
assert!(
message.contains("expected block height")
|| message.contains("mine transaction anchor is not on this chain")
+ || message.contains("blinded transaction spends missing output")
|| message.contains("blinded transaction expiry is too far in the future"),
"unexpected chaotic delivery error: {message}"
);
@@ -878,6 +880,7 @@ fn in_memory_network_converges_after_generated_offline_and_reordered_delivery()
if let Some(reason) = outcome.skipped_reason {
assert!(
reason.contains("at least one burn")
+ || reason.contains("required burn")
|| reason.contains("could not")
|| reason.contains("automatic burn failed"),
"unexpected chaotic mining skip reason: {reason}"
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -1311,11 +1311,55 @@ window.iunaApp = function iunaApp() {
return Math.max(0, Math.trunc(Number(this.status.chain?.mine_reward ?? 1000000)));
},
+ pobStatusLabel() {
+ const mining = this.status.mining;
+ if (!mining) return "-";
+ if (this.status.wallet_locked) return "Wallet locked";
+ if (!mining.automatic) return "Off";
+ if ((mining.burn_per_block ?? 0) <= 0) return "Anchor only";
+ if (mining.wallet_is_current_leader) return "Selected";
+ if (mining.current_leader) return "Waiting";
+ return "Recovery standby";
+ },
+
+ powStatusShortLabel() {
+ 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("searched")) return "Searching";
+ if (status.includes("waiting")) return "Waiting";
+ if (status.includes("failed")) return "Error";
+ return "On";
+ },
+
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";
},
+ currentFinalizerLabel() {
+ const leader = this.status.mining?.current_leader ?? this.status.chain?.next_leader;
+ if (!leader) return "-";
+ if (leader === this.status.wallet_address) return "you";
+ return this.short(leader);
+ },
+
+ localMiningMempoolLabel() {
+ const pending = this.status.chain?.pending_transactions;
+ if (typeof pending !== "number") return "-";
+ const visibleMines = this.localMineActionCount();
+ return `${pending} pending / ${visibleMines} visible mines`;
+ },
+
+ powDifficultyLabel() {
+ return this.status.chain?.current_mine_difficulty_bits ?? this.status.launch_profile?.mine_difficulty_bits ?? "-";
+ },
+
+ localMineActionCount() {
+ return this.mempool.filter((tx) => tx?.kind === "mine").length;
+ },
+
metricsCharts() {
return Array.isArray(this.blockchainMetrics?.charts) ? this.blockchainMetrics.charts : [];
},