iuna

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

commit 898b9b80c65f7686253b26855c091ab91cf5488a
parent 67b9c988caf56bbfbfdbc4f46f0116272650f73d
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue, 28 Jul 2026 10:11:21 +0200

Run background sync in setup mode

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
Massets/iuna-ui.js | 5+++++
Msrc/adapters/http.rs | 3++-
Msrc/app.rs | 94++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/main.rs | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
6 files changed, 155 insertions(+), 30 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -502,7 +502,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.1.8" +version = "0.1.9" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.1.8" +version = "0.1.9" edition = "2024" license = "Apache-2.0" diff --git a/assets/iuna-ui.js b/assets/iuna-ui.js @@ -801,6 +801,11 @@ window.iunaApp = function iunaApp() { return Math.max(0, reward - (this.feeEstimates.mine?.fee ?? this.powMineFeeValue())); }, + 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"; + }, + amountLabel(value) { const microiuna = Math.max(0, Math.trunc(Number(value) || 0)); const whole = Math.floor(microiuna / 1000000); diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -1899,7 +1899,7 @@ const INDEX_HTML: &str = r#"<!doctype html> .block-card { flex-basis: 108px; } } </style> - <script defer src="/assets/iuna-ui.js?v=57"></script> + <script defer src="/assets/iuna-ui.js?v=58"></script> <script defer src="/assets/alpine.min.js"></script> </head> <body x-data="iunaApp()" x-init="init()" @keydown.window.escape="closeModals()" x-cloak> @@ -2107,6 +2107,7 @@ const INDEX_HTML: &str = r#"<!doctype html> <button class="primary" type="submit">Save</button> </div> <div class="fee-preview" x-text="feeEstimateLabel('mine')"></div> + <div class="fee-preview" x-text="autoPowStatusLabel()"></div> </form> <div class="panel-separator"></div> <div class="stratum-config"> diff --git a/src/app.rs b/src/app.rs @@ -166,6 +166,8 @@ pub struct MiningStatus { pub burn_per_block: Amount, pub automatic_burn_fee: Amount, pub automatic_pow_mine_fee: Amount, + pub last_auto_pow_mine_anchor: Option<String>, + pub last_auto_pow_mine_status: Option<String>, pub vdf_rounds: u32, pub vdf_target_block_ms: u64, pub current_leader: Option<String>, @@ -206,6 +208,7 @@ pub struct NodeCore { burn_fee: Amount, last_auto_burn_height: Option<u64>, last_auto_pow_mine_anchor: Option<String>, + last_auto_pow_mine_status: Option<String>, outbox: Vec<GossipEnvelope>, } @@ -290,6 +293,7 @@ impl NodeCore { burn_fee, last_auto_burn_height: None, last_auto_pow_mine_anchor: None, + last_auto_pow_mine_status: None, outbox: Vec::new(), } } @@ -306,6 +310,7 @@ impl NodeCore { self.wallet = NodeWallet::Unlocked(wallet); self.last_auto_burn_height = None; self.last_auto_pow_mine_anchor = None; + self.last_auto_pow_mine_status = None; } pub fn ledger(&self) -> &Ledger { @@ -324,6 +329,10 @@ impl NodeCore { self.ledger.height() } + pub fn has_real_chain(&self) -> bool { + !self.ledger.is_setup_placeholder() + } + pub fn recent_blocks(&self, limit: usize) -> Vec<Block> { self.ledger.recent_blocks(limit) } @@ -457,6 +466,12 @@ impl NodeCore { burn_per_block: self.burn_per_block, automatic_burn_fee: self.burn_fee, automatic_pow_mine_fee: self.pow_mine_fee, + last_auto_pow_mine_anchor: self.last_auto_pow_mine_anchor.clone(), + last_auto_pow_mine_status: if self.pow_mining_enabled && !self.has_real_chain() { + Some("waiting for a real chain before PoW mining can start".to_string()) + } else { + self.last_auto_pow_mine_status.clone() + }, vdf_rounds: self.ledger.vdf_rounds(), vdf_target_block_ms: VDF_TARGET_BLOCK_MS, current_leader, @@ -503,6 +518,10 @@ impl NodeCore { self.pow_mining_enabled = enabled; if !enabled { self.last_auto_pow_mine_anchor = None; + self.last_auto_pow_mine_status = None; + } else { + self.last_auto_pow_mine_status = + Some("waiting for next automatic PoW mining tick".to_string()); } } @@ -511,6 +530,10 @@ impl NodeCore { self.pow_mine_fee = fee; if !enabled { self.last_auto_pow_mine_anchor = None; + self.last_auto_pow_mine_status = None; + } else { + self.last_auto_pow_mine_status = + Some("waiting for next automatic PoW mining tick".to_string()); } Ok(()) } @@ -789,6 +812,9 @@ impl NodeCore { }; if self.wallet.is_locked() { + if self.pow_mining_enabled { + self.last_auto_pow_mine_status = Some("wallet is locked".to_string()); + } return AutoMinePlan { pow_mined: None, burned: None, @@ -802,7 +828,11 @@ impl NodeCore { plan.pow_mined = tx; None } - Err(error) => Some(format!("automatic PoW mining failed: {error:#}")), + Err(error) => { + let message = format!("automatic PoW mining failed: {error:#}"); + self.last_auto_pow_mine_status = Some(message.clone()); + Some(message) + } }; if !self.automatic_mining_enabled { @@ -852,6 +882,7 @@ impl NodeCore { fn prepare_automatic_pow_mine(&mut self) -> Result<Option<Transaction>> { if !self.pow_mining_enabled { + self.last_auto_pow_mine_status = None; return Ok(None); } let anchor = self @@ -860,17 +891,27 @@ impl NodeCore { .last() .map(|block| block.hash.clone()) .context("ledger has no anchor block")?; - if self.last_auto_pow_mine_anchor.as_deref() == Some(anchor.as_str()) - || self.wallet_has_mine_for_anchor(&anchor) - { + if self.last_auto_pow_mine_anchor.as_deref() == Some(anchor.as_str()) { + self.last_auto_pow_mine_status = + Some("already queued a mine action for the current tip".to_string()); + return Ok(None); + } + if self.wallet_has_mine_for_anchor(&anchor) { + self.last_auto_pow_mine_anchor = Some(anchor); + self.last_auto_pow_mine_status = + Some("mine action is already pending for the current tip".to_string()); return Ok(None); } let (tx, _) = self.build_mine_with_fee_rate(self.pow_mine_fee)?; if self.ledger.submit_transaction(tx.clone())? { self.last_auto_pow_mine_anchor = Some(anchor); + self.last_auto_pow_mine_status = + Some("queued mine action for the current tip".to_string()); self.outbox.push(GossipEnvelope::Transaction(tx.clone())); return Ok(Some(tx)); } + self.last_auto_pow_mine_status = + Some("mine action was already known by the mempool".to_string()); Ok(None) } @@ -1024,6 +1065,7 @@ impl NodeCore { if imported { self.last_auto_burn_height = None; self.last_auto_pow_mine_anchor = None; + self.last_auto_pow_mine_status = None; self.enqueue_imported_blocks(previous_height); } Ok(()) @@ -1043,6 +1085,7 @@ impl NodeCore { self.ledger = ledger; self.last_auto_burn_height = None; self.last_auto_pow_mine_anchor = None; + self.last_auto_pow_mine_status = None; self.enqueue_imported_blocks(previous_height); Ok(true) } @@ -1406,7 +1449,7 @@ fn converge_fee_by_byte( mod tests { use std::collections::BTreeMap; - use crate::domain::{MICRO_IUNA, Transaction, Wallet}; + use crate::domain::{Ledger, MICRO_IUNA, Transaction, Wallet}; use super::{NodeConfig, NodeCore}; @@ -1479,10 +1522,18 @@ mod tests { node.ledger().current_mine_difficulty_bits() ); assert_eq!(node.ledger().pending().len(), 1); + assert_eq!( + node.status().mining.last_auto_pow_mine_status.as_deref(), + Some("queued mine action for the current tip") + ); let second = node.prepare_automatic_mining(3); assert!(second.pow_mined.is_none()); assert_eq!(node.ledger().pending().len(), 1); + assert_eq!( + node.status().mining.last_auto_pow_mine_status.as_deref(), + Some("already queued a mine action for the current tip") + ); } #[test] @@ -1536,12 +1587,21 @@ mod tests { fn automatic_pow_mining_reports_fee_rate_above_reward() { let wallet = Wallet::from_seed("automatic-pow-mining-too-high-fee-wallet"); let mut node = NodeCore::new(NodeConfig { - wallet, + wallet: wallet.clone(), genesis_allocations: BTreeMap::new(), vdf_rounds: 10, burn_per_block: 0, burn_fee: 0, }); + let mut genesis_allocations = BTreeMap::new(); + genesis_allocations.insert(wallet.address().to_string(), 1); + let ledger = Ledger::new_with_genesis_burns( + genesis_allocations, + vec![crate::domain::GenesisBurn::new(wallet.address(), 1)], + 10, + ) + .unwrap(); + node.import_verified_ledger(ledger).unwrap(); node.set_pow_mining_settings(true, MICRO_IUNA).unwrap(); let plan = node.prepare_automatic_mining(1); @@ -1554,6 +1614,28 @@ mod tests { .contains("fee exceeds reward") ); assert!(node.status().mining.pow_mining_enabled); + assert!( + node.status() + .mining + .last_auto_pow_mine_status + .as_deref() + .unwrap_or_default() + .contains("fee exceeds reward") + ); + } + + #[test] + fn automatic_pow_status_reports_setup_placeholder_wait() { + let wallet = Wallet::from_seed("automatic-pow-setup-placeholder-wallet"); + let ledger = Ledger::new(BTreeMap::new(), 1); + let mut node = NodeCore::from_ledger(wallet, ledger, 0); + + node.set_pow_mining_enabled(true); + + assert_eq!( + node.status().mining.last_auto_pow_mine_status.as_deref(), + Some("waiting for a real chain before PoW mining can start") + ); } #[test] diff --git a/src/main.rs b/src/main.rs @@ -115,26 +115,26 @@ async fn main() -> Result<()> { }; } - if has_chain { - let persistence_node = Arc::clone(&node); - let persistence_store = chain_store.clone(); - tokio::spawn(async move { - run_chain_persistence(persistence_node, persistence_store).await; - }); - - let finalizer_node = Arc::clone(&node); - let finalizer_gossip = gossip.clone(); - tokio::spawn(async move { - run_automatic_finalizer(finalizer_node, finalizer_gossip).await; - }); - - let sync_node = Arc::clone(&node); - let sync_gossip = gossip.clone(); - tokio::spawn(async move { - run_peer_sync(sync_node, sync_gossip).await; - }); - } else { - println!("setup mode: no chain selected; skipping finalization and chain persistence"); + let persistence_node = Arc::clone(&node); + let persistence_store = chain_store.clone(); + tokio::spawn(async move { + run_chain_persistence(persistence_node, persistence_store).await; + }); + + let finalizer_node = Arc::clone(&node); + let finalizer_gossip = gossip.clone(); + tokio::spawn(async move { + run_automatic_finalizer(finalizer_node, finalizer_gossip).await; + }); + + let sync_node = Arc::clone(&node); + let sync_gossip = gossip.clone(); + tokio::spawn(async move { + run_peer_sync(sync_node, sync_gossip).await; + }); + + if !has_chain { + println!("setup mode: waiting to join or create a chain"); } http::serve( @@ -525,6 +525,10 @@ async fn join_chain_ledger(join_peers: &[String], advertised_addr: SocketAddr) - async fn run_automatic_finalizer(node: SharedNode, gossip: p2p::GossipNetwork) { let mut last_logged_skip: Option<(u64, String)> = None; loop { + if !node.lock().await.has_real_chain() { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + continue; + } let (height, plan, outbox) = { let mut node = node.lock().await; let height = node.chain_height(); @@ -537,6 +541,14 @@ async fn run_automatic_finalizer(node: SharedNode, gossip: p2p::GossipNetwork) { eprintln!("p2p broadcast failed after automatic burn: {error:#}"); } + if let Some(tx) = &plan.pow_mined { + println!( + "auto-pow queued mine action for height {} ({})", + height, + tx.signature() + ); + } + let Some(work) = plan.work else { if let Some(reason) = &plan.skipped_reason { let skip = (height, reason.clone()); @@ -618,7 +630,13 @@ async fn run_chain_persistence_with_interval( let mut last_saved_tip: Option<String> = None; loop { tokio::time::sleep(interval).await; - let snapshot = { node.lock().await.chain_snapshot() }; + let snapshot = { + let node = node.lock().await; + if !node.has_real_chain() { + continue; + } + node.chain_snapshot() + }; let Some(tip_hash) = snapshot.blocks.last().map(|block| block.hash.clone()) else { continue; }; @@ -1117,4 +1135,23 @@ VALUES (1, 4, 'bad-tip', '{"not":"a chain snapshot"}', 0) assert_eq!(restored_tip.as_deref(), Some(expected_tip.as_str())); } + + #[tokio::test] + async fn persistence_loop_skips_setup_placeholder_chain() { + let dir = tempdir().unwrap(); + let store = SqliteChainStore::open(dir.path().join("chain.sqlite3")).unwrap(); + let wallet = Wallet::from_seed("background-persistence-setup"); + let ledger = Ledger::new(BTreeMap::new(), 1); + let node = Arc::new(Mutex::new(NodeCore::from_ledger(wallet, ledger, 0))); + + let persistence_task = tokio::spawn(run_chain_persistence_with_interval( + Arc::clone(&node), + store.clone(), + Duration::from_millis(10), + )); + tokio::time::sleep(Duration::from_millis(50)).await; + persistence_task.abort(); + + assert!(store.load().unwrap().is_none()); + } }