stratum.rs 27 KB

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