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:
| M | src/app.rs | | | 121 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/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;