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