iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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:
Msrc/adapters/http.rs | 32++++++++++++++++++++++++++------
Msrc/app.rs | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mtests/properties.rs | 3+++
Mwww/assets/iuna-ui.js | 44++++++++++++++++++++++++++++++++++++++++++++
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 : []; },