iuna

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

commit d20439af6d5862edb3281ed3c715ca285ec8ba62
parent 083e1536f109a026d64e669d483dbd5a934c6a66
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Fri, 31 Jul 2026 22:13:43 +0200

Run automatic PoW mining independently

Diffstat:
Msrc/app.rs | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 58+++++++++++++++++++++++++++++++++++++++++++++++-----------
2 files changed, 168 insertions(+), 11 deletions(-)

diff --git a/src/app.rs b/src/app.rs @@ -966,6 +966,79 @@ impl NodeCore { plan } + pub fn prepare_automatic_pow_mining(&mut self) -> Result<Option<Transaction>> { + if self.wallet.is_locked() { + if self.pow_mining_enabled { + self.last_auto_pow_mine_status = Some("wallet is locked".to_string()); + } + return Ok(None); + } + if self.pow_mining_enabled && !self.has_real_chain() { + self.last_auto_pow_mine_status = + Some("waiting for a real chain before PoW mining can start".to_string()); + self.auto_pow_mine_cursor = None; + return Ok(None); + } + + self.prepare_automatic_pow_mine() + } + + pub fn prepare_automatic_finalization(&mut self, timestamp_ms: u64) -> AutoMinePlan { + let mut plan = AutoMinePlan { + pow_mined: None, + burned: None, + work: None, + skipped_reason: None, + }; + + if self.wallet.is_locked() { + plan.skipped_reason = Some("wallet is locked".to_string()); + return plan; + } + + if !self.automatic_mining_enabled { + plan.skipped_reason = Some("automatic mining is off".to_string()); + return plan; + } + + match self.prepare_automatic_burn() { + Ok(tx) => plan.burned = tx, + Err(error) => { + plan.skipped_reason = Some(format!("automatic burn failed: {error:#}")); + return plan; + } + } + + let wallet_rank = self + .ledger + .finalizer_rank_for_next_block(self.wallet.address()); + if wallet_rank.is_none() { + let selected_leader = self.ledger.expected_leader_for_next_block(); + plan.skipped_reason = selected_leader.map(|leader| { + format!("wallet is waiting for selected finalizer {leader} to finish the VDF") + }); + return plan; + } + + match self + .ledger + .prepare_next_block(self.wallet.address(), timestamp_ms) + { + Ok(work) => { + plan.work = Some(work); + } + Err(error) => { + plan.skipped_reason = Some(format!("{error:#}")); + } + } + + plan + } + + pub fn record_automatic_pow_mining_error(&mut self, message: String) { + self.last_auto_pow_mine_status = Some(message); + } + fn prepare_automatic_pow_mine(&mut self) -> Result<Option<Transaction>> { if !self.pow_mining_enabled { self.last_auto_pow_mine_status = None; @@ -1889,6 +1962,54 @@ mod tests { } #[test] + fn automatic_pow_mining_can_tick_without_finalization() { + let wallet = Wallet::from_seed("automatic-pow-independent-wallet"); + let mut allocations = BTreeMap::new(); + allocations.insert(wallet.address().to_string(), 1); + let mut node = NodeCore::new(NodeConfig { + wallet, + genesis_allocations: allocations, + vdf_rounds: 10, + burn_per_block: 0, + burn_fee: 0, + }); + + node.set_pow_mining_enabled(true); + node.prepare_automatic_pow_mining().unwrap(); + let first_searched = node + .auto_pow_mine_cursor + .as_ref() + .expect("PoW cursor should be initialized") + .searched; + + node.prepare_automatic_pow_mining().unwrap(); + let second_searched = node + .auto_pow_mine_cursor + .as_ref() + .expect("PoW cursor should keep tracking the current tip") + .searched; + + assert!(second_searched > first_searched); + } + + #[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 { + wallet, + genesis_allocations: BTreeMap::new(), + vdf_rounds: 10, + burn_per_block: 0, + burn_fee: 0, + }); + + node.set_pow_mining_enabled(true); + let _ = node.prepare_automatic_finalization(1); + + assert!(node.auto_pow_mine_cursor.is_none()); + } + + #[test] fn automatic_pow_mining_uses_protocol_finalizer_fee() { let wallet = Wallet::from_seed("automatic-pow-mining-fee-wallet"); let mut node = NodeCore::new(NodeConfig { diff --git a/src/main.rs b/src/main.rs @@ -162,6 +162,12 @@ async fn main() -> Result<()> { run_automatic_finalizer(finalizer_node, finalizer_gossip, debug_logging).await; }); + let pow_miner_node = Arc::clone(&node); + let pow_miner_gossip = gossip.clone(); + tokio::spawn(async move { + run_automatic_pow_miner(pow_miner_node, pow_miner_gossip, debug_logging).await; + }); + let sync_node = Arc::clone(&node); let sync_gossip = gossip.clone(); tokio::spawn(async move { @@ -600,7 +606,7 @@ async fn run_automatic_finalizer(node: SharedNode, gossip: p2p::GossipNetwork, d let (height, plan, outbox) = { let mut node = node.lock().await; let height = node.chain_height(); - let plan = node.prepare_automatic_mining(now_ms()); + let plan = node.prepare_automatic_finalization(now_ms()); let outbox = node.drain_outbox(); (height, plan, outbox) }; @@ -611,16 +617,6 @@ async fn run_automatic_finalizer(node: SharedNode, gossip: p2p::GossipNetwork, d } } - if debug { - 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()); @@ -680,6 +676,46 @@ async fn run_automatic_finalizer(node: SharedNode, gossip: p2p::GossipNetwork, d } } +async fn run_automatic_pow_miner(node: SharedNode, gossip: p2p::GossipNetwork, debug: bool) { + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + let (height, pow_mined, outbox) = { + let mut node = node.lock().await; + if !node.has_real_chain() { + continue; + } + let height = node.chain_height(); + let pow_mined = match node.prepare_automatic_pow_mining() { + Ok(tx) => tx, + Err(error) => { + node.record_automatic_pow_mining_error(format!( + "automatic PoW mining failed: {error:#}" + )); + None + } + }; + let outbox = node.drain_outbox(); + (height, pow_mined, outbox) + }; + + if let Err(error) = gossip.broadcast(outbox).await { + if debug { + eprintln!("p2p broadcast failed after automatic PoW mining: {error:#}"); + } + } + + if debug { + if let Some(tx) = &pow_mined { + println!( + "auto-pow queued mine action for height {} ({})", + height, + tx.signature() + ); + } + } + } +} + async fn run_peer_sync(node: SharedNode, gossip: p2p::GossipNetwork, debug: bool) { loop { tokio::time::sleep(std::time::Duration::from_secs(5)).await;