rpc_xmr.rs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  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. use std::{collections::HashMap, str::FromStr};
  19. use darkfi::{
  20. blockchain::{
  21. header_store::PowData,
  22. monero::{
  23. extract_aux_merkle_root_from_block,
  24. fixed_array::{FixedByteArray, MaxSizeVec},
  25. merkle_proof::MerkleProof,
  26. monero_block_deserialize, MoneroPowData,
  27. },
  28. HeaderHash,
  29. },
  30. rpc::jsonrpc::{ErrorCode, ErrorCode::InvalidParams, JsonError, JsonResponse, JsonResult},
  31. validator::consensus::Proposal,
  32. };
  33. use darkfi_sdk::crypto::PublicKey;
  34. use hex::FromHex;
  35. use tinyjson::JsonValue;
  36. use tracing::{error, info};
  37. use crate::{
  38. proto::ProposalMessage,
  39. task::miner::{generate_next_block, MinerRewardsRecipientConfig},
  40. DarkfiNode,
  41. };
  42. // https://github.com/SChernykh/p2pool/blob/master/docs/MERGE_MINING.MD
  43. impl DarkfiNode {
  44. // RPCAPI:
  45. // Gets a unique ID that identifies this merge mined chain and
  46. // separates it from other chains.
  47. //
  48. // * `chain_id`: A unique 32-byte hex-encoded value that identifies
  49. // this merge mined chain.
  50. //
  51. // darkfid will send the hash of the genesis block header.
  52. //
  53. // --> {"jsonrpc":"2.0", "method": "merge_mining_get_chain_id", "id": 1}
  54. // <-- {"jsonrpc":"2.0", "result": {"chain_id": "0f28c...7863"}, "id": 1}
  55. pub async fn xmr_merge_mining_get_chain_id(&self, id: u16, _params: JsonValue) -> JsonResult {
  56. let (_, genesis_hash) = match self.validator.blockchain.genesis() {
  57. Ok(v) => v,
  58. Err(e) => {
  59. error!(
  60. target: "darkfid::rpc::xmr_merge_mining_get_chain_id",
  61. "[RPC-XMR] Error fetching genesis block hash: {e}"
  62. );
  63. return JsonError::new(ErrorCode::InternalError, None, id).into()
  64. }
  65. };
  66. // TODO: XXX: This should also have more specialized identifiers.
  67. // e.g. chain_id = H(genesis || aux_nonce || checkpoint_height)
  68. let response =
  69. HashMap::from([("chain_id".to_string(), JsonValue::from(genesis_hash.to_string()))]);
  70. JsonResponse::new(JsonValue::from(response), id).into()
  71. }
  72. // RPCAPI:
  73. // Gets a blob of data (usually a new block for the merge mined chain)
  74. // and its hash to be used for merge mining.
  75. //
  76. // **Request:**
  77. // * `address` - A wallet address on the merge mined chain
  78. // * `aux_hash` - Merge mining job that is currently being used
  79. // * `height` - Monero height
  80. // * `prev_id` - Hash of the previous Monero block
  81. //
  82. // **Response:**
  83. // * `aux_blob` - A hex-encoded blob of data. Merge mined chain defines the
  84. // contents of this blob. It's opaque to p2pool and will not be changed by it
  85. // * `aux_diff` - Mining difficulty (decimal number)
  86. // * `aux_hash` - A 32-byte hex-encoded hash of the `aux_blob`. Merge mined chain
  87. // defines how exactly this hash is calculated. It's opaque to p2pool.
  88. //
  89. // --> {"jsonrpc":"2.0", "method": "merge_mining_get_aux_block", "params": {"address": "MERGE_MINED_CHAIN_ADDRESS", "aux_hash": "f6952d6eef555ddd87aca66e56b91530222d6e318414816f3ba7cf5bf694bf0f", "height": 3000000, "prev_id":"ad505b0be8a49b89273e307106fa42133cbd804456724c5e7635bd953215d92a"}, "id": 1}
  90. // <-- {"jsonrpc":"2.0", "result": {"aux_blob": "4c6f72656d20697073756d", "aux_diff": 123456, "aux_hash":"f6952d6eef555ddd87aca66e56b91530222d6e318414816f3ba7cf5bf694bf0f"}, "id": 1}
  91. pub async fn xmr_merge_mining_get_aux_block(&self, id: u16, params: JsonValue) -> JsonResult {
  92. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  93. return JsonError::new(InvalidParams, None, id).into()
  94. };
  95. // Validate address
  96. let Some(address) = params.get("address") else {
  97. return JsonError::new(InvalidParams, Some("missing address".to_string()), id).into()
  98. };
  99. let Some(address) = address.get::<String>() else {
  100. return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
  101. .into()
  102. };
  103. let Ok(address) = PublicKey::from_str(address) else {
  104. return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
  105. .into()
  106. };
  107. // Validate aux_hash
  108. let Some(aux_hash) = params.get("aux_hash") else {
  109. return JsonError::new(InvalidParams, Some("missing aux_hash".to_string()), id).into()
  110. };
  111. let Some(aux_hash) = aux_hash.get::<String>() else {
  112. return JsonError::new(InvalidParams, Some("invalid aux_hash format".to_string()), id)
  113. .into()
  114. };
  115. let Ok(aux_hash) = HeaderHash::from_str(aux_hash) else {
  116. return JsonError::new(InvalidParams, Some("invalid aux_hash format".to_string()), id)
  117. .into()
  118. };
  119. // Validate height
  120. let Some(height) = params.get("height") else {
  121. return JsonError::new(InvalidParams, Some("missing height".to_string()), id).into()
  122. };
  123. let Some(height) = height.get::<f64>() else {
  124. return JsonError::new(InvalidParams, Some("invalid height format".to_string()), id)
  125. .into()
  126. };
  127. let height = *height as u64;
  128. // Validate prev_id
  129. let Some(prev_id) = params.get("prev_id") else {
  130. return JsonError::new(InvalidParams, Some("missing prev_id".to_string()), id).into()
  131. };
  132. let Some(prev_id) = prev_id.get::<String>() else {
  133. return JsonError::new(InvalidParams, Some("invalid prev_id format".to_string()), id)
  134. .into()
  135. };
  136. let Ok(prev_id) = hex::decode(prev_id) else {
  137. return JsonError::new(InvalidParams, Some("invalid prev_id format".to_string()), id)
  138. .into()
  139. };
  140. let prev_id = monero::Hash::from_slice(&prev_id);
  141. info!(
  142. target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
  143. "[RPC-XMR] Got blocktemplate request: address={}, aux_hash={}, height={}, prev_id={}",
  144. address, aux_hash, height, prev_id,
  145. );
  146. // Method params format is correct. Let's check if we provided this
  147. // mining job already. If so, we can just return an empty response.
  148. // We'll also obtain a lock here to avoid getting polled multiple
  149. // times and potentially missing a job. The lock is released when
  150. // this function exits.
  151. let mut mm_blocktemplates = self.mm_blocktemplates.lock().await;
  152. if mm_blocktemplates.contains_key(&aux_hash) {
  153. return JsonResponse::new(JsonValue::from(HashMap::new()), id).into()
  154. }
  155. // If it's a new job, clear the previous one(s).
  156. mm_blocktemplates.clear();
  157. // At this point, we should query the Validator for a new blocktemplate.
  158. // We first need to construct `MinerRewardsRecipientConfig` from the
  159. // address provided to us through the RPC.
  160. let recipient_config =
  161. MinerRewardsRecipientConfig { recipient: address, spend_hook: None, user_data: None };
  162. // Now let's try to construct the blocktemplate.
  163. let mut extended_fork = match self.best_current_fork().await {
  164. Ok(f) => f,
  165. Err(e) => {
  166. error!(
  167. target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
  168. "[RPC-XMR] Finding best fork index failed: {e}",
  169. );
  170. return JsonError::new(ErrorCode::InternalError, None, id).into()
  171. }
  172. };
  173. // Find the difficulty. Note we cast it to f64 here.
  174. let difficulty: f64 = match extended_fork.module.next_difficulty() {
  175. Ok(v) => {
  176. // We will attempt to cast it to f64. This should always work.
  177. v.to_string().parse().unwrap()
  178. }
  179. Err(e) => {
  180. error!(
  181. target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
  182. "[RPC-XMR] Finding next mining difficulty failed: {e}",
  183. );
  184. return JsonError::new(ErrorCode::InternalError, None, id).into()
  185. }
  186. };
  187. let (_, blocktemplate, block_signing_secret) = match generate_next_block(
  188. &mut extended_fork,
  189. &recipient_config,
  190. &self.powrewardv1_zk.zkbin,
  191. &self.powrewardv1_zk.provingkey,
  192. self.validator.consensus.module.read().await.target,
  193. self.validator.verify_fees,
  194. )
  195. .await
  196. {
  197. Ok(v) => v,
  198. Err(e) => {
  199. error!(
  200. target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
  201. "[RPC-XMR] Failed to generate next blocktemplate: {e}",
  202. );
  203. return JsonError::new(ErrorCode::InternalError, None, id).into()
  204. }
  205. };
  206. // Now we have the blocktemplate. We'll mark it down in memory,
  207. // and then ship it to RPC.
  208. let blockhash = blocktemplate.header.template_hash();
  209. mm_blocktemplates.insert(blockhash, (blocktemplate, block_signing_secret));
  210. let response = JsonValue::from(HashMap::from([
  211. ("aux_blob".to_string(), JsonValue::from(blockhash.as_string())),
  212. ("aux_diff".to_string(), JsonValue::from(difficulty)),
  213. ("aux_hash".to_string(), JsonValue::from(blockhash.as_string())),
  214. ]));
  215. info!("<-- {}", response.stringify().unwrap());
  216. JsonResponse::new(response, id).into()
  217. }
  218. // RPCAPI:
  219. // Submits a PoW solution for the merge mined chain's block. Note that
  220. // when merge mining with Monero, the PoW solution is always a Monero
  221. // block template with merge mining data included into it.
  222. //
  223. // **Request:**
  224. // * `aux_blob`: Blob of data returned by `merge_mining_get_aux_block`
  225. // * `aux_hash`: A 32-byte hex-encoded hash of the `aux_blob`, same as above.
  226. // * `blob`: Monero block template that has enough PoW to satisfy the difficulty
  227. // returned by `merge_mining_get_aux_block`. It must also have a merge mining
  228. // tag in `tx_extra` of the coinbase transaction.
  229. // * `merkle_proof`: A proof that `aux_hash` was included when calculating the
  230. // Merkle root hash from the merge mining tag
  231. // * `path`: A path bitmap (32-bit unsigned integer) that complements `merkle_proof`
  232. // * `seed_hash`: A 32-byte hex-encoded key that is used to initialize the
  233. // RandomX dataset
  234. //
  235. // **Response:**
  236. // * `status`: Block submit status
  237. //
  238. // --> {"jsonrpc":"2.0", "method": "merge_mining_submit_solution", "params": {"aux_blob": "4c6f72656d20697073756d", "aux_hash": "f6952d6eef555ddd87aca66e56b91530222d6e318414816f3ba7cf5bf694bf0f", "blob": "...", "merkle_proof": ["hash1", "hash2", "hash3"], "path": 3, "seed_hash": "22c3d47c595ae888b5d7fc304235f92f8854644d4fad38c5680a5d4a81009fcd"}, "id": 1}
  239. // <-- {"jsonrpc":"2.0", "result": {"status": "accepted"}, "id": 1}
  240. pub async fn xmr_merge_mining_submit_solution(&self, id: u16, params: JsonValue) -> JsonResult {
  241. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  242. return JsonError::new(InvalidParams, None, id).into()
  243. };
  244. // Validate aux_blob
  245. let Some(aux_blob) = params.get("aux_blob") else {
  246. return JsonError::new(InvalidParams, Some("missing aux_blob".to_string()), id).into()
  247. };
  248. let Some(aux_blob) = aux_blob.get::<String>() else {
  249. return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id)
  250. .into()
  251. };
  252. let Ok(_aux_blob) = HeaderHash::from_str(aux_blob) else {
  253. return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id)
  254. .into()
  255. };
  256. // Validate aux_hash
  257. let Some(aux_hash) = params.get("aux_hash") else {
  258. return JsonError::new(InvalidParams, Some("missing aux_hash".to_string()), id).into()
  259. };
  260. let Some(aux_hash) = aux_hash.get::<String>() else {
  261. return JsonError::new(InvalidParams, Some("invalid aux_hash format".to_string()), id)
  262. .into()
  263. };
  264. let Ok(aux_hash) = HeaderHash::from_str(aux_hash) else {
  265. return JsonError::new(InvalidParams, Some("invalid aux_hash format".to_string()), id)
  266. .into()
  267. };
  268. // If we don't know about this `aux_hash`, we can just abort here.
  269. let mut mm_blocktemplates = self.mm_blocktemplates.lock().await;
  270. if !mm_blocktemplates.contains_key(&aux_hash) {
  271. return JsonError::new(InvalidParams, Some("unknown aux_hash".to_string()), id).into()
  272. }
  273. // Validate blob
  274. let Some(blob) = params.get("blob") else {
  275. return JsonError::new(InvalidParams, Some("missing blob".to_string()), id).into()
  276. };
  277. let Some(blob) = blob.get::<String>() else {
  278. return JsonError::new(InvalidParams, Some("invalid blob format".to_string()), id).into()
  279. };
  280. let Ok(block) = monero_block_deserialize(blob) else {
  281. return JsonError::new(InvalidParams, Some("invalid blob format".to_string()), id).into()
  282. };
  283. // Validate merkle_proof
  284. let Some(merkle_proof_j) = params.get("merkle_proof") else {
  285. return JsonError::new(InvalidParams, Some("missing merkle_proof".to_string()), id)
  286. .into()
  287. };
  288. let Some(merkle_proof_j) = merkle_proof_j.get::<Vec<JsonValue>>() else {
  289. return JsonError::new(
  290. InvalidParams,
  291. Some("invalid merkle_proof format".to_string()),
  292. id,
  293. )
  294. .into()
  295. };
  296. let mut merkle_proof: Vec<monero::Hash> = Vec::with_capacity(merkle_proof_j.len());
  297. for hash in merkle_proof_j.iter() {
  298. match hash.get::<String>() {
  299. Some(v) => {
  300. let Ok(val) = monero::Hash::from_hex(v) else {
  301. return JsonError::new(
  302. InvalidParams,
  303. Some("invalid merkle_proof format".to_string()),
  304. id,
  305. )
  306. .into()
  307. };
  308. merkle_proof.push(val);
  309. }
  310. None => {
  311. return JsonError::new(
  312. InvalidParams,
  313. Some("invalid merkle_proof format".to_string()),
  314. id,
  315. )
  316. .into()
  317. }
  318. }
  319. }
  320. // Validate path
  321. let Some(path) = params.get("path") else {
  322. return JsonError::new(InvalidParams, Some("missing path".to_string()), id).into()
  323. };
  324. let Some(path) = path.get::<f64>() else {
  325. return JsonError::new(InvalidParams, Some("invalid path format".to_string()), id).into()
  326. };
  327. let path = *path as u32;
  328. // Validate seed_hash
  329. let Some(seed_hash) = params.get("seed_hash") else {
  330. return JsonError::new(InvalidParams, Some("missing seed_hash".to_string()), id).into()
  331. };
  332. let Some(seed_hash) = seed_hash.get::<String>() else {
  333. return JsonError::new(InvalidParams, Some("invalid seed_hash format".to_string()), id)
  334. .into()
  335. };
  336. let Ok(seed_hash) = monero::Hash::from_hex(seed_hash) else {
  337. return JsonError::new(InvalidParams, Some("invalid seed_hash format".to_string()), id)
  338. .into()
  339. };
  340. let Ok(seed_hash) = FixedByteArray::from_bytes(seed_hash.as_bytes()) else {
  341. return JsonError::new(InvalidParams, Some("invalid seed_hash format".to_string()), id)
  342. .into()
  343. };
  344. info!(
  345. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  346. "[RPC-XMR] Got solution submission: aux_hash={aux_hash}",
  347. );
  348. // =======================================
  349. // Now we will validate the block contents
  350. // =======================================
  351. let Ok(merkle_root) = extract_aux_merkle_root_from_block(&block) else {
  352. error!(
  353. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  354. "[RPC-XMR] Extracting aux_merkle_root from XMR block failed",
  355. );
  356. return JsonError::new(InvalidParams, None, id).into()
  357. };
  358. let Some(merkle_root) = merkle_root else {
  359. error!(
  360. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  361. "[RPC-XMR] Did not find merge mining hash in block",
  362. );
  363. return JsonError::new(InvalidParams, None, id).into()
  364. };
  365. // Verify the merge mining hash
  366. let Some(aux_hash_merkle_proof) = MerkleProof::try_construct(merkle_proof, path) else {
  367. return JsonError::new(
  368. InvalidParams,
  369. Some("invalid aux_hash merkle proof".to_string()),
  370. id,
  371. )
  372. .into()
  373. };
  374. if aux_hash_merkle_proof.calculate_root(&monero::Hash::from(aux_hash.inner())) !=
  375. merkle_root
  376. {
  377. error!(
  378. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  379. "[RPC-XMR] Could not validate aux_hash Merkle root",
  380. );
  381. return JsonError::new(
  382. InvalidParams,
  383. Some("invalid aux_hash merkle proof".to_string()),
  384. id,
  385. )
  386. .into()
  387. }
  388. // Construct MoneroPowData
  389. let aux_chain_hashes =
  390. MaxSizeVec::from_items_truncate(vec![monero::Hash::from(aux_hash.inner())]);
  391. let monero_pow_data =
  392. match MoneroPowData::new(block, seed_hash, aux_chain_hashes, *aux_hash.inner()) {
  393. Ok(v) => v,
  394. Err(e) => {
  395. error!(
  396. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  397. "[RPC-XMR] Failed constructing MoneroPowData: {e}",
  398. );
  399. return JsonError::new(
  400. InvalidParams,
  401. Some("failed constructing moneropowdata".to_string()),
  402. id,
  403. )
  404. .into()
  405. }
  406. };
  407. if !monero_pow_data.is_coinbase_valid_merkle_root() {
  408. error!(
  409. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  410. "[RPC-XMR] MoneroPowData invalid coinbase Merkle proof",
  411. );
  412. return JsonError::new(
  413. InvalidParams,
  414. Some("invalid coinbase merkle proof".to_string()),
  415. id,
  416. )
  417. .into()
  418. }
  419. // Append MoneroPowData to the DarkFi block and verify it.
  420. let extended_fork = match self.best_current_fork().await {
  421. Ok(f) => f,
  422. Err(e) => {
  423. error!(
  424. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  425. "[RPC-XMR] Finding best fork index failed: {e}",
  426. );
  427. return JsonError::new(ErrorCode::InternalError, None, id).into()
  428. }
  429. };
  430. let (block, secret) = &mm_blocktemplates.get(&aux_hash).unwrap();
  431. let mut block = block.clone();
  432. block.header.pow_data = PowData::Monero(monero_pow_data);
  433. block.sign(secret);
  434. if let Err(e) = extended_fork.module.verify_current_block(&block.header) {
  435. error!(
  436. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  437. "[RPC-XMR] Failed verifying merge mined block: {e}",
  438. );
  439. return JsonError::new(ErrorCode::InternalError, None, id).into()
  440. }
  441. info!(
  442. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  443. "[RPC-XMR] Success verifying merge mined block!",
  444. );
  445. // At this point we should be able to clear the working job.
  446. // We still won't release the lock in hope of proposing the block
  447. // first.
  448. mm_blocktemplates.clear();
  449. // Propose the new block.
  450. info!(
  451. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  452. "[RPC-XMR] Proposing new block to network",
  453. );
  454. let proposal = Proposal::new(block);
  455. if let Err(e) = self.validator.append_proposal(&proposal).await {
  456. error!(
  457. target: "darkfid::rpc_xmr::xmr_merge_submit_solution",
  458. "[RPC-XMR] Error proposing new block: {e}",
  459. );
  460. return JsonError::new(ErrorCode::InternalError, None, id).into()
  461. }
  462. info!(
  463. target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
  464. "[RPC-XMR] Broadcasting new block to network",
  465. );
  466. let message = ProposalMessage(proposal);
  467. self.p2p_handler.p2p.broadcast(&message).await;
  468. JsonResponse::new(
  469. JsonValue::from(HashMap::from([(
  470. "status".to_string(),
  471. JsonValue::from("accepted".to_string()),
  472. )])),
  473. id,
  474. )
  475. .into()
  476. }
  477. }