stratum.rs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2023 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, io, str::FromStr, sync::Arc, time::Duration};
  19. use darkfi::{
  20. rpc::{
  21. jsonrpc::{
  22. ErrorCode::{InternalError, InvalidParams, ServerError},
  23. JsonError, JsonRequest, JsonResponse, JsonResult, JsonSubscriber,
  24. },
  25. util::JsonValue,
  26. },
  27. system::{sleep, timeout::timeout, StoppableTask, StoppableTaskPtr},
  28. Error, Result,
  29. };
  30. use log::{debug, error, info, warn};
  31. use monero::blockdata::transaction::{ExtraField, RawExtraField, SubField::MergeMining};
  32. use num_bigint::BigUint;
  33. use smol::{channel, lock::RwLock};
  34. use url::Url;
  35. use uuid::Uuid;
  36. use super::{error::RpcError, MiningProxy};
  37. /// Algo string representing Monero's RandomX
  38. pub const RANDOMX_ALGO: &str = "rx/0";
  39. /// A mining job instance
  40. #[derive(Clone)]
  41. struct MiningJob {
  42. /// Current job ID for the worker
  43. pub job_id: blake3::Hash,
  44. /// Full block being mined
  45. pub block: monero::Block,
  46. /// Difficulty target,
  47. pub target: String,
  48. /// Block height
  49. pub height: f64,
  50. /// RandomX seed hash
  51. pub seed_hash: String,
  52. }
  53. /// Single worker connected to the mining proxy
  54. pub struct Worker {
  55. /// Wallet address
  56. addr: monero::Address,
  57. /// Miner useragent
  58. _agent: String,
  59. /// JSON-RPC notification subscriber, used to send new job notifications
  60. job_sub: JsonSubscriber,
  61. /// Background keepalive task reference
  62. _ka_task: StoppableTaskPtr,
  63. /// Keepalive sender channel, pinged from Stratum keepalived
  64. ka_send: channel::Sender<()>,
  65. /// Background mining job task reference
  66. _job_task: StoppableTaskPtr,
  67. /// Block submit trigger sender channel, pinged from Stratum submit
  68. submit_send: channel::Sender<()>,
  69. /// Current mining job
  70. mining_job: MiningJob,
  71. }
  72. impl Worker {
  73. async fn notify_job(&mut self, mining_job: MiningJob) -> Result<()> {
  74. // Update the mining job
  75. self.mining_job = mining_job.clone();
  76. // Build notification params
  77. let params: JsonValue = JsonValue::Object(HashMap::from([
  78. ("blob".to_string(), hex::encode(mining_job.block.serialize_hashable()).into()),
  79. ("job_id".to_string(), mining_job.job_id.to_string().into()),
  80. ("target".to_string(), mining_job.target.into()),
  81. ("height".to_string(), mining_job.height.into()),
  82. ("seed_hash".to_string(), mining_job.seed_hash.into()),
  83. ("algo".to_string(), RANDOMX_ALGO.to_string().into()),
  84. ]));
  85. info!(
  86. target: "worker::notify_job",
  87. "[STRATUM] Sending mining job notification to worker",
  88. );
  89. self.job_sub.notify(params).await;
  90. Ok(())
  91. }
  92. }
  93. /// Send a HTTP JSON-RPC request to the given monerod RPC endpoint
  94. async fn monerod_request(endpoint: &Url, req: JsonRequest) -> Result<JsonValue> {
  95. let client = surf::Client::new();
  96. let mut response = match client
  97. .get(endpoint)
  98. .header("Content-Type", "application/json")
  99. .body(req.stringify().unwrap())
  100. .send()
  101. .await
  102. {
  103. Ok(v) => v,
  104. Err(e) => {
  105. error!(
  106. target: "stratum::monerod_request",
  107. "[STRATUM] Failed sending RPC request to monerod: {}", e,
  108. );
  109. return Err(io::Error::new(io::ErrorKind::Other, e).into())
  110. }
  111. };
  112. let response_bytes = match response.body_bytes().await {
  113. Ok(v) => v,
  114. Err(e) => {
  115. error!(
  116. target: "stratum::monerod_request",
  117. "[STRATUM] Failed reading monerod RPC response: {}", e,
  118. );
  119. return Err(io::Error::new(io::ErrorKind::Other, e).into())
  120. }
  121. };
  122. let response_string = match String::from_utf8(response_bytes) {
  123. Ok(v) => v,
  124. Err(e) => {
  125. error!(
  126. target: "stratum::monerod_request",
  127. "[STRATUM] Failed parsing monerod RPC response: {}", e,
  128. );
  129. return Err(io::Error::new(io::ErrorKind::Other, e).into())
  130. }
  131. };
  132. let response_json: JsonValue = match response_string.parse() {
  133. Ok(v) => v,
  134. Err(e) => {
  135. error!(
  136. target: "stratum::monerod_request",
  137. "[STRATUM] Failed parsing monerod RPC response JSON: {}", e,
  138. );
  139. return Err(io::Error::new(io::ErrorKind::Other, e).into())
  140. }
  141. };
  142. Ok(response_json)
  143. }
  144. /// Perform getblocktemplate from monerod and inject it with the
  145. /// necessary merge mining data.
  146. /// Returns data necessary to create a mining job
  147. async fn getblocktemplate(endpoint: &Url, wallet_address: &monero::Address) -> Result<MiningJob> {
  148. // Create the Merge Mining Tag: (`depth`, `merkle_root`)
  149. let mm_tag = MergeMining(Some(monero::VarInt(32)), monero::Hash([0_u8; 32]));
  150. // Construct `tx_extra` from all the extra fields we have to
  151. // add to the coinbase transaction in the block we're mining
  152. let tx_extra: RawExtraField = ExtraField(vec![mm_tag]).into();
  153. // Create the monerod JSON-RPC request. `reserve_size` is the space
  154. // we need to create for the `tx_extra` field created above.
  155. let req = JsonRequest::new(
  156. "get_block_template",
  157. HashMap::from([
  158. ("wallet_address".to_string(), wallet_address.to_string().into()),
  159. ("reserve_size".to_string(), (tx_extra.0.len() as f64).into()),
  160. ])
  161. .into(),
  162. );
  163. // Get block template from monerod
  164. info!(target: "stratum::getblocktemplate", "[STRATUM] Sending getblocktemplate to monero");
  165. let rep = match monerod_request(endpoint, req).await {
  166. Ok(v) => v,
  167. Err(e) => {
  168. error!(
  169. target: "stratum::getblocktemplate",
  170. "[STRATUM] Failed sending getblocktemplate to monerod: {}", e,
  171. );
  172. return Err(io::Error::new(io::ErrorKind::Other, e).into())
  173. }
  174. };
  175. // Now we have to modify the block template:
  176. // * Update the coinbase tx with our tx_extra field
  177. // * Update the `blockhashing_blob` in order to perform correct PoW
  178. // Deserialize the block template
  179. let mut block_template = monero::consensus::deserialize::<monero::Block>(
  180. &hex::decode(rep["result"]["blocktemplate_blob"].get::<String>().unwrap()).unwrap(),
  181. )
  182. .unwrap();
  183. // Modify the coinbase tx with our additional merge mining data
  184. block_template.miner_tx.prefix.extra = tx_extra;
  185. // Decode the difficulty and calculate the mining target
  186. let mut difficulty_hex = rep["result"]["wide_difficulty"]
  187. .get::<String>()
  188. .unwrap()
  189. .strip_prefix("0x")
  190. .unwrap()
  191. .to_string();
  192. // Needed because hex::decode doesn't accept odd-length
  193. if difficulty_hex.len() % 2 != 0 {
  194. difficulty_hex = format!("0{}", difficulty_hex);
  195. }
  196. let difficulty_raw = hex::decode(&difficulty_hex).unwrap();
  197. let difficulty = BigUint::from_radix_be(&difficulty_raw, 16).unwrap();
  198. // Calculate the target. XMRig expects the 64 least significant bits.
  199. let target_raw = BigUint::from_bytes_be(&[0xFF; 32]) / &difficulty;
  200. // This iterator is ordered least significant first
  201. let target_lsb: u64 = target_raw.iter_u64_digits().take(1).next().unwrap();
  202. let target = hex::encode(target_lsb.to_be_bytes());
  203. assert!(target.len() == 16);
  204. info!(target: "stratum::getblocktemplate", "[STRATUM] Difficulty: {}", difficulty_hex);
  205. info!(target: "stratum::getblocktemplate", "[STRATUM] Target: {}", target);
  206. // Get the remaining metadata
  207. let height = *rep["result"]["height"].get::<f64>().unwrap();
  208. let seed_hash = rep["result"]["seed_hash"].get::<String>().unwrap().to_string();
  209. // Create a deterministic job id
  210. let mut hasher = blake3::Hasher::new();
  211. hasher.update(&wallet_address.as_bytes());
  212. hasher.update(&height.to_le_bytes());
  213. hasher.update(seed_hash.as_bytes());
  214. let job_id = hasher.finalize();
  215. // Return the necessary data
  216. Ok(MiningJob { job_id, block: block_template, target, height, seed_hash })
  217. }
  218. impl MiningProxy {
  219. /// Background task listening for keepalives from a worker.
  220. /// If timeout is reached, the worker will be dropped.
  221. async fn keepalive_task(
  222. workers: Arc<RwLock<HashMap<Uuid, Worker>>>,
  223. uuid: Uuid,
  224. ka_recv: channel::Receiver<()>,
  225. ) -> Result<()> {
  226. debug!(target: "stratum::keepalive_task", "Spawned keepalive_task for worker {}", uuid);
  227. const TIMEOUT: Duration = Duration::from_secs(65);
  228. loop {
  229. let Ok(r) = timeout(TIMEOUT, ka_recv.recv()).await else {
  230. // Timeout, remove worker
  231. warn!(
  232. target: "stratum::keepalive_task",
  233. "keepalive_task for worker {} timed out", uuid,
  234. );
  235. workers.write().await.remove(&uuid);
  236. break
  237. };
  238. match r {
  239. Ok(()) => {
  240. debug!(
  241. target: "stratum::keepalive_task",
  242. "keepalive_task for worker {} got ping", uuid,
  243. );
  244. continue
  245. }
  246. Err(e) => {
  247. error!(
  248. target: "stratum::keepalive_task",
  249. "keepalive_task for worker {} channel recv error: {}", uuid, e,
  250. );
  251. warn!(
  252. target: "stratum::keepalive_task",
  253. "Dropping worker {}", uuid,
  254. );
  255. workers.write().await.remove(&uuid);
  256. break
  257. }
  258. }
  259. }
  260. Ok(())
  261. }
  262. /// Background task used to notify a worker about new mining jobs.
  263. /// `keepalive_task` iis able to remove workers from the worker pool,
  264. /// so this task can easily exit if the worker is not found.
  265. async fn job_task(
  266. workers: Arc<RwLock<HashMap<Uuid, Worker>>>,
  267. uuid: Uuid,
  268. endpoint: Url,
  269. submit_recv: channel::Receiver<()>,
  270. ) -> Result<()> {
  271. debug!(target: "stratum::job_task", "Spawned job_task for worker {}", uuid);
  272. const POLL_INTERVAL: Duration = Duration::from_secs(60);
  273. // Comfy wait for settling the Stratum login RPC call
  274. sleep(5).await;
  275. // In this loop, we'll be getting the block template for mining.
  276. // At the beginning of the loop, we'll perform a getblocktemplate,
  277. // and then inject our Merge Mining stuff, and forward it to the
  278. // miner. After the notification, we'll either poll or wait for a
  279. // trigger for a submitted block and reiterate the loop again in
  280. // order to get the next mining job.
  281. loop {
  282. // Get the workers lock and the worker reference
  283. debug!(target: "stratum::job_task", "Acquiring workers write lock...");
  284. let mut workers_ptr = workers.write().await;
  285. debug!(target: "stratum::job_task", "Acquired workers write lock");
  286. let Some(worker) = workers_ptr.get_mut(&uuid) else {
  287. info!(
  288. target: "stratum::job_task",
  289. "[STRATUM] Worker {} disconnected, exiting job_task", uuid,
  290. );
  291. break
  292. };
  293. // Get the next mining job
  294. let mining_job = match getblocktemplate(&endpoint, &worker.addr).await {
  295. Ok(v) => v,
  296. Err(e) => {
  297. error!(
  298. target: "stratum::job_task",
  299. "[STRATUM] Failed fetching getblocktemplate for worker {}: {}", uuid, e,
  300. );
  301. warn!(
  302. target: "stratum::job_task",
  303. "[STRATUM] Exiting job_task for worker {}", uuid,
  304. );
  305. break
  306. }
  307. };
  308. // In case it's the same job, we'll wait and try again
  309. if worker.mining_job.job_id == mining_job.job_id {
  310. // Drop the workers lock before sleeping.
  311. drop(workers_ptr);
  312. match timeout(POLL_INTERVAL, submit_recv.recv()).await {
  313. Ok(_) => continue,
  314. Err(_) => continue,
  315. }
  316. }
  317. // Notify the worker about the new job
  318. if let Err(e) = worker.notify_job(mining_job).await {
  319. error!(
  320. target: "stratum::job_task",
  321. "[STRATUM] Failed sending job to worker {}: {}", uuid, e,
  322. );
  323. warn!(
  324. target: "stratum::job_task",
  325. "[STRATUM] Exiting job_task for worker {}", uuid,
  326. );
  327. break
  328. }
  329. // Drop the workers lock before sleeping.
  330. drop(workers_ptr);
  331. // Now poll or wait for a trigger for a new job.
  332. match timeout(POLL_INTERVAL, submit_recv.recv()).await {
  333. Ok(_) => continue,
  334. Err(_) => continue,
  335. }
  336. }
  337. Ok(())
  338. }
  339. /// Stratum login method
  340. ///
  341. /// `darkfi-mmproxy` will check that the worker provided a valid
  342. /// address as the username, and will enforce `RANDOMX_ALGO` to
  343. /// be supported. Upon success, we will fetch the block template
  344. /// from monerod, inject it with our necessary merge mining info,
  345. /// and forward it to the worker.
  346. /// Additionally, we will spawn background tasks for new job and
  347. /// keepalive notifications for this worker.
  348. pub async fn stratum_login(&self, id: u16, params: JsonValue) -> JsonResult {
  349. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  350. return JsonError::new(InvalidParams, None, id).into()
  351. };
  352. if !params.contains_key("login") ||
  353. !params.contains_key("pass") ||
  354. !params.contains_key("agent") ||
  355. !params.contains_key("algo")
  356. {
  357. return JsonError::new(InvalidParams, None, id).into()
  358. }
  359. let Some(login) = params["login"].get::<String>() else {
  360. return JsonError::new(InvalidParams, Some("Invalid \"login\" object".to_string()), id)
  361. .into()
  362. };
  363. let Some(_pass) = params["pass"].get::<String>() else {
  364. return JsonError::new(InvalidParams, Some("Invalid \"pass\" object".to_string()), id)
  365. .into()
  366. };
  367. let Some(agent) = params["agent"].get::<String>() else {
  368. return JsonError::new(InvalidParams, Some("Invalid \"agent\" object".to_string()), id)
  369. .into()
  370. };
  371. let Some(algos) = params["algo"].get::<Vec<JsonValue>>() else {
  372. return JsonError::new(InvalidParams, Some("Invalid \"algo\" object".to_string()), id)
  373. .into()
  374. };
  375. // We'll only support `RANDOMX_ALGO`
  376. let mut found_randomx_algo = false;
  377. for algo in algos.iter() {
  378. if !algo.is_string() {
  379. return JsonError::new(InvalidParams, Some("Algo is not a string".to_string()), id)
  380. .into()
  381. }
  382. if algo.get::<String>().unwrap() == RANDOMX_ALGO {
  383. found_randomx_algo = true;
  384. break
  385. }
  386. }
  387. if !found_randomx_algo {
  388. return JsonError::new(
  389. RpcError::UnsupportedMiningAlgo.into(),
  390. Some("Unsupported mining algos".to_string()),
  391. id,
  392. )
  393. .into()
  394. }
  395. // Check valid login. We will parse the username as a Monero
  396. // address, and validate that it corresponds to the network
  397. // we're mining on.
  398. let addr = match monero::Address::from_str(login) {
  399. Ok(v) => v,
  400. Err(e) => {
  401. return JsonError::new(
  402. RpcError::InvalidWorkerLogin.into(),
  403. Some(format!("Invalid Monero address login: {}", e)),
  404. id,
  405. )
  406. .into()
  407. }
  408. };
  409. if addr.network != self.monerod_network {
  410. return JsonError::new(
  411. RpcError::InvalidWorkerLogin.into(),
  412. Some(format!(
  413. "Invalid Monero address network, expected \"{:?}\"",
  414. self.monerod_network
  415. )),
  416. id,
  417. )
  418. .into()
  419. }
  420. if addr.addr_type != monero::AddressType::Standard {
  421. return JsonError::new(
  422. RpcError::InvalidWorkerLogin.into(),
  423. Some(format!(
  424. "Invalid Monero address type, expected \"{}\"",
  425. monero::AddressType::Standard
  426. )),
  427. id,
  428. )
  429. .into()
  430. }
  431. // Now we have a valid address for mining.
  432. // Create a new UUID for the worker, and initialize the `Worker`
  433. // struct that will live throughout the miner's lifetime.
  434. let worker_uuid = Uuid::new_v4();
  435. // Create job subscriber
  436. let job_sub = JsonSubscriber::new("job");
  437. // Create keepalive channel
  438. let (ka_send, ka_recv) = channel::unbounded();
  439. // Create submit trigger channel
  440. let (submit_send, submit_recv) = channel::unbounded();
  441. // Create background keepalive task
  442. let ka_task = StoppableTask::new();
  443. // Create background job task
  444. let job_task = StoppableTask::new();
  445. // Get the current mining job for the worker
  446. let mining_job = match getblocktemplate(&self.monerod_rpc, &addr).await {
  447. Ok(v) => v,
  448. Err(e) => {
  449. error!(
  450. target: "stratum::login",
  451. "[STRATUM] Failed fetching block template for worker: {}", e,
  452. );
  453. return JsonError::new(InternalError, None, id).into()
  454. }
  455. };
  456. // Create worker
  457. let worker = Worker {
  458. addr,
  459. _agent: agent.clone(),
  460. job_sub: job_sub.clone(),
  461. _ka_task: ka_task.clone(),
  462. ka_send,
  463. _job_task: job_task.clone(),
  464. submit_send,
  465. mining_job: mining_job.clone(),
  466. };
  467. // Insert the worker into connections map
  468. self.workers.write().await.insert(worker_uuid, worker);
  469. // Spawn keepalive background task
  470. ka_task.start(
  471. Self::keepalive_task(self.workers.clone(), worker_uuid, ka_recv),
  472. move |_| async move { debug!("keepalive_task for {} exited", worker_uuid) },
  473. Error::DetachedTaskStopped,
  474. self.executor.clone(),
  475. );
  476. // Spawn job notification background task
  477. job_task.start(
  478. Self::job_task(
  479. self.workers.clone(),
  480. worker_uuid,
  481. self.monerod_rpc.clone(),
  482. submit_recv,
  483. ),
  484. move |_| async move { debug!("job_task for {} exited", worker_uuid) },
  485. Error::DetachedTaskStopped,
  486. self.executor.clone(),
  487. );
  488. info!("[STRATUM] Added worker {}", worker_uuid);
  489. // Finally, we return the job notification subscriber, along with the
  490. // initial job response as noted in:
  491. // https://github.com/xmrig/xmrig-proxy/blob/master/doc/STRATUM.md#example-success-reply
  492. let blob = hex::encode(mining_job.block.serialize_hashable());
  493. let response = JsonResponse::new(
  494. HashMap::from([
  495. ("status".to_string(), "OK".to_string().into()),
  496. ("id".to_string(), worker_uuid.to_string().into()),
  497. (
  498. "extensions".to_string(),
  499. vec!["algo".to_string().into(), "keepalive".to_string().into()].into(),
  500. ),
  501. (
  502. "job".to_string(),
  503. HashMap::from([
  504. ("blob".to_string(), blob.into()),
  505. ("job_id".to_string(), mining_job.job_id.to_string().into()),
  506. ("target".to_string(), mining_job.target.to_string().into()),
  507. ("height".to_string(), mining_job.height.into()),
  508. ("seed_hash".to_string(), mining_job.seed_hash.to_string().into()),
  509. ("algo".to_string(), RANDOMX_ALGO.to_string().into()),
  510. ])
  511. .into(),
  512. ),
  513. ])
  514. .into(),
  515. id,
  516. );
  517. JsonResult::SubscriberWithReply(job_sub, response)
  518. }
  519. /// Stratum submit method
  520. ///
  521. /// The miner submits the request after a share was found.
  522. pub async fn stratum_submit(&self, id: u16, params: JsonValue) -> JsonResult {
  523. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  524. return JsonError::new(InvalidParams, None, id).into()
  525. };
  526. if !params.contains_key("id") ||
  527. !params.contains_key("job_id") ||
  528. !params.contains_key("nonce") ||
  529. !params.contains_key("result") ||
  530. !params.contains_key("algo")
  531. {
  532. return JsonError::new(InvalidParams, None, id).into()
  533. }
  534. // Validate all the parameters
  535. let Some(worker_uuid) = params["id"].get::<String>() else {
  536. error!(target: "stratum::submit", "[STRATUM] Missing \"id\" field for stratum::submit");
  537. return JsonError::new(InvalidParams, Some("Missing \"id\" field".to_string()), id)
  538. .into()
  539. };
  540. let Ok(worker_uuid) = Uuid::try_from(worker_uuid.as_str()) else {
  541. error!(target: "stratum::submit", "[STRATUM] Invalid \"id\" field for stratum::submit");
  542. return JsonError::new(InvalidParams, Some("Invalid \"id\" field".to_string()), id)
  543. .into()
  544. };
  545. let Some(job_id) = params["job_id"].get::<String>() else {
  546. error!(target: "stratum::submit", "[STRATUM] Missing \"job_id\" field for stratum::submit");
  547. return JsonError::new(InvalidParams, Some("Missing \"job_id\" field".to_string()), id)
  548. .into()
  549. };
  550. let Ok(job_id) = blake3::Hash::from_str(job_id) else {
  551. error!(target: "stratum::submit", "[STRATUM] Invalid \"job_id\" field for stratum::submit");
  552. return JsonError::new(InvalidParams, Some("Invalid \"job_id\" field".to_string()), id)
  553. .into()
  554. };
  555. let Some(nonce) = params["nonce"].get::<String>() else {
  556. error!(target: "stratum::submit", "[STRATUM] Missing \"nonce\" field for stratum::submit");
  557. return JsonError::new(InvalidParams, Some("Missing \"nonce\" field".to_string()), id)
  558. .into()
  559. };
  560. let Ok(nonce) = u32::from_str_radix(nonce, 16) else {
  561. error!(target: "stratum::submit", "[STRATUM] Invalid \"nonce\" field for stratum::submit");
  562. return JsonError::new(InvalidParams, Some("Invalid \"nonce\" field".to_string()), id)
  563. .into()
  564. };
  565. let Some(_result) = params["result"].get::<String>() else {
  566. error!(target: "stratum::submit", "[STRATUM] Missing \"result\" field for stratum::submit");
  567. return JsonError::new(InvalidParams, Some("Invalid \"result\" field".to_string()), id)
  568. .into()
  569. };
  570. let Some(algo) = params["algo"].get::<String>() else {
  571. error!(target: "stratum::submit", "[STRATUM] Missing \"algo\" field for stratum::submit");
  572. return JsonError::new(InvalidParams, Some("Missing \"algo\" field".to_string()), id)
  573. .into()
  574. };
  575. if algo != RANDOMX_ALGO {
  576. error!(target: "stratum::submit", "[STRATUM] Invalid \"algo\" field for stratum::submit");
  577. return JsonError::new(InvalidParams, Some("Invalid \"algo\" field".to_string()), id)
  578. .into()
  579. }
  580. // Get the worker reference and confirm this is submitted for the current job
  581. debug!(target: "stratum::submit", "Acquiring workers read lock...");
  582. let workers_ptr = self.workers.read().await;
  583. debug!(target: "stratum::submit", "Acquired workers read lock");
  584. let Some(worker) = workers_ptr.get(&worker_uuid) else {
  585. error!(target: "stratum::submit", "[STRATUM] Unknown worker UUID for stratum::submit");
  586. return JsonError::new(InvalidParams, Some("Unknown worker UUID".to_string()), id).into()
  587. };
  588. if worker.mining_job.job_id != job_id {
  589. error!(target: "stratum::submit", "[STRATUM] Job ID mismatch for stratum::submit");
  590. return JsonError::new(InvalidParams, Some("Job ID mismatch".to_string()), id).into()
  591. }
  592. // Get the block template from the worker reference and update the nonce
  593. let mut block_template = worker.mining_job.block.clone();
  594. block_template.header.nonce = nonce;
  595. // Submit the block to monerod
  596. let block = monero::consensus::serialize_hex(&block_template);
  597. let params: JsonValue = vec![block.into()].into();
  598. let req = JsonRequest::new("submit_block", params);
  599. let resp = match monerod_request(&self.monerod_rpc, req).await {
  600. Ok(v) => v,
  601. Err(e) => {
  602. error!(
  603. target: "stratum::submit",
  604. "[STRATUM] Failed submitting block to monerod: {}", e,
  605. );
  606. return JsonError::new(
  607. InternalError,
  608. Some("Failed submitting block".to_string()),
  609. id,
  610. )
  611. .into()
  612. }
  613. };
  614. // Ping the job_task to reiterate.
  615. // We don't release the lock after this, so that we can hopefully first
  616. // return the result of the `submit` call, and then unlock job_task for
  617. // the new notification.
  618. let _ = worker.submit_send.send(()).await;
  619. match JsonResult::try_from_value(&resp) {
  620. Ok(JsonResult::Response(r)) => {
  621. info!(
  622. target: "stratum::submit",
  623. "[STRATUM] Sucessfully submitted block to monerod: {:?}", r,
  624. );
  625. let result = HashMap::from([("status".to_string(), "OK".to_string().into())]);
  626. JsonResponse::new(result.into(), id).into()
  627. }
  628. Ok(JsonResult::Error(e)) => {
  629. error!(
  630. target: "stratum::submit",
  631. "[STRATUM] Failed submitting block to monerod: {:?}", e,
  632. );
  633. JsonError::new(ServerError(e.error.code), Some(e.error.message), id).into()
  634. }
  635. Ok(x) => {
  636. error!(
  637. target: "stratum::submit",
  638. "[STRATUM] Unexpected RPC reply from monerod: {:?}", x,
  639. );
  640. JsonError::new(InternalError, Some("Failed submitting block".to_string()), id)
  641. .into()
  642. }
  643. Err(e) => {
  644. error!(
  645. target: "stratum::submit",
  646. "[STRATUM] Unexpected RPC reply from monerod: {}", e,
  647. );
  648. JsonError::new(InternalError, Some("Failed submitting block".to_string()), id)
  649. .into()
  650. }
  651. }
  652. }
  653. /// Nonstandard, but widely supported protocol extension.
  654. /// The miner sends `keepalived` to prevent connection timeout.
  655. /// `darkfi-mmproxy` makes having keepalived mandatory.
  656. pub async fn stratum_keepalived(&self, id: u16, params: JsonValue) -> JsonResult {
  657. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  658. return JsonError::new(InvalidParams, None, id).into()
  659. };
  660. if !params.contains_key("id") {
  661. return JsonError::new(InvalidParams, Some("Missing \"id\" field".to_string()), id)
  662. .into()
  663. };
  664. let Some(worker_uuid) = params["id"].get::<String>() else {
  665. return JsonError::new(InvalidParams, Some("Invalid \"id\" field".to_string()), id)
  666. .into()
  667. };
  668. let Ok(worker_uuid) = Uuid::try_from(worker_uuid.as_str()) else {
  669. return JsonError::new(InvalidParams, Some("Invalid \"id\" field".to_string()), id)
  670. .into()
  671. };
  672. // Get the worker reference
  673. let workers_ptr = self.workers.read().await;
  674. let Some(worker) = workers_ptr.get(&worker_uuid) else {
  675. return JsonError::new(InvalidParams, Some("Invalid \"id\" field".to_string()), id)
  676. .into()
  677. };
  678. // Ping the keepalive task
  679. if let Err(e) = worker.ka_send.send(()).await {
  680. error!(
  681. target: "stratum::keepalived",
  682. "[STRATUM] Keepalive task ping error for {}: {}", worker_uuid, e,
  683. );
  684. return JsonError::new(InternalError, None, id).into()
  685. }
  686. JsonResponse::new(
  687. JsonValue::Object(HashMap::from([(
  688. "status".to_string(),
  689. "KEEPALIVED".to_string().into(),
  690. )])),
  691. id,
  692. )
  693. .into()
  694. }
  695. }