darkfi_mmproxy/
main.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, sync::Arc};
20
21use darkfi::{
22    async_daemonize, cli_desc,
23    rpc::{
24        jsonrpc::{JsonRequest, JsonResponse},
25        util::JsonValue,
26    },
27    Error, Result,
28};
29use log::{debug, error, info};
30use serde::Deserialize;
31use smol::{stream::StreamExt, Executor};
32use structopt::StructOpt;
33use structopt_toml::StructOptToml;
34use surf::StatusCode;
35use url::Url;
36
37const CONFIG_FILE: &str = "darkfi_mmproxy.toml";
38const CONFIG_FILE_CONTENTS: &str = include_str!("../darkfi_mmproxy.toml");
39
40/// Monero RPC functions
41mod monerod;
42use monerod::MonerodRequest;
43
44#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
45#[serde(default)]
46#[structopt(name = "darkfi-mmproxy", about = cli_desc!())]
47struct Args {
48    #[structopt(short, parse(from_occurrences))]
49    /// Increase verbosity (-vvv supported)
50    verbose: u8,
51
52    #[structopt(short, long)]
53    /// Configuration file to use
54    config: Option<String>,
55
56    #[structopt(long)]
57    /// Set log file output
58    log: Option<String>,
59
60    #[structopt(flatten)]
61    mmproxy: MmproxyArgs,
62
63    #[structopt(flatten)]
64    monerod: MonerodArgs,
65}
66
67#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
68#[structopt()]
69struct MmproxyArgs {
70    #[structopt(long, default_value = "http://127.0.0.1:3333")]
71    /// darkfi-mmproxy JSON-RPC server listen URL
72    mmproxy_rpc: Url,
73}
74
75#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
76#[structopt()]
77struct MonerodArgs {
78    #[structopt(long, default_value = "mainnet")]
79    /// Monero network type (mainnet/testnet)
80    monero_network: String,
81
82    #[structopt(long, default_value = "http://127.0.0.1:18081")]
83    /// monerod JSON-RPC server listen URL
84    monero_rpc: Url,
85}
86
87/// Mining proxy state
88struct MiningProxy {
89    /// Monero network type
90    monero_network: monero::Network,
91    /// Monero RPC address
92    monero_rpc: Url,
93}
94
95impl MiningProxy {
96    /// Instantiate `MiningProxy` state
97    async fn new(monerod: MonerodArgs) -> Result<Self> {
98        let monero_network = match monerod.monero_network.to_lowercase().as_str() {
99            "mainnet" => monero::Network::Mainnet,
100            "testnet" => monero::Network::Testnet,
101            _ => {
102                error!("Invalid Monero network \"{}\"", monerod.monero_network);
103                return Err(Error::Custom(format!(
104                    "Invalid Monero network \"{}\"",
105                    monerod.monero_network
106                )))
107            }
108        };
109
110        // Test that monerod RPC is reachable and is configured
111        // with the matching network
112        let self_ = Self { monero_network, monero_rpc: monerod.monero_rpc };
113
114        let req = JsonRequest::new("getinfo", vec![].into());
115        let rep: JsonResponse = match self_.monero_request(MonerodRequest::Post(req)).await {
116            Ok(v) => JsonResponse::try_from(&v)?,
117            Err(e) => {
118                error!("Failed connecting to monerod RPC: {}", e);
119                return Err(e)
120            }
121        };
122
123        let Some(result) = rep.result.get::<HashMap<String, JsonValue>>() else {
124            error!("Invalid response from monerod RPC");
125            return Err(Error::Custom("Invalid response from monerod RPC".to_string()))
126        };
127
128        let nettype = result.get("nettype").unwrap().get::<String>().unwrap();
129
130        let mut xmr_is_mainnet = false;
131        let mut xmr_is_testnet = false;
132
133        match nettype.as_str() {
134            // Here we allow fakechain, which we get with monerod --regtest
135            "mainnet" | "fakechain" => xmr_is_mainnet = true,
136            "testnet" => xmr_is_testnet = true,
137            _ => unimplemented!("Missing handler for network {}", nettype),
138        }
139
140        if xmr_is_mainnet && monero_network != monero::Network::Mainnet {
141            error!("mmproxy requested testnet, but monerod is mainnet");
142            return Err(Error::Custom("Monero network mismatch".to_string()))
143        }
144
145        if xmr_is_testnet && monero_network != monero::Network::Testnet {
146            error!("mmproxy requested mainnet, but monerod is testnet");
147            return Err(Error::Custom("Monero network mismatch".to_string()))
148        }
149
150        Ok(self_)
151    }
152}
153
154async_daemonize!(realmain);
155async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
156    info!("Starting DarkFi x Monero merge mining proxy");
157
158    let mmproxy = Arc::new(MiningProxy::new(args.monerod).await?);
159    let mut app = tide::with_state(mmproxy);
160
161    // monerod `/getheight` endpoint proxy [HTTP GET]
162    app.at("/getheight").get(|req: tide::Request<Arc<MiningProxy>>| async move {
163        debug!(target: "monerod::getheight", "--> /getheight");
164        let mmproxy = req.state();
165        let return_data = mmproxy.monerod_get_height().await?;
166        let return_data = return_data.stringify()?;
167        debug!(target: "monerod::getheight", "<-- {}", return_data);
168        Ok(return_data)
169    });
170
171    // monerod `/getinfo` endpoint proxy [HTTP GET]
172    app.at("/getinfo").get(|req: tide::Request<Arc<MiningProxy>>| async move {
173        debug!(target: "monerod::getinfo", "--> /getinfo");
174        let mmproxy = req.state();
175        let return_data = mmproxy.monerod_get_info().await?;
176        let return_data = return_data.stringify()?;
177        debug!(target: "monerod::getinfo", "<-- {}", return_data);
178        Ok(return_data)
179    });
180
181    // monerod `/json_rpc` endpoint proxy [HTTP POST]
182    app.at("/json_rpc").post(|mut req: tide::Request<Arc<MiningProxy>>| async move {
183        let body_string = match req.body_string().await {
184            Ok(v) => v,
185            Err(e) => {
186                error!(target: "monerod::json_rpc", "Failed reading request body: {}", e);
187                return Err(surf::Error::new(StatusCode::BadRequest, Error::Custom(e.to_string())))
188            }
189        };
190        debug!(target: "monerod::json_rpc", "--> {}", body_string);
191
192        let json_str: JsonValue = match body_string.parse() {
193            Ok(v) => v,
194            Err(e) => {
195                error!(target: "monerod::json_rpc", "Failed parsing JSON body: {}", e);
196                return Err(surf::Error::new(StatusCode::BadRequest, Error::Custom(e.to_string())))
197            }
198        };
199
200        let JsonValue::Object(ref request) = json_str else {
201            return Err(surf::Error::new(
202                StatusCode::BadRequest,
203                Error::Custom("Invalid JSONRPC request".to_string()),
204            ))
205        };
206
207        if !request.contains_key("method") || !request["method"].is_string() {
208            return Err(surf::Error::new(
209                StatusCode::BadRequest,
210                Error::Custom("Invalid JSONRPC request".to_string()),
211            ))
212        }
213
214        let mmproxy = req.state();
215
216        // For XMRig we only have to handle 2 methods:
217        let return_data: JsonValue = match request["method"].get::<String>().unwrap().as_str() {
218            "getblocktemplate" => mmproxy.monerod_getblocktemplate(&json_str).await?,
219            "submitblock" => mmproxy.monerod_submit_block(&json_str).await?,
220            _ => {
221                return Err(surf::Error::new(
222                    StatusCode::BadRequest,
223                    Error::Custom("Invalid JSONRPC request".to_string()),
224                ))
225            }
226        };
227
228        let return_data = return_data.stringify()?;
229        debug!(target: "monerod::json_rpc",  "<-- {}", return_data);
230        Ok(return_data)
231    });
232
233    ex.spawn(async move { app.listen(args.mmproxy.mmproxy_rpc).await.unwrap() }).detach();
234    info!("Merge mining proxy ready, waiting for connections");
235
236    // Signal handling for graceful termination.
237    let (signals_handler, signals_task) = SignalHandler::new(ex)?;
238    signals_handler.wait_termination(signals_task).await?;
239    info!("Caught termination signal, cleaning up and exiting");
240
241    Ok(())
242}