darkfid/
rpc_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 std::{collections::HashMap, str::FromStr};
20
21use darkfi::{
22    blockchain::{BlockInfo, Header, HeaderHash},
23    rpc::jsonrpc::{ErrorCode, ErrorCode::InvalidParams, JsonError, JsonResponse, JsonResult},
24    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
25    util::{encoding::base64, time::Timestamp},
26    validator::{
27        consensus::{Fork, Proposal},
28        pow::{RANDOMX_KEY_CHANGE_DELAY, RANDOMX_KEY_CHANGING_HEIGHT},
29        verification::apply_producer_transaction,
30    },
31    zk::ProvingKey,
32    zkas::ZkBinary,
33    Error, Result,
34};
35use darkfi_money_contract::{client::pow_reward_v1::PoWRewardCallBuilder, MoneyFunction};
36use darkfi_sdk::{
37    crypto::{
38        pasta_prelude::PrimeField, FuncId, Keypair, MerkleTree, PublicKey, SecretKey,
39        MONEY_CONTRACT_ID,
40    },
41    pasta::pallas,
42    ContractCall,
43};
44use darkfi_serial::{serialize_async, Encodable};
45use num_bigint::BigUint;
46use rand::rngs::OsRng;
47use tinyjson::JsonValue;
48use tracing::{error, info};
49
50use crate::{proto::ProposalMessage, server_error, DarkfiNode, RpcError};
51
52/// Auxiliary structure representing node miner rewards recipient configuration.
53pub struct MinerRewardsRecipientConfig {
54    /// Wallet mining address to receive mining rewards
55    pub recipient: PublicKey,
56    /// Optional contract spend hook to use in the mining reward
57    pub spend_hook: Option<FuncId>,
58    /// Optional contract user data to use in the mining reward.
59    /// This is not arbitrary data.
60    pub user_data: Option<pallas::Base>,
61}
62
63/// Auxiliary structure representing a block template for native mining.
64pub struct BlockTemplate {
65    /// The block that is being mined
66    pub block: BlockInfo,
67    /// The base64 encoded RandomX key used
68    randomx_key: String,
69    /// The base64 encoded next RandomX key used
70    next_randomx_key: String,
71    /// The base64 encoded mining target used
72    target: String,
73    /// The signing secret for this block
74    secret: SecretKey,
75}
76
77impl DarkfiNode {
78    // RPCAPI:
79    // Queries the validator for the current and next RandomX keys.
80    // If no forks exist, retrieves the canonical ones.
81    // Returns the current and next RandomX keys, both encoded as
82    // base64 strings.
83    //
84    // **Params:**
85    // * `None`
86    //
87    // **Returns:**
88    // * `String`: Current RandomX key (base64 encoded)
89    // * `String`: Current next RandomX key (base64 encoded)
90    //
91    // --> {"jsonrpc": "2.0", "method": "miner.get_current_randomx_keys", "params": [], "id": 1}
92    // <-- {"jsonrpc": "2.0", "result": ["randomx_key", "next_randomx_key"], "id": 1}
93    pub async fn miner_get_current_randomx_keys(&self, id: u16, params: JsonValue) -> JsonResult {
94        // Verify request params
95        let Some(params) = params.get::<Vec<JsonValue>>() else {
96            return JsonError::new(InvalidParams, None, id).into()
97        };
98        if !params.is_empty() {
99            return JsonError::new(InvalidParams, None, id).into()
100        }
101
102        // Grab current RandomX keys
103        let (randomx_key, next_randomx_key) = match self.validator.current_randomx_keys().await {
104            Ok(keys) => keys,
105            Err(e) => {
106                error!(
107                    target: "darkfid::rpc::miner_get_current_randomx_keys",
108                    "[RPC] Retrieving current RandomX keys failed: {e}",
109                );
110                return JsonError::new(ErrorCode::InternalError, None, id).into()
111            }
112        };
113
114        // Encode them and build response
115        let response = JsonValue::Array(vec![
116            JsonValue::String(base64::encode(&serialize_async(&randomx_key).await)),
117            JsonValue::String(base64::encode(&serialize_async(&next_randomx_key).await)),
118        ]);
119
120        JsonResponse::new(response, id).into()
121    }
122
123    // RPCAPI:
124    // Queries the validator for the current best fork next header to
125    // mine.
126    // Returns the current RandomX key, the mining target and the next
127    // block header, all encoded as base64 strings.
128    //
129    // **Params:**
130    // * `header`    : Mining job Header hash that is currently being polled (as string)
131    // * `recipient` : Wallet mining address to receive mining rewards (as string)
132    // * `spend_hook`: Optional contract spend hook to use in the mining reward (as string)
133    // * `user_data` : Optional contract user data (not arbitrary data) to use in the mining reward (as string)
134    //
135    // **Returns:**
136    // * `String`: Current best fork RandomX key (base64 encoded)
137    // * `String`: Current best fork next RandomX key (base64 encoded)
138    // * `String`: Current best fork mining target (base64 encoded)
139    // * `String`: Current best fork next block header (base64 encoded)
140    //
141    // --> {"jsonrpc": "2.0", "method": "miner.get_header", "params": {"header": "hash", "recipient": "address"}, "id": 1}
142    // <-- {"jsonrpc": "2.0", "result": ["randomx_key", "next_randomx_key", "target", "header"], "id": 1}
143    pub async fn miner_get_header(&self, id: u16, params: JsonValue) -> JsonResult {
144        // Check if node is synced before responding to miner
145        if !*self.validator.synced.read().await {
146            return server_error(RpcError::NotSynced, id, None)
147        }
148
149        // Parse request params
150        let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
151            return JsonError::new(InvalidParams, None, id).into()
152        };
153        if params.len() < 2 || params.len() > 4 {
154            return JsonError::new(InvalidParams, None, id).into()
155        }
156
157        // Parse header hash
158        let Some(header_hash) = params.get("header") else {
159            return server_error(RpcError::MinerMissingHeader, id, None)
160        };
161        let Some(header_hash) = header_hash.get::<String>() else {
162            return server_error(RpcError::MinerInvalidHeader, id, None)
163        };
164        let Ok(header_hash) = HeaderHash::from_str(header_hash) else {
165            return server_error(RpcError::MinerInvalidHeader, id, None)
166        };
167
168        // Parse recipient wallet address
169        let Some(recipient) = params.get("recipient") else {
170            return server_error(RpcError::MinerMissingRecipient, id, None)
171        };
172        let Some(recipient) = recipient.get::<String>() else {
173            return server_error(RpcError::MinerInvalidRecipient, id, None)
174        };
175        let Ok(recipient) = PublicKey::from_str(recipient) else {
176            return server_error(RpcError::MinerInvalidRecipient, id, None)
177        };
178
179        // Parse spend hook
180        let spend_hook = match params.get("spend_hook") {
181            Some(spend_hook) => {
182                let Some(spend_hook) = spend_hook.get::<String>() else {
183                    return server_error(RpcError::MinerInvalidSpendHook, id, None)
184                };
185                let Ok(spend_hook) = FuncId::from_str(spend_hook) else {
186                    return server_error(RpcError::MinerInvalidSpendHook, id, None)
187                };
188                Some(spend_hook)
189            }
190            None => None,
191        };
192
193        // Parse user data
194        let user_data: Option<pallas::Base> = match params.get("user_data") {
195            Some(user_data) => {
196                let Some(user_data) = user_data.get::<String>() else {
197                    return server_error(RpcError::MinerInvalidUserData, id, None)
198                };
199                let Ok(bytes) = bs58::decode(&user_data).into_vec() else {
200                    return server_error(RpcError::MinerInvalidUserData, id, None)
201                };
202                let bytes: [u8; 32] = match bytes.try_into() {
203                    Ok(b) => b,
204                    Err(_) => return server_error(RpcError::MinerInvalidUserData, id, None),
205                };
206                let Some(user_data) = pallas::Base::from_repr(bytes).into() else {
207                    return server_error(RpcError::MinerInvalidUserData, id, None)
208                };
209                Some(user_data)
210            }
211            None => None,
212        };
213
214        // Now that method params format is correct, we can check if we
215        // already have a mining job for this wallet. If we already
216        // have it, we check if the fork it extends is still the best
217        // one. If both checks pass, we can just return an empty
218        // response if the request `aux_hash` matches the job one,
219        // otherwise return the job block template hash. In case the
220        // best fork has changed, we drop this job and generate a
221        // new one. If we don't know this wallet, we create a new job.
222        // We'll also obtain a lock here to avoid getting polled
223        // multiple times and potentially missing a job. The lock is
224        // released when this function exits.
225        let address_bytes = serialize_async(&(recipient, spend_hook, user_data)).await;
226        let mut blocktemplates = self.blocktemplates.lock().await;
227        let mut extended_fork = match self.validator.best_current_fork().await {
228            Ok(f) => f,
229            Err(e) => {
230                error!(
231                    target: "darkfid::rpc::miner_get_header",
232                    "[RPC] Finding best fork index failed: {e}",
233                );
234                return JsonError::new(ErrorCode::InternalError, None, id).into()
235            }
236        };
237        if let Some(blocktemplate) = blocktemplates.get(&address_bytes) {
238            let last_proposal = match extended_fork.last_proposal() {
239                Ok(p) => p,
240                Err(e) => {
241                    error!(
242                        target: "darkfid::rpc::miner_get_header",
243                        "[RPC] Retrieving best fork last proposal failed: {e}",
244                    );
245                    return JsonError::new(ErrorCode::InternalError, None, id).into()
246                }
247            };
248            if last_proposal.hash == blocktemplate.block.header.previous {
249                return if blocktemplate.block.header.hash() != header_hash {
250                    JsonResponse::new(
251                        JsonValue::Array(vec![
252                            JsonValue::String(blocktemplate.randomx_key.clone()),
253                            JsonValue::String(blocktemplate.next_randomx_key.clone()),
254                            JsonValue::String(blocktemplate.target.clone()),
255                            JsonValue::String(base64::encode(
256                                &serialize_async(&blocktemplate.block.header).await,
257                            )),
258                        ]),
259                        id,
260                    )
261                    .into()
262                } else {
263                    JsonResponse::new(JsonValue::Array(vec![]), id).into()
264                }
265            }
266            blocktemplates.remove(&address_bytes);
267        }
268
269        // At this point, we should query the Validator for a new blocktemplate.
270        // We first need to construct `MinerRewardsRecipientConfig` from the
271        // address configuration provided to us through the RPC.
272        let recipient_str = format!("{recipient}");
273        let spend_hook_str = match spend_hook {
274            Some(spend_hook) => format!("{spend_hook}"),
275            None => String::from("-"),
276        };
277        let user_data_str = match user_data {
278            Some(user_data) => bs58::encode(user_data.to_repr()).into_string(),
279            None => String::from("-"),
280        };
281        let recipient_config = MinerRewardsRecipientConfig { recipient, spend_hook, user_data };
282
283        // Now let's try to construct the blocktemplate.
284        let (target, block, secret) = match generate_next_block(
285            &mut extended_fork,
286            &recipient_config,
287            &self.powrewardv1_zk.zkbin,
288            &self.powrewardv1_zk.provingkey,
289            self.validator.consensus.module.read().await.target,
290            self.validator.verify_fees,
291        )
292        .await
293        {
294            Ok(v) => v,
295            Err(e) => {
296                error!(
297                    target: "darkfid::rpc::miner_get_header",
298                    "[RPC] Failed to generate next blocktemplate: {e}",
299                );
300                return JsonError::new(ErrorCode::InternalError, None, id).into()
301            }
302        };
303
304        // Grab the RandomX key to use.
305        // We only use the next key when the next block is the
306        // height changing one.
307        let randomx_key = if block.header.height > RANDOMX_KEY_CHANGING_HEIGHT &&
308            block.header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY
309        {
310            base64::encode(&serialize_async(&extended_fork.module.darkfi_rx_keys.1).await)
311        } else {
312            base64::encode(&serialize_async(&extended_fork.module.darkfi_rx_keys.0).await)
313        };
314
315        // Grab the next RandomX key to use so miner can pregenerate
316        // mining VMs.
317        let next_randomx_key =
318            base64::encode(&serialize_async(&extended_fork.module.darkfi_rx_keys.1).await);
319
320        // Convert the target
321        let target = base64::encode(&target.to_bytes_le());
322
323        // Construct the block template
324        let blocktemplate = BlockTemplate {
325            block,
326            randomx_key: randomx_key.clone(),
327            next_randomx_key: next_randomx_key.clone(),
328            target: target.clone(),
329            secret,
330        };
331
332        // Now we have the blocktemplate. We'll mark it down in memory,
333        // and then ship it to RPC.
334        let header_hash = blocktemplate.block.header.hash().to_string();
335        let header = base64::encode(&serialize_async(&blocktemplate.block.header).await);
336        blocktemplates.insert(address_bytes, blocktemplate);
337        info!(
338            target: "darkfid::rpc::miner_get_header",
339            "[RPC] Created new blocktemplate: address={recipient_str}, spend_hook={spend_hook_str}, user_data={user_data_str}, hash={header_hash}"
340        );
341
342        let response = JsonValue::Array(vec![
343            JsonValue::String(randomx_key),
344            JsonValue::String(next_randomx_key),
345            JsonValue::String(target),
346            JsonValue::String(header),
347        ]);
348
349        JsonResponse::new(response, id).into()
350    }
351
352    // RPCAPI:
353    // Submits a PoW solution header nonce for a block.
354    // Returns the block submittion status.
355    //
356    // **Params:**
357    // * `recipient` : Wallet mining address used (as string)
358    // * `spend_hook`: Optional contract spend hook used (as string)
359    // * `user_data` : Optional contract user data (not arbitrary data) used (as string)
360    // * `nonce`     : The solution header nonce (as f64)
361    //
362    // **Returns:**
363    // * `String`: Block submit status
364    //
365    // --> {"jsonrpc": "2.0", "method": "miner.submit_solution", "params": {"recipient": "address", "nonce": 42}, "id": 1}
366    // <-- {"jsonrpc": "2.0", "result": "accepted", "id": 1}
367    pub async fn miner_submit_solution(&self, id: u16, params: JsonValue) -> JsonResult {
368        // Check if node is synced before responding to p2pool
369        if !*self.validator.synced.read().await {
370            return server_error(RpcError::NotSynced, id, None)
371        }
372
373        // Parse request params
374        let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
375            return JsonError::new(InvalidParams, None, id).into()
376        };
377        if params.len() < 2 || params.len() > 4 {
378            return JsonError::new(InvalidParams, None, id).into()
379        }
380
381        // Parse recipient wallet address
382        let Some(recipient) = params.get("recipient") else {
383            return server_error(RpcError::MinerMissingRecipient, id, None)
384        };
385        let Some(recipient) = recipient.get::<String>() else {
386            return server_error(RpcError::MinerInvalidRecipient, id, None)
387        };
388        let Ok(recipient) = PublicKey::from_str(recipient) else {
389            return server_error(RpcError::MinerInvalidRecipient, id, None)
390        };
391
392        // Parse spend hook
393        let spend_hook = match params.get("spend_hook") {
394            Some(spend_hook) => {
395                let Some(spend_hook) = spend_hook.get::<String>() else {
396                    return server_error(RpcError::MinerInvalidSpendHook, id, None)
397                };
398                let Ok(spend_hook) = FuncId::from_str(spend_hook) else {
399                    return server_error(RpcError::MinerInvalidSpendHook, id, None)
400                };
401                Some(spend_hook)
402            }
403            None => None,
404        };
405
406        // Parse user data
407        let user_data: Option<pallas::Base> = match params.get("user_data") {
408            Some(user_data) => {
409                let Some(user_data) = user_data.get::<String>() else {
410                    return server_error(RpcError::MinerInvalidUserData, id, None)
411                };
412                let Ok(bytes) = bs58::decode(&user_data).into_vec() else {
413                    return server_error(RpcError::MinerInvalidUserData, id, None)
414                };
415                let bytes: [u8; 32] = match bytes.try_into() {
416                    Ok(b) => b,
417                    Err(_) => return server_error(RpcError::MinerInvalidUserData, id, None),
418                };
419                let Some(user_data) = pallas::Base::from_repr(bytes).into() else {
420                    return server_error(RpcError::MinerInvalidUserData, id, None)
421                };
422                Some(user_data)
423            }
424            None => None,
425        };
426
427        // Parse nonce
428        let Some(nonce) = params.get("nonce") else {
429            return server_error(RpcError::MinerMissingNonce, id, None)
430        };
431        let Some(nonce) = nonce.get::<f64>() else {
432            return server_error(RpcError::MinerInvalidNonce, id, None)
433        };
434
435        // If we don't know about this job, we can just abort here.
436        let address_bytes = serialize_async(&(recipient, spend_hook, user_data)).await;
437        let mut blocktemplates = self.blocktemplates.lock().await;
438        let Some(blocktemplate) = blocktemplates.get(&address_bytes) else {
439            return server_error(RpcError::MinerUnknownJob, id, None)
440        };
441
442        info!(
443            target: "darkfid::rpc::miner_submit_solution",
444            "[RPC] Got solution submission for block template: {}", blocktemplate.block.header.hash(),
445        );
446
447        // Sign the DarkFi block
448        let mut block = blocktemplate.block.clone();
449        block.header.nonce = *nonce as u64;
450        block.sign(&blocktemplate.secret);
451        info!(
452            target: "darkfid::rpc::miner_submit_solution",
453            "[RPC] Mined block header hash: {}", blocktemplate.block.header.hash(),
454        );
455
456        // At this point we should be able to remove the submitted job.
457        // We still won't release the lock in hope of proposing the block
458        // first.
459        blocktemplates.remove(&address_bytes);
460
461        // Propose the new block
462        info!(
463            target: "darkfid::rpc::miner_submit_solution",
464            "[RPC] Proposing new block to network",
465        );
466        let proposal = Proposal::new(block);
467        if let Err(e) = self.validator.append_proposal(&proposal).await {
468            error!(
469                target: "darkfid::rpc::miner_submit_solution",
470                "[RPC] Error proposing new block: {e}",
471            );
472            return JsonResponse::new(JsonValue::String(String::from("rejected")), id).into()
473        }
474
475        let proposals_sub = self.subscribers.get("proposals").unwrap();
476        let enc_prop = JsonValue::String(base64::encode(&serialize_async(&proposal).await));
477        proposals_sub.notify(vec![enc_prop].into()).await;
478
479        info!(
480            target: "darkfid::rpc::miner_submit_solution",
481            "[RPC] Broadcasting new block to network",
482        );
483        let message = ProposalMessage(proposal);
484        self.p2p_handler.p2p.broadcast(&message).await;
485
486        JsonResponse::new(JsonValue::String(String::from("accepted")), id).into()
487    }
488}
489
490/// Auxiliary function to generate next block in an atomic manner.
491pub async fn generate_next_block(
492    extended_fork: &mut Fork,
493    recipient_config: &MinerRewardsRecipientConfig,
494    zkbin: &ZkBinary,
495    pk: &ProvingKey,
496    block_target: u32,
497    verify_fees: bool,
498) -> Result<(BigUint, BlockInfo, SecretKey)> {
499    // Grab forks' last block proposal(previous)
500    let last_proposal = extended_fork.last_proposal()?;
501
502    // Grab forks' next block height
503    let next_block_height = last_proposal.block.header.height + 1;
504
505    // Grab forks' unproposed transactions
506    let (mut txs, _, fees, overlay) = extended_fork
507        .unproposed_txs(&extended_fork.blockchain, next_block_height, block_target, verify_fees)
508        .await?;
509
510    // Create an ephemeral block signing keypair. Its secret key will
511    // be stored in the PowReward transaction's encrypted note for
512    // later retrieval. It is encrypted towards the recipient's public
513    // key.
514    let block_signing_keypair = Keypair::random(&mut OsRng);
515
516    // Generate reward transaction
517    let tx = generate_transaction(
518        next_block_height,
519        fees,
520        &block_signing_keypair,
521        recipient_config,
522        zkbin,
523        pk,
524    )?;
525
526    // Apply producer transaction in the overlay
527    let _ = apply_producer_transaction(
528        &overlay,
529        next_block_height,
530        block_target,
531        &tx,
532        &mut MerkleTree::new(1),
533    )
534    .await?;
535    txs.push(tx);
536
537    // Grab the updated contracts states root
538    overlay.lock().unwrap().contracts.update_state_monotree(&mut extended_fork.state_monotree)?;
539    let Some(state_root) = extended_fork.state_monotree.get_headroot()? else {
540        return Err(Error::ContractsStatesRootNotFoundError);
541    };
542
543    // Drop new trees opened by the unproposed transactions overlay
544    overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
545
546    // Generate the new header
547    let mut header =
548        Header::new(last_proposal.hash, next_block_height, Timestamp::current_time(), 0);
549    header.state_root = state_root;
550
551    // Generate the block
552    let mut next_block = BlockInfo::new_empty(header);
553
554    // Add transactions to the block
555    next_block.append_txs(txs);
556
557    // Grab the next mine target
558    let target = extended_fork.module.next_mine_target()?;
559
560    Ok((target, next_block, block_signing_keypair.secret))
561}
562
563/// Auxiliary function to generate a Money::PoWReward transaction.
564fn generate_transaction(
565    block_height: u32,
566    fees: u64,
567    block_signing_keypair: &Keypair,
568    recipient_config: &MinerRewardsRecipientConfig,
569    zkbin: &ZkBinary,
570    pk: &ProvingKey,
571) -> Result<Transaction> {
572    // Build the transaction debris
573    let debris = PoWRewardCallBuilder {
574        signature_keypair: *block_signing_keypair,
575        block_height,
576        fees,
577        recipient: Some(recipient_config.recipient),
578        spend_hook: recipient_config.spend_hook,
579        user_data: recipient_config.user_data,
580        mint_zkbin: zkbin.clone(),
581        mint_pk: pk.clone(),
582    }
583    .build()?;
584
585    // Generate and sign the actual transaction
586    let mut data = vec![MoneyFunction::PoWRewardV1 as u8];
587    debris.params.encode(&mut data)?;
588    let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
589    let mut tx_builder =
590        TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
591    let mut tx = tx_builder.build()?;
592    let sigs = tx.create_sigs(&[block_signing_keypair.secret])?;
593    tx.signatures = vec![sigs];
594
595    Ok(tx)
596}