darkfid/task/
miner.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use darkfi::{
20    blockchain::{BlockInfo, Header, HeaderHash},
21    rpc::{jsonrpc::JsonNotification, util::JsonValue},
22    system::{ExecutorPtr, StoppableTask, Subscription},
23    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
24    util::{encoding::base64, time::Timestamp},
25    validator::{
26        consensus::{Fork, Proposal},
27        utils::best_fork_index,
28        verification::apply_producer_transaction,
29    },
30    zk::{empty_witnesses, ProvingKey, ZkCircuit},
31    zkas::ZkBinary,
32    Error, Result,
33};
34use darkfi_money_contract::{
35    client::pow_reward_v1::PoWRewardCallBuilder, MoneyFunction, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
36};
37use darkfi_sdk::{
38    crypto::{poseidon_hash, FuncId, MerkleTree, PublicKey, SecretKey, MONEY_CONTRACT_ID},
39    pasta::pallas,
40    ContractCall,
41};
42use darkfi_serial::{serialize_async, Encodable};
43use log::{error, info};
44use num_bigint::BigUint;
45use rand::rngs::OsRng;
46use smol::channel::{Receiver, Sender};
47
48use crate::{proto::ProposalMessage, task::garbage_collect_task, DarkfiNodePtr};
49
50/// Auxiliary structure representing node miner rewards recipient configuration
51pub struct MinerRewardsRecipientConfig {
52    pub recipient: PublicKey,
53    pub spend_hook: Option<FuncId>,
54    pub user_data: Option<pallas::Base>,
55}
56
57/// Async task used for participating in the PoW block production.
58///
59/// Miner initializes their setup and waits for next confirmation,
60/// by listenning for new proposals from the network, for optimal
61/// conditions. After confirmation occurs, they start the actual
62/// miner loop, where they first grab the best ranking fork to extend,
63/// and start mining procedure for its next block. Additionally, they
64/// listen to the network for new proposals, and check if these
65/// proposals produce a new best ranking fork. If they do, the stop
66/// mining. These two tasks run in parallel, and after one of them
67/// finishes, node triggers confirmation check.
68pub async fn miner_task(
69    node: &DarkfiNodePtr,
70    recipient_config: &MinerRewardsRecipientConfig,
71    skip_sync: bool,
72    ex: &ExecutorPtr,
73) -> Result<()> {
74    // Initialize miner configuration
75    info!(target: "darkfid::task::miner_task", "Starting miner task...");
76
77    // Grab zkas proving keys and bin for PoWReward transaction
78    info!(target: "darkfid::task::miner_task", "Generating zkas bin and proving keys...");
79    let (zkbin, _) = node.validator.blockchain.contracts.get_zkas(
80        &node.validator.blockchain.sled_db,
81        &MONEY_CONTRACT_ID,
82        MONEY_CONTRACT_ZKAS_MINT_NS_V1,
83    )?;
84    let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
85    let pk = ProvingKey::build(zkbin.k, &circuit);
86
87    // Generate a random master secret key, to derive all signing keys from.
88    // This enables us to deanonimize proposals from reward recipient(miner).
89    // TODO: maybe miner wants to keep this master secret so they can
90    //       verify their signature in the future?
91    info!(target: "darkfid::task::miner_task", "Generating signing key...");
92    let mut secret = SecretKey::random(&mut OsRng);
93
94    // Grab blocks subscriber
95    let block_sub = node.subscribers.get("blocks").unwrap();
96
97    // Grab proposals subscriber and subscribe to it
98    let proposals_sub = node.subscribers.get("proposals").unwrap();
99    let subscription = proposals_sub.publisher.clone().subscribe().await;
100
101    // Listen for blocks until next confirmation, for optimal conditions
102    if !skip_sync {
103        info!(target: "darkfid::task::miner_task", "Waiting for next confirmation...");
104        loop {
105            subscription.receive().await;
106
107            // Check if we can confirmation anything and broadcast them
108            let confirmed = node.validator.confirmation().await?;
109
110            if confirmed.is_empty() {
111                continue
112            }
113
114            let mut notif_blocks = Vec::with_capacity(confirmed.len());
115            for block in confirmed {
116                notif_blocks
117                    .push(JsonValue::String(base64::encode(&serialize_async(&block).await)));
118            }
119            block_sub.notify(JsonValue::Array(notif_blocks)).await;
120            break;
121        }
122    }
123
124    // Create channels so threads can signal each other
125    let (sender, stop_signal) = smol::channel::bounded(1);
126
127    // Create the garbage collection task using a dummy task
128    let gc_task = StoppableTask::new();
129    gc_task.clone().start(
130        async { Ok(()) },
131        |_| async { /* Do nothing */ },
132        Error::GarbageCollectionTaskStopped,
133        ex.clone(),
134    );
135
136    info!(target: "darkfid::task::miner_task", "Miner initialized successfully!");
137
138    // Start miner loop
139    loop {
140        // Grab best current fork
141        let forks = node.validator.consensus.forks.read().await;
142        let index = match best_fork_index(&forks) {
143            Ok(i) => i,
144            Err(e) => {
145                error!(
146                    target: "darkfid::task::miner_task",
147                    "Finding best fork index failed: {e}"
148                );
149                continue
150            }
151        };
152        let extended_fork = match forks[index].full_clone() {
153            Ok(f) => f,
154            Err(e) => {
155                error!(
156                    target: "darkfid::task::miner_task",
157                    "Fork full clone creation failed: {e}"
158                );
159                continue
160            }
161        };
162        drop(forks);
163
164        // Grab extended fork last proposal hash
165        let last_proposal_hash = extended_fork.last_proposal()?.hash;
166
167        // Start listenning for network proposals and mining next block for best fork.
168        match smol::future::or(
169            listen_to_network(node, last_proposal_hash, &subscription, &sender),
170            mine(
171                node,
172                extended_fork,
173                &mut secret,
174                recipient_config,
175                &zkbin,
176                &pk,
177                &stop_signal,
178                skip_sync,
179            ),
180        )
181        .await
182        {
183            Ok(_) => { /* Do nothing */ }
184            Err(Error::NetworkNotConnected) => {
185                error!(target: "darkfid::task::miner_task", "Node disconnected from the network");
186                subscription.unsubscribe().await;
187                return Err(Error::NetworkNotConnected)
188            }
189            Err(e) => {
190                error!(
191                    target: "darkfid::task::miner_task",
192                    "Error during listen_to_network() or mine(): {e}"
193                );
194                continue
195            }
196        }
197
198        // Check if we can confirm anything and broadcast them
199        let confirmed = match node.validator.confirmation().await {
200            Ok(f) => f,
201            Err(e) => {
202                error!(
203                    target: "darkfid::task::miner_task",
204                    "Confirmation failed: {e}"
205                );
206                continue
207            }
208        };
209
210        if confirmed.is_empty() {
211            continue
212        }
213
214        let mut notif_blocks = Vec::with_capacity(confirmed.len());
215        for block in confirmed {
216            notif_blocks.push(JsonValue::String(base64::encode(&serialize_async(&block).await)));
217        }
218        block_sub.notify(JsonValue::Array(notif_blocks)).await;
219
220        // Invoke the detached garbage collection task
221        gc_task.clone().stop().await;
222        gc_task.clone().start(
223            garbage_collect_task(node.clone()),
224            |res| async {
225                match res {
226                    Ok(()) | Err(Error::GarbageCollectionTaskStopped) => { /* Do nothing */ }
227                    Err(e) => {
228                        error!(target: "darkfid", "Failed starting garbage collection task: {e}")
229                    }
230                }
231            },
232            Error::GarbageCollectionTaskStopped,
233            ex.clone(),
234        );
235    }
236}
237
238/// Async task to listen for incoming proposals and check if the best fork has changed.
239async fn listen_to_network(
240    node: &DarkfiNodePtr,
241    last_proposal_hash: HeaderHash,
242    subscription: &Subscription<JsonNotification>,
243    sender: &Sender<()>,
244) -> Result<()> {
245    loop {
246        // Wait until a new proposal has been received
247        subscription.receive().await;
248
249        // Grab a lock over node forks
250        let forks = node.validator.consensus.forks.read().await;
251
252        // Grab best current fork index
253        let index = best_fork_index(&forks)?;
254
255        // Verify if proposals sequence has changed
256        if forks[index].last_proposal()?.hash != last_proposal_hash {
257            drop(forks);
258            break
259        }
260
261        drop(forks);
262    }
263
264    // Signal miner to abort mining
265    sender.send(()).await?;
266    if let Err(e) = node.miner_daemon_request("abort", &JsonValue::Array(vec![])).await {
267        error!(target: "darkfid::task::miner::listen_to_network", "Failed to execute miner daemon abort request: {e}");
268    }
269
270    Ok(())
271}
272
273/// Async task to generate and mine provided fork index next block,
274/// while listening for a stop signal.
275#[allow(clippy::too_many_arguments)]
276async fn mine(
277    node: &DarkfiNodePtr,
278    extended_fork: Fork,
279    secret: &mut SecretKey,
280    recipient_config: &MinerRewardsRecipientConfig,
281    zkbin: &ZkBinary,
282    pk: &ProvingKey,
283    stop_signal: &Receiver<()>,
284    skip_sync: bool,
285) -> Result<()> {
286    smol::future::or(
287        wait_stop_signal(stop_signal),
288        mine_next_block(node, extended_fork, secret, recipient_config, zkbin, pk, skip_sync),
289    )
290    .await
291}
292
293/// Async task to wait for listener's stop signal.
294pub async fn wait_stop_signal(stop_signal: &Receiver<()>) -> Result<()> {
295    // Clean stop signal channel
296    if stop_signal.is_full() {
297        stop_signal.recv().await?;
298    }
299
300    // Wait for listener signal
301    stop_signal.recv().await?;
302
303    Ok(())
304}
305
306/// Async task to generate and mine provided fork index next block.
307async fn mine_next_block(
308    node: &DarkfiNodePtr,
309    mut extended_fork: Fork,
310    secret: &mut SecretKey,
311    recipient_config: &MinerRewardsRecipientConfig,
312    zkbin: &ZkBinary,
313    pk: &ProvingKey,
314    skip_sync: bool,
315) -> Result<()> {
316    // Grab next target and block
317    let (next_target, mut next_block) = generate_next_block(
318        &mut extended_fork,
319        secret,
320        recipient_config,
321        zkbin,
322        pk,
323        node.validator.consensus.module.read().await.target,
324        node.validator.verify_fees,
325    )
326    .await?;
327
328    // Execute request to minerd and parse response
329    let target = JsonValue::String(next_target.to_string());
330    let block = JsonValue::String(base64::encode(&serialize_async(&next_block).await));
331    let response =
332        node.miner_daemon_request_with_retry("mine", &JsonValue::Array(vec![target, block])).await;
333    next_block.header.nonce = *response.get::<f64>().unwrap() as u64;
334
335    // Sign the mined block
336    next_block.sign(secret);
337
338    // Verify it
339    extended_fork.module.verify_current_block(&next_block)?;
340
341    // Check if we are connected to the network
342    if !skip_sync && !node.p2p_handler.p2p.is_connected() {
343        return Err(Error::NetworkNotConnected)
344    }
345
346    // Append the mined block as a proposal
347    let proposal = Proposal::new(next_block);
348    node.validator.append_proposal(&proposal).await?;
349
350    // Broadcast proposal to the network
351    let message = ProposalMessage(proposal);
352    node.p2p_handler.p2p.broadcast(&message).await;
353
354    Ok(())
355}
356
357/// Auxiliary function to generate next block in an atomic manner.
358async fn generate_next_block(
359    extended_fork: &mut Fork,
360    secret: &mut SecretKey,
361    recipient_config: &MinerRewardsRecipientConfig,
362    zkbin: &ZkBinary,
363    pk: &ProvingKey,
364    block_target: u32,
365    verify_fees: bool,
366) -> Result<(BigUint, BlockInfo)> {
367    // Grab forks' last block proposal(previous)
368    let last_proposal = extended_fork.last_proposal()?;
369
370    // Grab forks' next block height
371    let next_block_height = last_proposal.block.header.height + 1;
372
373    // Grab forks' unproposed transactions
374    let (mut txs, _, fees, overlay) = extended_fork
375        .unproposed_txs(&extended_fork.blockchain, next_block_height, block_target, verify_fees)
376        .await?;
377
378    // We are deriving the next secret key for optimization.
379    // Next secret is the poseidon hash of:
380    //  [prefix, current(previous) secret, signing(block) height].
381    let prefix = pallas::Base::from_raw([4, 0, 0, 0]);
382    let next_secret = poseidon_hash([prefix, secret.inner(), (next_block_height as u64).into()]);
383    *secret = SecretKey::from(next_secret);
384
385    // Generate reward transaction
386    let tx = generate_transaction(next_block_height, fees, secret, recipient_config, zkbin, pk)?;
387
388    // Apply producer transaction in the overlay
389    let _ = apply_producer_transaction(
390        &overlay,
391        next_block_height,
392        block_target,
393        &tx,
394        &mut MerkleTree::new(1),
395    )
396    .await?;
397    txs.push(tx);
398
399    // Grab the updated contracts states root
400    overlay.lock().unwrap().contracts.update_state_monotree(&mut extended_fork.state_monotree)?;
401    let Some(state_root) = extended_fork.state_monotree.get_headroot()? else {
402        return Err(Error::ContractsStatesRootNotFoundError);
403    };
404
405    // Drop new trees opened by the unproposed transactions overlay
406    overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
407
408    // Generate the new header
409    let mut header =
410        Header::new(last_proposal.hash, next_block_height, Timestamp::current_time(), 0);
411    header.state_root = state_root;
412
413    // Generate the block
414    let mut next_block = BlockInfo::new_empty(header);
415
416    // Add transactions to the block
417    next_block.append_txs(txs);
418
419    // Grab the next mine target
420    let target = extended_fork.module.next_mine_target()?;
421
422    Ok((target, next_block))
423}
424
425/// Auxiliary function to generate a Money::PoWReward transaction.
426fn generate_transaction(
427    block_height: u32,
428    fees: u64,
429    secret: &SecretKey,
430    recipient_config: &MinerRewardsRecipientConfig,
431    zkbin: &ZkBinary,
432    pk: &ProvingKey,
433) -> Result<Transaction> {
434    // Build the transaction debris
435    let debris = PoWRewardCallBuilder {
436        signature_public: PublicKey::from_secret(*secret),
437        block_height,
438        fees,
439        recipient: Some(recipient_config.recipient),
440        spend_hook: recipient_config.spend_hook,
441        user_data: recipient_config.user_data,
442        mint_zkbin: zkbin.clone(),
443        mint_pk: pk.clone(),
444    }
445    .build()?;
446
447    // Generate and sign the actual transaction
448    let mut data = vec![MoneyFunction::PoWRewardV1 as u8];
449    debris.params.encode(&mut data)?;
450    let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
451    let mut tx_builder =
452        TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
453    let mut tx = tx_builder.build()?;
454    let sigs = tx.create_sigs(&[*secret])?;
455    tx.signatures = vec![sigs];
456
457    Ok(tx)
458}