1use std::{collections::HashMap, str::FromStr};
20
21use darkfi::{
22 blockchain::{
23 header_store::PowData,
24 monero::{
25 fixed_array::FixedByteArray, merkle_proof::MerkleProof, monero_block_deserialize,
26 MoneroPowData,
27 },
28 HeaderHash,
29 },
30 rpc::jsonrpc::{ErrorCode, ErrorCode::InvalidParams, JsonError, JsonResponse, JsonResult},
31 util::encoding::base64,
32 validator::consensus::Proposal,
33};
34use darkfi_sdk::{
35 crypto::{pasta_prelude::PrimeField, FuncId, PublicKey},
36 pasta::pallas,
37};
38use darkfi_serial::{deserialize_async, serialize_async};
39use hex::FromHex;
40use tinyjson::JsonValue;
41use tracing::{error, info};
42
43use crate::{
44 proto::ProposalMessage,
45 rpc_miner::{generate_next_block, MinerRewardsRecipientConfig},
46 server_error, DarkfiNode, RpcError,
47};
48
49impl DarkfiNode {
52 pub async fn xmr_merge_mining_get_chain_id(&self, id: u16, params: JsonValue) -> JsonResult {
64 if !params.get::<Vec<JsonValue>>().unwrap().is_empty() {
66 return JsonError::new(InvalidParams, None, id).into()
67 }
68
69 let (_, genesis_hash) = match self.validator.blockchain.genesis() {
71 Ok(v) => v,
72 Err(e) => {
73 error!(
74 target: "darkfid::rpc::xmr_merge_mining_get_chain_id",
75 "[RPC-XMR] Error fetching genesis block hash: {e}"
76 );
77 return JsonError::new(ErrorCode::InternalError, None, id).into()
78 }
79 };
80
81 let response =
85 HashMap::from([("chain_id".to_string(), JsonValue::from(genesis_hash.to_string()))]);
86 JsonResponse::new(JsonValue::from(response), id).into()
87 }
88
89 pub async fn xmr_merge_mining_get_aux_block(&self, id: u16, params: JsonValue) -> JsonResult {
107 if !*self.validator.synced.read().await {
109 return server_error(RpcError::NotSynced, id, None)
110 }
111
112 let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
114 return JsonError::new(InvalidParams, None, id).into()
115 };
116
117 let Some(address) = params.get("address") else {
119 return JsonError::new(InvalidParams, Some("missing address".to_string()), id).into()
120 };
121 let Some(address) = address.get::<String>() else {
122 return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
123 .into()
124 };
125 let Some(address_bytes) = base64::decode(address) else {
126 return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
127 .into()
128 };
129 let Ok((recipient, spend_hook, user_data)) =
130 deserialize_async::<(PublicKey, Option<String>, Option<String>)>(&address_bytes).await
131 else {
132 return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
133 .into()
134 };
135 let spend_hook = match spend_hook {
136 Some(s) => match FuncId::from_str(&s) {
137 Ok(s) => Some(s),
138 Err(_) => {
139 return JsonError::new(
140 InvalidParams,
141 Some("invalid address format".to_string()),
142 id,
143 )
144 .into()
145 }
146 },
147 None => None,
148 };
149 let user_data: Option<pallas::Base> = match user_data {
150 Some(u) => {
151 let Ok(bytes) = bs58::decode(&u).into_vec() else {
152 return JsonError::new(
153 InvalidParams,
154 Some("invalid address format".to_string()),
155 id,
156 )
157 .into()
158 };
159 let bytes: [u8; 32] = match bytes.try_into() {
160 Ok(b) => b,
161 Err(_) => {
162 return JsonError::new(
163 InvalidParams,
164 Some("invalid address format".to_string()),
165 id,
166 )
167 .into()
168 }
169 };
170 match pallas::Base::from_repr(bytes).into() {
171 Some(v) => Some(v),
172 None => {
173 return JsonError::new(
174 InvalidParams,
175 Some("invalid address format".to_string()),
176 id,
177 )
178 .into()
179 }
180 }
181 }
182 None => None,
183 };
184
185 let Some(aux_hash) = params.get("aux_hash") else {
187 return JsonError::new(InvalidParams, Some("missing aux_hash".to_string()), id).into()
188 };
189 let Some(aux_hash) = aux_hash.get::<String>() else {
190 return JsonError::new(InvalidParams, Some("invalid aux_hash format".to_string()), id)
191 .into()
192 };
193 let Ok(aux_hash) = HeaderHash::from_str(aux_hash) else {
194 return JsonError::new(InvalidParams, Some("invalid aux_hash format".to_string()), id)
195 .into()
196 };
197
198 let Some(height) = params.get("height") else {
200 return JsonError::new(InvalidParams, Some("missing height".to_string()), id).into()
201 };
202 let Some(height) = height.get::<f64>() else {
203 return JsonError::new(InvalidParams, Some("invalid height format".to_string()), id)
204 .into()
205 };
206 let height = *height as u64;
207
208 let Some(prev_id) = params.get("prev_id") else {
210 return JsonError::new(InvalidParams, Some("missing prev_id".to_string()), id).into()
211 };
212 let Some(prev_id) = prev_id.get::<String>() else {
213 return JsonError::new(InvalidParams, Some("invalid prev_id format".to_string()), id)
214 .into()
215 };
216 let Ok(prev_id) = hex::decode(prev_id) else {
217 return JsonError::new(InvalidParams, Some("invalid prev_id format".to_string()), id)
218 .into()
219 };
220 let prev_id = monero::Hash::from_slice(&prev_id);
221
222 let mut mm_blocktemplates = self.mm_blocktemplates.lock().await;
234 let mut extended_fork = match self.validator.best_current_fork().await {
235 Ok(f) => f,
236 Err(e) => {
237 error!(
238 target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
239 "[RPC-XMR] Finding best fork index failed: {e}",
240 );
241 return JsonError::new(ErrorCode::InternalError, None, id).into()
242 }
243 };
244 if let Some((block, difficulty, _)) = mm_blocktemplates.get(&address_bytes) {
245 let last_proposal = match extended_fork.last_proposal() {
246 Ok(p) => p,
247 Err(e) => {
248 error!(
249 target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
250 "[RPC-XMR] Retrieving best fork last proposal failed: {e}",
251 );
252 return JsonError::new(ErrorCode::InternalError, None, id).into()
253 }
254 };
255 if last_proposal.hash == block.header.previous {
256 let blockhash = block.header.template_hash();
257 return if blockhash != aux_hash {
258 JsonResponse::new(
259 JsonValue::from(HashMap::from([
260 ("aux_blob".to_string(), JsonValue::from(hex::encode(address_bytes))),
261 ("aux_diff".to_string(), JsonValue::from(*difficulty)),
262 ("aux_hash".to_string(), JsonValue::from(blockhash.as_string())),
263 ])),
264 id,
265 )
266 .into()
267 } else {
268 JsonResponse::new(JsonValue::from(HashMap::new()), id).into()
269 }
270 }
271 mm_blocktemplates.remove(&address_bytes);
272 }
273
274 let recipient_str = format!("{recipient}");
278 let spend_hook_str = match spend_hook {
279 Some(spend_hook) => format!("{spend_hook}"),
280 None => String::from("-"),
281 };
282 let user_data_str = match user_data {
283 Some(user_data) => bs58::encode(user_data.to_repr()).into_string(),
284 None => String::from("-"),
285 };
286 let recipient_config = MinerRewardsRecipientConfig { recipient, spend_hook, user_data };
287
288 let difficulty: f64 = match extended_fork.module.next_difficulty() {
291 Ok(v) => {
292 v.to_string().parse().unwrap()
294 }
295 Err(e) => {
296 error!(
297 target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
298 "[RPC-XMR] Finding next mining difficulty failed: {e}",
299 );
300 return JsonError::new(ErrorCode::InternalError, None, id).into()
301 }
302 };
303
304 let (_, blocktemplate, block_signing_secret) = match generate_next_block(
305 &mut extended_fork,
306 &recipient_config,
307 &self.powrewardv1_zk.zkbin,
308 &self.powrewardv1_zk.provingkey,
309 self.validator.consensus.module.read().await.target,
310 self.validator.verify_fees,
311 )
312 .await
313 {
314 Ok(v) => v,
315 Err(e) => {
316 error!(
317 target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
318 "[RPC-XMR] Failed to generate next blocktemplate: {e}",
319 );
320 return JsonError::new(ErrorCode::InternalError, None, id).into()
321 }
322 };
323
324 let blockhash = blocktemplate.header.template_hash();
327 mm_blocktemplates
328 .insert(address_bytes.clone(), (blocktemplate, difficulty, block_signing_secret));
329 info!(
330 target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
331 "[RPC-XMR] Created new blocktemplate: address={recipient_str}, spend_hook={spend_hook_str}, user_data={user_data_str}, aux_hash={blockhash}, height={height}, prev_id={prev_id}"
332 );
333
334 let response = JsonValue::from(HashMap::from([
335 ("aux_blob".to_string(), JsonValue::from(hex::encode(address_bytes))),
336 ("aux_diff".to_string(), JsonValue::from(difficulty)),
337 ("aux_hash".to_string(), JsonValue::from(blockhash.as_string())),
338 ]));
339
340 JsonResponse::new(response, id).into()
341 }
342
343 pub async fn xmr_merge_mining_submit_solution(&self, id: u16, params: JsonValue) -> JsonResult {
366 if !*self.validator.synced.read().await {
368 return server_error(RpcError::NotSynced, id, None)
369 }
370
371 let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
373 return JsonError::new(InvalidParams, None, id).into()
374 };
375
376 let Some(aux_blob) = params.get("aux_blob") else {
378 return JsonError::new(InvalidParams, Some("missing aux_blob".to_string()), id).into()
379 };
380 let Some(aux_blob) = aux_blob.get::<String>() else {
381 return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id)
382 .into()
383 };
384 let Ok(address_bytes) = hex::decode(aux_blob) else {
385 return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id)
386 .into()
387 };
388 let Ok((_, spend_hook, user_data)) =
389 deserialize_async::<(PublicKey, Option<String>, Option<String>)>(&address_bytes).await
390 else {
391 return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id)
392 .into()
393 };
394 if let Some(spend_hook) = spend_hook {
395 if FuncId::from_str(&spend_hook).is_err() {
396 return JsonError::new(
397 InvalidParams,
398 Some("invalid aux_blob format".to_string()),
399 id,
400 )
401 .into()
402 }
403 };
404 if let Some(user_data) = user_data {
405 let Ok(bytes) = bs58::decode(&user_data).into_vec() else {
406 return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
407 .into()
408 };
409 let bytes: [u8; 32] = match bytes.try_into() {
410 Ok(b) => b,
411 Err(_) => {
412 return JsonError::new(
413 InvalidParams,
414 Some("invalid aux_blob format".to_string()),
415 id,
416 )
417 .into()
418 }
419 };
420 let _: pallas::Base = match pallas::Base::from_repr(bytes).into() {
421 Some(v) => v,
422 None => {
423 return JsonError::new(
424 InvalidParams,
425 Some("invalid aux_blob format".to_string()),
426 id,
427 )
428 .into()
429 }
430 };
431 };
432
433 let Some(aux_hash) = params.get("aux_hash") else {
435 return JsonError::new(InvalidParams, Some("missing aux_hash".to_string()), id).into()
436 };
437 let Some(aux_hash) = aux_hash.get::<String>() else {
438 return JsonError::new(InvalidParams, Some("invalid aux_hash format".to_string()), id)
439 .into()
440 };
441 let Ok(aux_hash) = HeaderHash::from_str(aux_hash) else {
442 return JsonError::new(InvalidParams, Some("invalid aux_hash format".to_string()), id)
443 .into()
444 };
445
446 let mut mm_blocktemplates = self.mm_blocktemplates.lock().await;
448 if !mm_blocktemplates.contains_key(&address_bytes) {
449 return JsonError::new(InvalidParams, Some("unknown address".to_string()), id).into()
450 }
451
452 let Some(blob) = params.get("blob") else {
454 return JsonError::new(InvalidParams, Some("missing blob".to_string()), id).into()
455 };
456 let Some(blob) = blob.get::<String>() else {
457 return JsonError::new(InvalidParams, Some("invalid blob format".to_string()), id).into()
458 };
459 let Ok(block) = monero_block_deserialize(blob) else {
460 return JsonError::new(InvalidParams, Some("invalid blob format".to_string()), id).into()
461 };
462
463 let Some(merkle_proof_j) = params.get("merkle_proof") else {
465 return JsonError::new(InvalidParams, Some("missing merkle_proof".to_string()), id)
466 .into()
467 };
468 let Some(merkle_proof_j) = merkle_proof_j.get::<Vec<JsonValue>>() else {
469 return JsonError::new(
470 InvalidParams,
471 Some("invalid merkle_proof format".to_string()),
472 id,
473 )
474 .into()
475 };
476 let mut merkle_proof: Vec<monero::Hash> = Vec::with_capacity(merkle_proof_j.len());
477 for hash in merkle_proof_j.iter() {
478 match hash.get::<String>() {
479 Some(v) => {
480 let Ok(val) = monero::Hash::from_hex(v) else {
481 return JsonError::new(
482 InvalidParams,
483 Some("invalid merkle_proof format".to_string()),
484 id,
485 )
486 .into()
487 };
488
489 merkle_proof.push(val);
490 }
491 None => {
492 return JsonError::new(
493 InvalidParams,
494 Some("invalid merkle_proof format".to_string()),
495 id,
496 )
497 .into()
498 }
499 }
500 }
501
502 let Some(path) = params.get("path") else {
504 return JsonError::new(InvalidParams, Some("missing path".to_string()), id).into()
505 };
506 let Some(path) = path.get::<f64>() else {
507 return JsonError::new(InvalidParams, Some("invalid path format".to_string()), id).into()
508 };
509 let path = *path as u32;
510
511 let Some(seed_hash) = params.get("seed_hash") else {
513 return JsonError::new(InvalidParams, Some("missing seed_hash".to_string()), id).into()
514 };
515 let Some(seed_hash) = seed_hash.get::<String>() else {
516 return JsonError::new(InvalidParams, Some("invalid seed_hash format".to_string()), id)
517 .into()
518 };
519 let Ok(seed_hash) = monero::Hash::from_hex(seed_hash) else {
520 return JsonError::new(InvalidParams, Some("invalid seed_hash format".to_string()), id)
521 .into()
522 };
523 let Ok(seed_hash) = FixedByteArray::from_bytes(seed_hash.as_bytes()) else {
524 return JsonError::new(InvalidParams, Some("invalid seed_hash format".to_string()), id)
525 .into()
526 };
527
528 info!(
529 target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
530 "[RPC-XMR] Got solution submission: aux_hash={aux_hash}",
531 );
532
533 let Some(merkle_proof) = MerkleProof::try_construct(merkle_proof, path) else {
535 return JsonError::new(
536 InvalidParams,
537 Some("could not construct aux chain merkle proof".to_string()),
538 id,
539 )
540 .into()
541 };
542 let monero_pow_data = match MoneroPowData::new(block, seed_hash, merkle_proof) {
543 Ok(v) => v,
544 Err(e) => {
545 error!(
546 target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
547 "[RPC-XMR] Failed constructing MoneroPowData: {e}",
548 );
549 return JsonError::new(
550 InvalidParams,
551 Some("failed constructing moneropowdata".to_string()),
552 id,
553 )
554 .into()
555 }
556 };
557
558 let (block, _, secret) = &mm_blocktemplates.get(&address_bytes).unwrap();
560 let mut block = block.clone();
561 block.header.pow_data = PowData::Monero(monero_pow_data);
562 block.sign(secret);
563
564 mm_blocktemplates.remove(&address_bytes);
568
569 info!(
571 target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
572 "[RPC-XMR] Proposing new block to network",
573 );
574 let proposal = Proposal::new(block);
575 if let Err(e) = self.validator.append_proposal(&proposal).await {
576 error!(
577 target: "darkfid::rpc_xmr::xmr_merge_submit_solution",
578 "[RPC-XMR] Error proposing new block: {e}",
579 );
580 return JsonResponse::new(
581 JsonValue::from(HashMap::from([(
582 "status".to_string(),
583 JsonValue::from("rejected".to_string()),
584 )])),
585 id,
586 )
587 .into()
588 }
589
590 let proposals_sub = self.subscribers.get("proposals").unwrap();
591 let enc_prop = JsonValue::String(base64::encode(&serialize_async(&proposal).await));
592 proposals_sub.notify(vec![enc_prop].into()).await;
593
594 info!(
595 target: "darkfid::rpc_xmr::xmr_merge_mining_submit_solution",
596 "[RPC-XMR] Broadcasting new block to network",
597 );
598 let message = ProposalMessage(proposal);
599 self.p2p_handler.p2p.broadcast(&message).await;
600
601 JsonResponse::new(
602 JsonValue::from(HashMap::from([(
603 "status".to_string(),
604 JsonValue::from("accepted".to_string()),
605 )])),
606 id,
607 )
608 .into()
609 }
610}