darkfid/rpc/
xmr.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 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::{
20    collections::{HashMap, HashSet},
21    str::FromStr,
22};
23
24use async_trait::async_trait;
25use hex::FromHex;
26use smol::lock::MutexGuard;
27use tinyjson::JsonValue;
28use tracing::{debug, error, info};
29
30use darkfi::{
31    blockchain::{
32        header_store::PowData,
33        monero::{
34            fixed_array::FixedByteArray, merkle_proof::MerkleProof, monero_block_deserialize,
35            MoneroPowData,
36        },
37        HeaderHash,
38    },
39    rpc::{
40        jsonrpc::{
41            ErrorCode, ErrorCode::InvalidParams, JsonError, JsonRequest, JsonResponse, JsonResult,
42        },
43        server::RequestHandler,
44    },
45    system::StoppableTaskPtr,
46};
47use darkfi_sdk::crypto::keypair::Network;
48
49use crate::{
50    error::{miner_status_response, server_error, RpcError},
51    registry::model::MinerRewardsRecipientConfig,
52    DarkfiNode,
53};
54
55// https://github.com/SChernykh/p2pool/blob/master/docs/MERGE_MINING.MD
56
57/// HTTP JSON-RPC `RequestHandler` for p2pool/merge mining
58pub struct MmRpcHandler;
59
60#[async_trait]
61#[rustfmt::skip]
62impl RequestHandler<MmRpcHandler> for DarkfiNode {
63    async fn handle_request(&self, req: JsonRequest) -> JsonResult {
64        debug!(target: "darkfid::rpc::rpc_xmr", "--> {}", req.stringify().unwrap());
65
66        match req.method.as_str() {
67            // ================================================
68            // P2Pool methods requested for Monero Merge Mining
69            // ================================================
70            "merge_mining_get_chain_id" => self.xmr_merge_mining_get_chain_id(req.id, req.params).await,
71            "merge_mining_get_aux_block" => self.xmr_merge_mining_get_aux_block(req.id, req.params).await,
72            "merge_mining_submit_solution" => self.xmr_merge_mining_submit_solution(req.id, req.params).await,
73            _ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
74        }
75    }
76
77    async fn connections_mut(&self) -> MutexGuard<'life0, HashSet<StoppableTaskPtr>> {
78        self.registry.mm_rpc_connections.lock().await
79    }
80}
81
82impl DarkfiNode {
83    // RPCAPI:
84    // Gets a unique ID that identifies this merge mined chain and
85    // separates it from other chains.
86    //
87    // * `chain_id`: A unique 32-byte hash that identifies this merge
88    //   mined chain.
89    //
90    // darkfid will send the hash:
91    //  H(genesis_hash || network || hard_fork_height)
92    //
93    // --> {"jsonrpc": "2.0", "method": "merge_mining_get_chain_id", "id": 1}
94    // <-- {"jsonrpc": "2.0", "result": {"chain_id": "0f28c...7863"}, "id": 1}
95    pub async fn xmr_merge_mining_get_chain_id(&self, id: u16, params: JsonValue) -> 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 genesis block to use as chain identifier
105        let (_, genesis_hash) = match self.validator.blockchain.genesis() {
106            Ok(v) => v,
107            Err(e) => {
108                error!(
109                    target: "darkfid::rpc::rpc_xmr::xmr_merge_mining_get_chain_id",
110                    "[RPC-XMR] Error fetching genesis block hash: {e}"
111                );
112                return JsonError::new(ErrorCode::InternalError, None, id).into()
113            }
114        };
115
116        // Generate the chain id
117        let mut hasher = blake3::Hasher::new();
118        hasher.update(genesis_hash.inner());
119        match self.registry.network {
120            Network::Mainnet => hasher.update("mainnet".as_bytes()),
121            Network::Testnet => hasher.update("testnet".as_bytes()),
122        };
123        hasher.update(&0u32.to_le_bytes());
124        let chain_id = hasher.finalize().to_string();
125
126        let response = HashMap::from([("chain_id".to_string(), JsonValue::from(chain_id))]);
127        JsonResponse::new(JsonValue::from(response), id).into()
128    }
129
130    // RPCAPI:
131    // Gets a blob of data, the blocks hash and difficutly used for
132    // merge mining.
133    //
134    // **Request:**
135    // * `address` : A wallet address or its base-64 encoded mining configuration on the merge mined chain
136    // * `aux_hash`: Merge mining job that is currently being polled
137    // * `height`  : Monero height
138    // * `prev_id` : Hash of the previous Monero block
139    //
140    // **Response:**
141    // * `aux_blob`: A hex-encoded blob of empty data
142    // * `aux_diff`: Mining difficulty (decimal number)
143    // * `aux_hash`: A 32-byte hex-encoded hash of merge mined block
144    //
145    // --> {
146    //       "jsonrpc": "2.0",
147    //       "method": "merge_mining_get_aux_block",
148    //       "params": {
149    //         "address": "MERGE_MINED_CHAIN_ADDRESS",
150    //         "aux_hash": "f6952d6eef555ddd87aca66e56b91530222d6e318414816f3ba7cf5bf694bf0f",
151    //         "height": 3000000,
152    //         "prev_id":"ad505b0be8a49b89273e307106fa42133cbd804456724c5e7635bd953215d92a"
153    //       },
154    //       "id": 1
155    //     }
156    // <-- {
157    //       "jsonrpc":"2.0",
158    //       "result": {
159    //         "aux_blob": "fad344115...3151531",
160    //         "aux_diff": 123456,
161    //         "aux_hash":"f6952d6eef555ddd87aca66e56b91530222d6e318414816f3ba7cf5bf694bf0f"
162    //       },
163    //       "id": 1
164    //     }
165    pub async fn xmr_merge_mining_get_aux_block(&self, id: u16, params: JsonValue) -> JsonResult {
166        // Check if node is synced before responding to p2pool
167        if !*self.validator.synced.read().await {
168            return JsonResponse::new(JsonValue::from(HashMap::new()), id).into()
169        }
170
171        // Parse request params
172        let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
173            return JsonError::new(InvalidParams, None, id).into()
174        };
175
176        // Parse aux_hash
177        let Some(aux_hash) = params.get("aux_hash") else {
178            return server_error(RpcError::MinerMissingAuxHash, id, None)
179        };
180        let Some(aux_hash) = aux_hash.get::<String>() else {
181            return server_error(RpcError::MinerInvalidAuxHash, id, None)
182        };
183        if HeaderHash::from_str(aux_hash).is_err() {
184            return server_error(RpcError::MinerInvalidAuxHash, id, None)
185        };
186
187        // Check if we already have this job
188        if self.registry.mm_jobs.read().await.contains_key(&aux_hash.to_string()) {
189            return JsonResponse::new(JsonValue::from(HashMap::new()), id).into()
190        }
191
192        // Parse address
193        let Some(wallet) = params.get("address") else {
194            return server_error(RpcError::MinerMissingAddress, id, None)
195        };
196        let Some(wallet) = wallet.get::<String>() else {
197            return server_error(RpcError::MinerInvalidAddress, id, None)
198        };
199        let config =
200            match MinerRewardsRecipientConfig::from_str(&self.registry.network, wallet).await {
201                Ok(c) => c,
202                Err(e) => return server_error(e, id, None),
203            };
204
205        // Parse height
206        let Some(height) = params.get("height") else {
207            return server_error(RpcError::MinerMissingHeight, id, None)
208        };
209        let Some(height) = height.get::<f64>() else {
210            return server_error(RpcError::MinerInvalidHeight, id, None)
211        };
212        let height = *height as u64;
213
214        // Parse prev_id
215        let Some(prev_id) = params.get("prev_id") else {
216            return server_error(RpcError::MinerMissingPrevId, id, None)
217        };
218        let Some(prev_id) = prev_id.get::<String>() else {
219            return server_error(RpcError::MinerInvalidPrevId, id, None)
220        };
221        let Ok(prev_id) = hex::decode(prev_id) else {
222            return server_error(RpcError::MinerInvalidPrevId, id, None)
223        };
224        let prev_id = monero::Hash::from_slice(&prev_id);
225
226        // Register the new merge miner
227        let (job_id, difficulty) =
228            match self.registry.register_merge_miner(&self.validator, wallet, &config).await {
229                Ok(p) => p,
230                Err(e) => {
231                    error!(
232                        target: "darkfid::rpc::rpc_xmr::xmr_merge_mining_get_aux_block",
233                        "[RPC-XMR] Failed to register merge miner: {e}",
234                    );
235                    return JsonResponse::new(JsonValue::from(HashMap::new()), id).into()
236                }
237            };
238
239        // Now we have the new job, we ship it to RPC
240        info!(
241            target: "darkfid::rpc::rpc_xmr::xmr_merge_mining_get_aux_block",
242            "[RPC-XMR] Created new merge mining job: aux_hash={job_id}, height={height}, prev_id={prev_id}"
243        );
244        let response = JsonValue::from(HashMap::from([
245            ("aux_blob".to_string(), JsonValue::from(hex::encode(vec![]))),
246            ("aux_diff".to_string(), JsonValue::from(difficulty)),
247            ("aux_hash".to_string(), JsonValue::from(job_id)),
248        ]));
249        JsonResponse::new(response, id).into()
250    }
251
252    // RPCAPI:
253    // Submits a PoW solution for the merge mined chain's block. Note that
254    // when merge mining with Monero, the PoW solution is always a Monero
255    // block template with merge mining data included into it.
256    //
257    // **Request:**
258    // * `aux_blob`: Blob of data returned by `merge_mining_get_aux_block`
259    // * `aux_hash`: A 32-byte hex-encoded hash of merge mined block
260    // * `blob`: Monero block template that has enough PoW to satisfy the difficulty
261    //   returned by `merge_mining_get_aux_block`. It must also have a merge mining
262    //   tag in `tx_extra` of the coinbase transaction.
263    // * `merkle_proof`: A proof that `aux_hash` was included when calculating the
264    //   Merkle root hash from the merge mining tag
265    // * `path`: A path bitmap (32-bit unsigned integer) that complements `merkle_proof`
266    // * `seed_hash`: A 32-byte hex-encoded key that is used to initialize the
267    //   RandomX dataset
268    //
269    // **Response:**
270    // * `status`: Block submit status
271    //
272    // --> {
273    //       "jsonrpc":"2.0",
274    //       "method": "merge_mining_submit_solution",
275    //       "params": {
276    //         "aux_blob": "124125....35215136",
277    //         "aux_hash": "f6952d6eef555ddd87aca66e56b91530222d6e318414816f3ba7cf5bf694bf0f",
278    //         "blob": "...",
279    //         "merkle_proof": ["hash1", "hash2", "hash3"],
280    //         "path": 3,
281    //         "seed_hash": "22c3d47c595ae888b5d7fc304235f92f8854644d4fad38c5680a5d4a81009fcd"
282    //       },
283    //       "id": 1
284    //     }
285    // <-- {"jsonrpc":"2.0", "result": {"status": "accepted"}, "id": 1}
286    pub async fn xmr_merge_mining_submit_solution(&self, id: u16, params: JsonValue) -> JsonResult {
287        // Check if node is synced before responding to p2pool
288        if !*self.validator.synced.read().await {
289            return miner_status_response(id, "rejected")
290        }
291
292        // Grab registry submissions lock
293        let submit_lock = self.registry.submit_lock.write().await;
294
295        // Parse request params
296        let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
297            return JsonError::new(InvalidParams, None, id).into()
298        };
299
300        // Parse aux_hash
301        let Some(aux_hash) = params.get("aux_hash") else {
302            return server_error(RpcError::MinerMissingAuxHash, id, None)
303        };
304        let Some(aux_hash) = aux_hash.get::<String>() else {
305            return server_error(RpcError::MinerInvalidAuxHash, id, None)
306        };
307        if HeaderHash::from_str(aux_hash).is_err() {
308            return server_error(RpcError::MinerInvalidAuxHash, id, None)
309        }
310
311        // If we don't know about this mm job, we can just abort here
312        let mut mm_jobs = self.registry.mm_jobs.write().await;
313        let Some(wallet) = mm_jobs.get(aux_hash) else {
314            return miner_status_response(id, "rejected")
315        };
316
317        // If this job wallet template doesn't exist, we can just
318        // abort here.
319        let mut block_templates = self.registry.block_templates.write().await;
320        let Some(block_template) = block_templates.get_mut(wallet) else {
321            return miner_status_response(id, "rejected")
322        };
323
324        // If this template has been already submitted, reject this
325        // submission.
326        if block_template.submitted {
327            return miner_status_response(id, "rejected")
328        }
329
330        // Parse aux_blob
331        let Some(aux_blob) = params.get("aux_blob") else {
332            return server_error(RpcError::MinerMissingAuxBlob, id, None)
333        };
334        let Some(aux_blob) = aux_blob.get::<String>() else {
335            return server_error(RpcError::MinerInvalidAuxBlob, id, None)
336        };
337        let Ok(aux_blob) = hex::decode(aux_blob) else {
338            return server_error(RpcError::MinerInvalidAuxBlob, id, None)
339        };
340        if !aux_blob.is_empty() {
341            return server_error(RpcError::MinerInvalidAuxBlob, id, None)
342        }
343
344        // Parse blob
345        let Some(blob) = params.get("blob") else {
346            return server_error(RpcError::MinerMissingBlob, id, None)
347        };
348        let Some(blob) = blob.get::<String>() else {
349            return server_error(RpcError::MinerInvalidBlob, id, None)
350        };
351        let Ok(block) = monero_block_deserialize(blob) else {
352            return server_error(RpcError::MinerInvalidBlob, id, None)
353        };
354
355        // Parse merkle_proof
356        let Some(merkle_proof_j) = params.get("merkle_proof") else {
357            return server_error(RpcError::MinerMissingMerkleProof, id, None)
358        };
359        let Some(merkle_proof_j) = merkle_proof_j.get::<Vec<JsonValue>>() else {
360            return server_error(RpcError::MinerInvalidMerkleProof, id, None)
361        };
362        let mut merkle_proof: Vec<monero::Hash> = Vec::with_capacity(merkle_proof_j.len());
363        for hash in merkle_proof_j.iter() {
364            match hash.get::<String>() {
365                Some(v) => {
366                    let Ok(val) = monero::Hash::from_hex(v) else {
367                        return server_error(RpcError::MinerInvalidMerkleProof, id, None)
368                    };
369
370                    merkle_proof.push(val);
371                }
372                None => return server_error(RpcError::MinerInvalidMerkleProof, id, None),
373            }
374        }
375
376        // Parse path
377        let Some(path) = params.get("path") else {
378            return server_error(RpcError::MinerMissingPath, id, None)
379        };
380        let Some(path) = path.get::<f64>() else {
381            return server_error(RpcError::MinerInvalidPath, id, None)
382        };
383        let path = *path as u32;
384
385        // Parse seed_hash
386        let Some(seed_hash) = params.get("seed_hash") else {
387            return server_error(RpcError::MinerMissingSeedHash, id, None)
388        };
389        let Some(seed_hash) = seed_hash.get::<String>() else {
390            return server_error(RpcError::MinerInvalidSeedHash, id, None)
391        };
392        let Ok(seed_hash) = monero::Hash::from_hex(seed_hash) else {
393            return server_error(RpcError::MinerInvalidSeedHash, id, None)
394        };
395        let Ok(seed_hash) = FixedByteArray::from_bytes(seed_hash.as_bytes()) else {
396            return server_error(RpcError::MinerInvalidSeedHash, id, None)
397        };
398
399        info!(
400            target: "darkfid::rpc::rpc_xmr::xmr_merge_mining_submit_solution",
401            "[RPC-XMR] Got solution submission: aux_hash={aux_hash}",
402        );
403
404        // Construct the MoneroPowData
405        let Some(merkle_proof) = MerkleProof::try_construct(merkle_proof, path) else {
406            return server_error(RpcError::MinerMerkleProofConstructionFailed, id, None)
407        };
408        let monero_pow_data = match MoneroPowData::new(block, seed_hash, merkle_proof) {
409            Ok(v) => v,
410            Err(e) => {
411                error!(
412                    target: "darkfid::rpc::rpc_xmr::xmr_merge_mining_submit_solution",
413                    "[RPC-XMR] Failed constructing MoneroPowData: {e}",
414                );
415                return server_error(RpcError::MinerMoneroPowDataConstructionFailed, id, None)
416            }
417        };
418
419        // Append MoneroPowData to the DarkFi block and sign it
420        let mut block = block_template.block.clone();
421        block.header.pow_data = PowData::Monero(monero_pow_data);
422        block.sign(&block_template.secret);
423
424        // Submit the new block through the registry
425        if let Err(e) =
426            self.registry.submit(&self.validator, &self.subscribers, &self.p2p_handler, block).await
427        {
428            error!(
429                target: "darkfid::rpc::rpc_xmr::xmr_merge_mining_submit_solution",
430                "[RPC-XMR] Error submitting new block: {e}",
431            );
432
433            // Try to refresh the jobs before returning error
434            let mut jobs = self.registry.jobs.write().await;
435            if let Err(e) = self
436                .registry
437                .refresh_jobs(&mut block_templates, &mut jobs, &mut mm_jobs, &self.validator)
438                .await
439            {
440                error!(
441                    target: "darkfid::rpc::rpc_xmr::xmr_merge_mining_submit_solution",
442                    "[RPC-XMR] Error refreshing registry jobs: {e}",
443                );
444            }
445
446            // Release all locks
447            drop(block_templates);
448            drop(jobs);
449            drop(mm_jobs);
450            drop(submit_lock);
451
452            return miner_status_response(id, "rejected")
453        }
454
455        // Mark block as submitted
456        block_template.submitted = true;
457
458        // Release all locks
459        drop(block_templates);
460        drop(mm_jobs);
461        drop(submit_lock);
462
463        miner_status_response(id, "accepted")
464    }
465}