darkfi_mmproxy/
monerod.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    rpc::{
23        jsonrpc::{JsonRequest, JsonResponse},
24        util::JsonValue,
25    },
26    Error, Result,
27};
28use log::{debug, error, info};
29use monero::blockdata::transaction::{ExtraField, RawExtraField, SubField::MergeMining};
30
31use super::MiningProxy;
32
33/// Types of requests that can be sent to monerod
34pub(crate) enum MonerodRequest {
35    Get(String),
36    Post(JsonRequest),
37}
38
39impl MiningProxy {
40    /// Perform a JSON-RPC request to monerod's endpoint with the given method
41    pub(crate) async fn monero_request(&self, req: MonerodRequest) -> Result<JsonValue> {
42        let mut rep = match req {
43            MonerodRequest::Get(method) => {
44                let endpoint = format!("{}{}", self.monero_rpc, method);
45
46                match surf::get(&endpoint).await {
47                    Ok(v) => v,
48                    Err(e) => {
49                        let e = format!("Failed sending monerod GET request: {}", e);
50                        error!(target: "monerod::monero_request", "{}", e);
51                        return Err(Error::Custom(e))
52                    }
53                }
54            }
55            MonerodRequest::Post(data) => {
56                let endpoint = format!("{}json_rpc", self.monero_rpc);
57                let client = surf::Client::new();
58
59                match client
60                    .get(endpoint)
61                    .header("Content-Type", "application/json")
62                    .body(data.stringify().unwrap())
63                    .send()
64                    .await
65                {
66                    Ok(v) => v,
67                    Err(e) => {
68                        let e = format!("Failed sending monerod POST request: {}", e);
69                        error!(target: "monerod::monero_request", "{}", e);
70                        return Err(Error::Custom(e))
71                    }
72                }
73            }
74        };
75
76        let json_rep: JsonValue = match rep.body_string().await {
77            Ok(v) => match v.parse() {
78                Ok(v) => v,
79                Err(e) => {
80                    let e = format!("Failed parsing JSON string from monerod response: {}", e);
81                    error!(target: "monerod::monero_request", "{}", e);
82                    return Err(Error::Custom(e))
83                }
84            },
85            Err(e) => {
86                let e = format!("Failed parsing body string from monerod response:  {}", e);
87                error!(target: "monerod::monero_request", "{}", e);
88                return Err(Error::Custom(e))
89            }
90        };
91
92        Ok(json_rep)
93    }
94
95    /// Proxy the `/getheight` RPC request
96    pub async fn monerod_get_height(&self) -> Result<JsonValue> {
97        info!(target: "monerod::getheight", "Proxying /getheight request");
98        let rep = self.monero_request(MonerodRequest::Get("getheight".to_string())).await?;
99        Ok(rep)
100    }
101
102    /// Proxy the `/getinfo` RPC request
103    pub async fn monerod_get_info(&self) -> Result<JsonValue> {
104        info!(target: "monerod::getinfo", "Proxying /getinfo request");
105        let rep = self.monero_request(MonerodRequest::Get("getinfo".to_string())).await?;
106        Ok(rep)
107    }
108
109    /// Proxy the `submitblock` RPC request
110    pub async fn monerod_submit_block(&self, req: &JsonValue) -> Result<JsonValue> {
111        info!(target: "monerod::submitblock", "Proxying submitblock request");
112        let request = JsonRequest::try_from(req)?;
113
114        if !request.params.is_array() {
115            return Err(Error::Custom("Invalid request".to_string()))
116        }
117
118        for block in request.params.get::<Vec<JsonValue>>().unwrap() {
119            let Some(block) = block.get::<String>() else {
120                return Err(Error::Custom("Invalid request".to_string()))
121            };
122
123            debug!(
124                target: "monerod::submitblock", "{:#?}",
125                monero::consensus::deserialize::<monero::Block>(&hex::decode(block).unwrap()).unwrap(),
126            );
127        }
128
129        let response = self.monero_request(MonerodRequest::Post(request)).await?;
130        Ok(response)
131    }
132
133    /// Perform the `getblocktemplate` request and modify it with the necessary
134    /// merge mining data.
135    pub async fn monerod_getblocktemplate(&self, req: &JsonValue) -> Result<JsonValue> {
136        info!(target: "monerod::getblocktemplate", "Proxying getblocktemplate request");
137        let mut request = JsonRequest::try_from(req)?;
138
139        if !request.params.is_object() {
140            return Err(Error::Custom("Invalid request".to_string()))
141        }
142
143        let params: &mut HashMap<String, JsonValue> = request.params.get_mut().unwrap();
144        if !params.contains_key("wallet_address") {
145            return Err(Error::Custom("Invalid request".to_string()))
146        }
147
148        let Some(wallet_address) = params["wallet_address"].get::<String>() else {
149            return Err(Error::Custom("Invalid request".to_string()))
150        };
151
152        let Ok(wallet_address) = monero::Address::from_str(wallet_address) else {
153            return Err(Error::Custom("Invalid request".to_string()))
154        };
155
156        if wallet_address.network != self.monero_network {
157            return Err(Error::Custom("Monero network address mismatch".to_string()))
158        }
159
160        if wallet_address.addr_type != monero::AddressType::Standard {
161            return Err(Error::Custom("Non-standard Monero address".to_string()))
162        }
163
164        // Create the Merge Mining data
165        // TODO: This is where we're gonna include the necessary DarkFi data
166        // that has to end up in Monero blocks.
167        let mm_tag = MergeMining(monero::VarInt(32), monero::Hash([0_u8; 32]));
168
169        // Construct `tx_extra` from all the extra fields we have to add to
170        // the coinbase transaction in the block we're mining.
171        let tx_extra: RawExtraField = ExtraField(vec![mm_tag]).into();
172
173        // Modify the params `reserve_size` to fit our Merge Mining data
174        debug!(target: "monerod::getblocktemplate", "Inserting \"reserve_size\":{}", tx_extra.0.len());
175        params.insert("reserve_size".to_string(), (tx_extra.0.len() as f64).into());
176
177        // Remove `extra_nonce` from the request, XMRig tends to send this in daemon-mode
178        params.remove("extra_nonce");
179
180        // Perform the `getblocktemplate` call:
181        let gbt_response = self.monero_request(MonerodRequest::Post(request)).await?;
182        debug!(target: "monerod::getblocktemplate", "Got {}", gbt_response.stringify()?);
183        let mut gbt_response = JsonResponse::try_from(&gbt_response)?;
184        let gbt_result: &mut HashMap<String, JsonValue> = gbt_response.result.get_mut().unwrap();
185
186        // Now we have to modify the block template:
187        let mut block_template = monero::consensus::deserialize::<monero::Block>(
188            &hex::decode(gbt_result["blocktemplate_blob"].get::<String>().unwrap()).unwrap(),
189        )
190        .unwrap();
191
192        // Update coinbase tx with our extra field
193        block_template.miner_tx.prefix.extra = tx_extra;
194
195        // Update `blocktemplate_blob` with the modified block:
196        gbt_result.insert(
197            "blocktemplate_blob".to_string(),
198            hex::encode(monero::consensus::serialize(&block_template)).into(),
199        );
200
201        // Update `blockhashing_blob` in order to perform correct PoW:
202        gbt_result.insert(
203            "blockhashing_blob".to_string(),
204            hex::encode(block_template.serialize_hashable()).into(),
205        );
206
207        // Return the modified JSON response
208        Ok((&gbt_response).into())
209    }
210}