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