stratum.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 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, HashSet};
  19. use async_trait::async_trait;
  20. use smol::lock::MutexGuard;
  21. use tinyjson::JsonValue;
  22. use tracing::{debug, error, info};
  23. use darkfi::{
  24. rpc::{
  25. jsonrpc::{
  26. ErrorCode, ErrorCode::InvalidParams, JsonError, JsonRequest, JsonResponse, JsonResult,
  27. },
  28. server::RequestHandler,
  29. },
  30. system::StoppableTaskPtr,
  31. };
  32. use crate::{
  33. error::{miner_status_response, server_error, RpcError},
  34. registry::model::MinerRewardsRecipientConfig,
  35. DarkfiNode,
  36. };
  37. // https://github.com/xmrig/xmrig-proxy/blob/master/doc/STRATUM.md
  38. // https://github.com/xmrig/xmrig-proxy/blob/master/doc/STRATUM_EXT.md
  39. /// JSON-RPC `RequestHandler` for Stratum
  40. pub struct StratumRpcHandler;
  41. #[async_trait]
  42. #[rustfmt::skip]
  43. impl RequestHandler<StratumRpcHandler> for DarkfiNode {
  44. async fn handle_request(&self, req: JsonRequest) -> JsonResult {
  45. debug!(target: "darkfid::rpc::stratum_rpc", "--> {}", req.stringify().unwrap());
  46. match req.method.as_str() {
  47. // ======================
  48. // Stratum mining methods
  49. // ======================
  50. "login" => self.stratum_login(req.id, req.params).await,
  51. "submit" => self.stratum_submit(req.id, req.params).await,
  52. "keepalived" => self.stratum_keepalived(req.id, req.params).await,
  53. _ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
  54. }
  55. }
  56. async fn connections_mut(&self) -> MutexGuard<'life0, HashSet<StoppableTaskPtr>> {
  57. self.registry.stratum_rpc_connections.lock().await
  58. }
  59. }
  60. impl DarkfiNode {
  61. // RPCAPI:
  62. // Register a new mining client to the registry and generate a new
  63. // job.
  64. //
  65. // **Request:**
  66. // * `login` : A wallet address or its base-64 encoded mining configuration
  67. // * `pass` : Unused client password field
  68. // * `agent` : Client agent description
  69. // * `algo` : Client supported mining algorithms
  70. //
  71. // **Response:**
  72. // * `id` : Registry client ID
  73. // * `job` : The generated mining job
  74. // * `status` : Response status
  75. //
  76. // The generated mining job map consists of the following fields:
  77. // * `blob` : The hex encoded block hashing blob of the job block
  78. // * `job_id` : Registry mining job ID
  79. // * `height` : The job block height
  80. // * `target` : Current mining target
  81. // * `algo` : The mining algorithm - RandomX
  82. // * `seed_hash` : Current RandomX key
  83. // * `next_seed_hash`: (optional) Next RandomX key if it is known
  84. //
  85. // --> {
  86. // "jsonrpc": "2.0",
  87. // "method": "login",
  88. // "params": {
  89. // "login": "WALLET_ADDRESS",
  90. // "pass": "x",
  91. // "agent": "XMRig",
  92. // "algo": ["rx/0"]
  93. // },
  94. // "id": 1
  95. // }
  96. // <-- {
  97. // "jsonrpc": "2.0",
  98. // "result": {
  99. // "id": "unique_connection-id",
  100. // "job": {
  101. // "blob": "abcdef...001234",
  102. // "job_id": "unique_job-id",
  103. // "height": 1234,
  104. // "target": "abcd1234",
  105. // "algo": "rx/0",
  106. // "seed_hash": "deadbeef...0234",
  107. // "next_seed_hash": "c0fefe...1243"
  108. // },
  109. // "status": "OK"
  110. // },
  111. // "id": 1
  112. // }
  113. pub async fn stratum_login(&self, id: u16, params: JsonValue) -> JsonResult {
  114. // Check if node is synced before responding
  115. let validator = self.validator.read().await;
  116. if !validator.synced {
  117. return JsonResponse::new(JsonValue::from(HashMap::new()), id).into()
  118. }
  119. // Parse request params
  120. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  121. return JsonError::new(InvalidParams, None, id).into()
  122. };
  123. // Parse login
  124. let Some(wallet) = params.get("login") else {
  125. return server_error(RpcError::MinerMissingLogin, id, None)
  126. };
  127. let Some(wallet) = wallet.get::<String>() else {
  128. return server_error(RpcError::MinerInvalidLogin, id, None)
  129. };
  130. let config =
  131. match MinerRewardsRecipientConfig::from_str(&self.registry.network, wallet).await {
  132. Ok(c) => c,
  133. Err(e) => return server_error(e, id, None),
  134. };
  135. // Parse password
  136. let Some(pass) = params.get("pass") else {
  137. return server_error(RpcError::MinerMissingPassword, id, None)
  138. };
  139. let Some(_pass) = pass.get::<String>() else {
  140. return server_error(RpcError::MinerInvalidPassword, id, None)
  141. };
  142. // Parse agent
  143. let Some(agent) = params.get("agent") else {
  144. return server_error(RpcError::MinerMissingAgent, id, None)
  145. };
  146. let Some(agent) = agent.get::<String>() else {
  147. return server_error(RpcError::MinerInvalidAgent, id, None)
  148. };
  149. // Parge algo
  150. let Some(algo) = params.get("algo") else {
  151. return server_error(RpcError::MinerMissingAlgo, id, None)
  152. };
  153. let Some(algo) = algo.get::<Vec<JsonValue>>() else {
  154. return server_error(RpcError::MinerInvalidAlgo, id, None)
  155. };
  156. // Iterate through `algo` to see if "rx/0" is supported.
  157. // rx/0 is RandomX.
  158. let mut found_rx0 = false;
  159. for i in algo {
  160. let Some(algo) = i.get::<String>() else {
  161. return server_error(RpcError::MinerInvalidAlgo, id, None)
  162. };
  163. if algo == "rx/0" {
  164. found_rx0 = true;
  165. break
  166. }
  167. }
  168. if !found_rx0 {
  169. return server_error(RpcError::MinerRandomXNotSupported, id, None)
  170. }
  171. // Register the new miner
  172. info!(
  173. target: "darkfid::rpc::rpc_stratum::stratum_login",
  174. "[RPC-STRATUM] Got login from {wallet} ({agent})",
  175. );
  176. let (client_id, job_id, job, publisher) =
  177. match self.registry.register_miner(&validator, wallet, &config).await {
  178. Ok(p) => p,
  179. Err(e) => {
  180. error!(
  181. target: "darkfid::rpc::rpc_stratum::stratum_login",
  182. "[RPC-STRATUM] Failed to register miner: {e}",
  183. );
  184. return JsonResponse::new(JsonValue::from(HashMap::new()), id).into()
  185. }
  186. };
  187. // Now we have the new job, we ship it to RPC
  188. info!(
  189. target: "darkfid::rpc::rpc_stratum::stratum_login",
  190. "[RPC-STRATUM] Created new mining job for client {client_id}: {job_id}"
  191. );
  192. let response = JsonValue::from(HashMap::from([
  193. ("id".to_string(), JsonValue::from(client_id)),
  194. ("job".to_string(), job),
  195. ("status".to_string(), JsonValue::from(String::from("OK"))),
  196. ]));
  197. (publisher, JsonResponse::new(response, id)).into()
  198. }
  199. // RPCAPI:
  200. // Miner submits a job solution.
  201. //
  202. // **Request:**
  203. // * `id` : Registry client ID
  204. // * `job_id` : Registry mining job ID
  205. // * `nonce` : The hex encoded solution header nonce.
  206. // * `result` : RandomX calculated hash
  207. //
  208. // **Response:**
  209. // * `status`: Block submit status
  210. //
  211. // --> {
  212. // "jsonrpc": "2.0",
  213. // "method": "submit",
  214. // "params": {
  215. // "id": "unique_connection-id",
  216. // "job_id": "unique_job-id",
  217. // "nonce": "d0030040",
  218. // "result": "e1364b8782719d7683e2ccd3d8f724bc59dfa780a9e960e7c0e0046acdb40100"
  219. // },
  220. // "id": 1
  221. // }
  222. // <-- {"jsonrpc": "2.0", "result": {"status": "OK"}, "id": 1}
  223. pub async fn stratum_submit(&self, id: u16, params: JsonValue) -> JsonResult {
  224. // Check if node is synced before responding
  225. let mut validator = self.validator.write().await;
  226. if !validator.synced {
  227. return miner_status_response(id, "rejected")
  228. }
  229. // Grab registry submissions lock
  230. let submit_lock = self.registry.submit_lock.write().await;
  231. // Parse request params
  232. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  233. return JsonError::new(InvalidParams, None, id).into()
  234. };
  235. // Parse client id
  236. let Some(client_id) = params.get("id") else {
  237. return server_error(RpcError::MinerMissingClientId, id, None)
  238. };
  239. let Some(client_id) = client_id.get::<String>() else {
  240. return server_error(RpcError::MinerInvalidClientId, id, None)
  241. };
  242. // If we don't know about this client, we can just abort here
  243. let mut jobs = self.registry.jobs.write().await;
  244. let Some(client) = jobs.get(client_id) else {
  245. return miner_status_response(id, "rejected")
  246. };
  247. // Parse job id
  248. let Some(job_id) = params.get("job_id") else {
  249. return server_error(RpcError::MinerMissingJobId, id, None)
  250. };
  251. let Some(job_id) = job_id.get::<String>() else {
  252. return server_error(RpcError::MinerInvalidJobId, id, None)
  253. };
  254. // If this job doesn't match the client one, we can just abort
  255. // here.
  256. if &client.job != job_id {
  257. return miner_status_response(id, "rejected")
  258. }
  259. // If this client job wallet template doesn't exist, we can
  260. // just abort here.
  261. let mut block_templates = self.registry.block_templates.write().await;
  262. let Some(block_template) = block_templates.get_mut(&client.wallet) else {
  263. return miner_status_response(id, "rejected")
  264. };
  265. // If this template has been already submitted, reject this
  266. // submission.
  267. if block_template.submitted {
  268. return miner_status_response(id, "rejected")
  269. }
  270. // Parse nonce
  271. let Some(nonce) = params.get("nonce") else {
  272. return server_error(RpcError::MinerMissingNonce, id, None)
  273. };
  274. let Some(nonce) = nonce.get::<String>() else {
  275. return server_error(RpcError::MinerInvalidNonce, id, None)
  276. };
  277. let Ok(nonce_bytes) = hex::decode(nonce) else {
  278. return server_error(RpcError::MinerInvalidNonce, id, None)
  279. };
  280. if nonce_bytes.len() != 4 {
  281. return server_error(RpcError::MinerInvalidNonce, id, None)
  282. }
  283. let nonce = u32::from_le_bytes(nonce_bytes.try_into().unwrap());
  284. // Parse result
  285. let Some(result) = params.get("result") else {
  286. return server_error(RpcError::MinerMissingResult, id, None)
  287. };
  288. let Some(_result) = result.get::<String>() else {
  289. return server_error(RpcError::MinerInvalidResult, id, None)
  290. };
  291. info!(
  292. target: "darkfid::rpc::rpc_stratum::stratum_submit",
  293. "[RPC-STRATUM] Got solution submission from client {client_id} for job: {job_id}",
  294. );
  295. // Update the block nonce and sign it
  296. let mut block = block_template.block.clone();
  297. block.header.nonce = nonce;
  298. block.sign(&block_template.secret);
  299. // Submit the new block through the registry
  300. if let Err(e) =
  301. self.registry.submit(&mut validator, &self.subscribers, &self.p2p_handler, block).await
  302. {
  303. error!(
  304. target: "darkfid::rpc::rpc_stratum::stratum_submit",
  305. "[RPC-STRATUM] Error submitting new block: {e}",
  306. );
  307. // Try to refresh the jobs before returning error
  308. let mut mm_jobs = self.registry.mm_jobs.write().await;
  309. if let Err(e) = self
  310. .registry
  311. .refresh_jobs(&mut block_templates, &mut jobs, &mut mm_jobs, &validator)
  312. .await
  313. {
  314. error!(
  315. target: "darkfid::rpc::rpc_stratum::stratum_submit",
  316. "[RPC-STRATUM] Error refreshing registry jobs: {e}",
  317. );
  318. }
  319. // Release all locks
  320. drop(block_templates);
  321. drop(jobs);
  322. drop(mm_jobs);
  323. drop(submit_lock);
  324. return miner_status_response(id, "rejected")
  325. }
  326. // Mark block as submitted
  327. block_template.submitted = true;
  328. // Release all locks
  329. drop(block_templates);
  330. drop(jobs);
  331. drop(submit_lock);
  332. miner_status_response(id, "OK")
  333. }
  334. // RPCAPI:
  335. // Miner sends `keepalived` to prevent connection timeout.
  336. //
  337. // **Request:**
  338. // * `id` : Registry client ID
  339. //
  340. // **Response:**
  341. // * `status`: Response status
  342. //
  343. // --> {"jsonrpc": "2.0", "method": "keepalived", "params": {"id": "foo"}, "id": 1}
  344. // <-- {"jsonrpc": "2.0", "result": {"status": "KEEPALIVED"}, "id": 1}
  345. pub async fn stratum_keepalived(&self, id: u16, params: JsonValue) -> JsonResult {
  346. // Parse request params
  347. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  348. return JsonError::new(InvalidParams, None, id).into()
  349. };
  350. // Parse client id
  351. let Some(client_id) = params.get("id") else {
  352. return server_error(RpcError::MinerMissingClientId, id, None)
  353. };
  354. let Some(client_id) = client_id.get::<String>() else {
  355. return server_error(RpcError::MinerInvalidClientId, id, None)
  356. };
  357. // If we don't know about this client job, we can just abort here
  358. if !self.registry.jobs.read().await.contains_key(client_id) {
  359. return server_error(RpcError::MinerUnknownClient, id, None)
  360. };
  361. // Respond with keepalived message
  362. miner_status_response(id, "KEEPALIVED")
  363. }
  364. }