/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2023 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{collections::HashMap, str::FromStr}; use darkfi::{ rpc::{ jsonrpc::{JsonRequest, JsonResponse}, util::JsonValue, }, Error, Result, }; use log::{debug, error, info}; use monero::blockdata::transaction::{ExtraField, RawExtraField, SubField::MergeMining}; use super::MiningProxy; /// Types of requests that can be sent to monerod pub(crate) enum MonerodRequest { Get(String), Post(JsonRequest), } impl MiningProxy { /// Perform a JSON-RPC request to monerod's endpoint with the given method pub(crate) async fn monero_request(&self, req: MonerodRequest) -> Result { let mut rep = match req { MonerodRequest::Get(method) => { let endpoint = format!("{}{}", self.monero_rpc, method); match surf::get(&endpoint).await { Ok(v) => v, Err(e) => { let e = format!("Failed sending monerod GET request: {}", e); error!(target: "monerod::monero_request", "{}", e); return Err(Error::Custom(e)) } } } MonerodRequest::Post(data) => { let endpoint = format!("{}json_rpc", self.monero_rpc); let client = surf::Client::new(); match client .get(endpoint) .header("Content-Type", "application/json") .body(data.stringify().unwrap()) .send() .await { Ok(v) => v, Err(e) => { let e = format!("Failed sending monerod POST request: {}", e); error!(target: "monerod::monero_request", "{}", e); return Err(Error::Custom(e)) } } } }; let json_rep: JsonValue = match rep.body_string().await { Ok(v) => match v.parse() { Ok(v) => v, Err(e) => { let e = format!("Failed parsing JSON string from monerod response: {}", e); error!(target: "monerod::monero_request", "{}", e); return Err(Error::Custom(e)) } }, Err(e) => { let e = format!("Failed parsing body string from monerod response: {}", e); error!(target: "monerod::monero_request", "{}", e); return Err(Error::Custom(e)) } }; Ok(json_rep) } /// Proxy the `/getheight` RPC request pub async fn monerod_get_height(&self) -> Result { info!(target: "monerod::getheight", "Proxying /getheight request"); let rep = self.monero_request(MonerodRequest::Get("getheight".to_string())).await?; Ok(rep) } /// Proxy the `/getinfo` RPC request pub async fn monerod_get_info(&self) -> Result { info!(target: "monerod::getinfo", "Proxying /getinfo request"); let rep = self.monero_request(MonerodRequest::Get("getinfo".to_string())).await?; Ok(rep) } /// Proxy the `submitblock` RPC request pub async fn monerod_submit_block(&self, req: &JsonValue) -> Result { info!(target: "monerod::submitblock", "Proxying submitblock request"); let request = JsonRequest::try_from(req)?; if !request.params.is_array() { return Err(Error::Custom("Invalid request".to_string())) } for block in request.params.get::>().unwrap() { let Some(block) = block.get::() else { return Err(Error::Custom("Invalid request".to_string())) }; debug!( target: "monerod::submitblock", "{:#?}", monero::consensus::deserialize::(&hex::decode(block).unwrap()).unwrap(), ); } let response = self.monero_request(MonerodRequest::Post(request)).await?; Ok(response) } /// Perform the `getblocktemplate` request and modify it with the necessary /// merge mining data. pub async fn monerod_getblocktemplate(&self, req: &JsonValue) -> Result { info!(target: "monerod::getblocktemplate", "Proxying getblocktemplate request"); let mut request = JsonRequest::try_from(req)?; if !request.params.is_object() { return Err(Error::Custom("Invalid request".to_string())) } let params: &mut HashMap = request.params.get_mut().unwrap(); if !params.contains_key("wallet_address") { return Err(Error::Custom("Invalid request".to_string())) } let Some(wallet_address) = params["wallet_address"].get::() else { return Err(Error::Custom("Invalid request".to_string())) }; let Ok(wallet_address) = monero::Address::from_str(wallet_address) else { return Err(Error::Custom("Invalid request".to_string())) }; if wallet_address.network != self.monero_network { return Err(Error::Custom("Monero network address mismatch".to_string())) } if wallet_address.addr_type != monero::AddressType::Standard { return Err(Error::Custom("Non-standard Monero address".to_string())) } // Create the Merge Mining data // TODO: This is where we're gonna include the necessary DarkFi data // that has to end up in Monero blocks. let mm_tag = MergeMining(Some(monero::VarInt(32)), monero::Hash([0_u8; 32])); // Construct `tx_extra` from all the extra fields we have to add to // the coinbase transaction in the block we're mining. let tx_extra: RawExtraField = ExtraField(vec![mm_tag]).into(); // Modify the params `reserve_size` to fit our Merge Mining data debug!(target: "monerod::getblocktemplate", "Inserting \"reserve_size\":{}", tx_extra.0.len()); params.insert("reserve_size".to_string(), (tx_extra.0.len() as f64).into()); // Remove `extra_nonce` from the request, XMRig tends to send this in daemon-mode params.remove("extra_nonce"); // Perform the `getblocktemplate` call: let gbt_response = self.monero_request(MonerodRequest::Post(request)).await?; debug!(target: "monerod::getblocktemplate", "Got {}", gbt_response.stringify()?); let mut gbt_response = JsonResponse::try_from(&gbt_response)?; let gbt_result: &mut HashMap = gbt_response.result.get_mut().unwrap(); // Now we have to modify the block template: let mut block_template = monero::consensus::deserialize::( &hex::decode(gbt_result["blocktemplate_blob"].get::().unwrap()).unwrap(), ) .unwrap(); // Update coinbase tx with our extra field block_template.miner_tx.prefix.extra = tx_extra; // Update `blocktemplate_blob` with the modified block: gbt_result.insert( "blocktemplate_blob".to_string(), hex::encode(monero::consensus::serialize(&block_template)).into(), ); // Update `blockhashing_blob` in order to perform correct PoW: gbt_result.insert( "blockhashing_blob".to_string(), hex::encode(block_template.serialize_hashable()).into(), ); // Return the modified JSON response Ok((&gbt_response).into()) } }