rpc_stratum.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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. use std::{collections::HashMap, str::FromStr};
  19. use darkfi::{
  20. rpc::jsonrpc::{ErrorCode, ErrorCode::InvalidParams, JsonError, JsonResponse, JsonResult},
  21. util::{encoding::base64, time::Timestamp},
  22. validator::consensus::Proposal,
  23. };
  24. use darkfi_sdk::crypto::keypair::Address;
  25. use darkfi_serial::serialize_async;
  26. use tinyjson::JsonValue;
  27. use tracing::{error, info};
  28. use crate::{
  29. proto::ProposalMessage,
  30. rpc_miner::{generate_next_block, MinerRewardsRecipientConfig},
  31. BlockTemplate, DarkfiNode, MiningJobs,
  32. };
  33. // https://github.com/xmrig/xmrig-proxy/blob/master/doc/STRATUM.md
  34. // https://github.com/xmrig/xmrig-proxy/blob/master/doc/STRATUM_EXT.md
  35. // TODO: We often just return InvalidParams. These should be cleaned up
  36. // and more verbose.
  37. // TODO: The jobs storing method is not the most ideal. Think of a better one.
  38. // `self.mining_jobs`
  39. // Random testnet address for reference:
  40. // fUfG4WhbHP5C2MhYW3FHctVqi2jfXHamoQeU8KiirKVtoMBEUejkwq9F
  41. impl DarkfiNode {
  42. // RPCAPI:
  43. // Miner sends a `login` request after establishing connection
  44. // in order to authorize.
  45. //
  46. // The server will return a job along with an id.
  47. // ```
  48. // "job": {
  49. // "blob": 070780e6b9d...4d62fa6c77e76c3001",
  50. // "job_id": "q7PLUPL25UV0z5Ij14IyMk8htXbj",
  51. // "target": "b88d0600",
  52. // "algo": "rx/0"
  53. // }
  54. // ```
  55. //
  56. // --> {"jsonrpc":"2.0", "method": "login", "id": 1, "params": {"login": "receiving_address", "pass": "x", "agent": "XMRig", "algo": ["rx/0"]}}
  57. // <-- {"jsonrpc":"2.0", "id": 1, "result": {"id": "1be0b7b6-b15a-47be-a17d-46b2911cf7d0", "job": { ... }, "status": "OK"}}
  58. pub async fn stratum_login(&self, id: u16, params: JsonValue) -> JsonResult {
  59. // TODO: Fail when not synced
  60. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  61. return JsonError::new(InvalidParams, None, id).into()
  62. };
  63. let Some(login) = params.get("login") else {
  64. return JsonError::new(InvalidParams, Some("Missing 'login'".to_string()), id).into()
  65. };
  66. let Some(login) = login.get::<String>() else {
  67. return JsonError::new(InvalidParams, None, id).into()
  68. };
  69. let Some(pass) = params.get("pass") else {
  70. return JsonError::new(InvalidParams, Some("Missing 'pass'".to_string()), id).into()
  71. };
  72. let Some(_pass) = pass.get::<String>() else {
  73. return JsonError::new(InvalidParams, None, id).into()
  74. };
  75. let Some(agent) = params.get("agent") else {
  76. return JsonError::new(InvalidParams, Some("Missing 'agent'".to_string()), id).into()
  77. };
  78. let Some(agent) = agent.get::<String>() else {
  79. return JsonError::new(InvalidParams, None, id).into()
  80. };
  81. let Some(algo) = params.get("algo") else {
  82. return JsonError::new(InvalidParams, Some("Missing 'algo'".to_string()), id).into()
  83. };
  84. let Some(algo) = algo.get::<Vec<JsonValue>>() else {
  85. return JsonError::new(InvalidParams, None, id).into()
  86. };
  87. // Try to parse `login` as valid address. This will be the
  88. // block reward recipient.
  89. let Ok(address) = Address::from_str(login) else {
  90. return JsonError::new(InvalidParams, Some("Invalid address".to_string()), id).into()
  91. };
  92. if address.network() != self.network {
  93. return JsonError::new(InvalidParams, Some("Invalid address prefix".to_string()), id)
  94. .into()
  95. }
  96. // Iterate through `algo` to see if "rx/0" is supported.
  97. // rx/0 is RandomX.
  98. let mut found_rx0 = false;
  99. for i in algo {
  100. let Some(algo) = i.get::<String>() else {
  101. return JsonError::new(InvalidParams, None, id).into()
  102. };
  103. if algo == "rx/0" {
  104. found_rx0 = true;
  105. break
  106. }
  107. }
  108. if !found_rx0 {
  109. return JsonError::new(InvalidParams, Some("rx/0 not supported".to_string()), id).into()
  110. }
  111. info!("[STRATUM] Got login from {} ({})", address, agent);
  112. let conn_id = {
  113. let mut hasher = blake3::Hasher::new();
  114. hasher.update(&address.to_string().into_bytes());
  115. hasher.update(&Timestamp::current_time().inner().to_le_bytes());
  116. *hasher.finalize().as_bytes()
  117. };
  118. // Now we should register this login, and create a blocktemplate and
  119. // a job for them.
  120. // TODO: We also have to spawn the notification task that will send
  121. // JSONRPC notifications to this connection when a new job is available.
  122. // We'll clear any existing jobs for this login.
  123. let mut mining_jobs = self.mining_jobs.lock().await;
  124. mining_jobs.insert(conn_id, MiningJobs::default());
  125. // Find applicable chain fork
  126. let mut extended_fork = match self.validator.best_current_fork().await {
  127. Ok(f) => f,
  128. Err(e) => {
  129. error!(
  130. target: "darkfid::rpc_stratum::stratum_login",
  131. "[STRATUM] Finding best fork index failed: {e}",
  132. );
  133. return JsonError::new(ErrorCode::InternalError, None, id).into()
  134. }
  135. };
  136. // Query the Validator for a new blocktemplate.
  137. // We first need to construct `MinerRewardsRecipientConfig` from the
  138. // address configuration provided to us through the RPC.
  139. // TODO: Parse any spend hook from the login. We might also want to
  140. // define a specific address format if it includes extra data.
  141. // We could also include arbitrary information in the login password.
  142. let recipient_config =
  143. MinerRewardsRecipientConfig { recipient: address, spend_hook: None, user_data: None };
  144. // Find next block target
  145. let target = self.validator.consensus.module.read().await.target;
  146. // Generate blocktemplate with all the information.
  147. // This will return the mining target, the entire block, and the
  148. // ephemeral secret used to sign the mined block.
  149. let (target, block, secret) = match generate_next_block(
  150. &mut extended_fork,
  151. &recipient_config,
  152. &self.powrewardv1_zk.zkbin,
  153. &self.powrewardv1_zk.provingkey,
  154. target,
  155. self.validator.verify_fees,
  156. )
  157. .await
  158. {
  159. Ok(v) => v,
  160. Err(e) => {
  161. error!(
  162. target: "darkfid::rpc_stratum::stratum_login",
  163. "[STRATUM] Failed to generate next blocktemplate: {e}",
  164. );
  165. return JsonError::new(ErrorCode::InternalError, None, id).into()
  166. }
  167. };
  168. // Reference the RandomX dataset seed
  169. // TODO: We can also send `next_seed_hash` when we know it.
  170. let seed_hash = extended_fork.module.darkfi_rx_keys.0.inner();
  171. // We will store this in our mining jobs map for reference when
  172. // a miner solution is submitted.
  173. let blocktemplate =
  174. BlockTemplate { block, randomx_key: *seed_hash, target: target.clone(), secret };
  175. // Construct everything needed for the Stratum response.
  176. let blob = blocktemplate.block.header.to_blockhashing_blob();
  177. let job_id = *blocktemplate.block.header.hash().inner();
  178. let height = blocktemplate.block.header.height as f64;
  179. // The target should be compacted to 8 bytes little-endian.
  180. let target = &target.to_bytes_le()[..8];
  181. // Store the job. unwrap should be fine because we created this above.
  182. let jobs = mining_jobs.get_mut(&conn_id).unwrap();
  183. jobs.insert(job_id, blocktemplate);
  184. // Construct response
  185. let job: HashMap<String, JsonValue> = HashMap::from([
  186. ("blob".to_string(), hex::encode(&blob).to_string().into()),
  187. ("job_id".to_string(), hex::encode(job_id).to_string().into()),
  188. ("height".to_string(), height.into()),
  189. ("target".to_string(), hex::encode(target).into()),
  190. ("algo".to_string(), "rx/0".to_string().into()),
  191. ("seed_hash".to_string(), hex::encode(seed_hash).into()),
  192. ]);
  193. let result = HashMap::from([
  194. ("id".to_string(), hex::encode(conn_id).into()),
  195. ("job".to_string(), job.into()),
  196. ("status".to_string(), "OK".to_string().into()),
  197. ]);
  198. // Ship it.
  199. JsonResponse::new(result.into(), id).into()
  200. }
  201. // RPCAPI:
  202. // Miner submits a job solution.
  203. //
  204. // --> {"jsonrpc":"2.0", "method": "submit", "id": 1, "params": {"id": "...", "job_id": "...", "nonce": "d0030040", "result": "e1364b8782719d7683e2ccd3d8f724bc59dfa780a9e960e7c0e0046acdb40100"}}
  205. // <-- {"jsonrpc":"2.0", "id": 1, "result": {"status": "OK"}}
  206. pub async fn stratum_submit(&self, id: u16, params: JsonValue) -> JsonResult {
  207. // TODO: Maybe grab an exclusive lock to avoid the xmrig spam while
  208. // we're doing the pow verification. xmrig spams us whenever it gets
  209. // a solution, and this will end up in cloning a bunch of blocktemplates
  210. // and is going to cause memory usage to go up significantly.
  211. // Ideally we should block here until we finish each submit one-by-one
  212. // and find a valid one. Then when we do find a valid one, we should
  213. // clear the existing job(s) so this method will just return an error
  214. // and not have to do all the block shenanigans.
  215. // Additionally when a block is proposed successfully, the node should
  216. // send a new job notification to xmrig so we should be fine.
  217. // That notification part should also clear the existing jobs.
  218. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  219. return JsonError::new(InvalidParams, None, id).into()
  220. };
  221. let Some(conn_id) = params.get("id") else {
  222. return JsonError::new(InvalidParams, Some("Missing 'id'".to_string()), id).into()
  223. };
  224. let Some(conn_id) = conn_id.get::<String>() else {
  225. return JsonError::new(InvalidParams, None, id).into()
  226. };
  227. let Some(job_id) = params.get("job_id") else {
  228. return JsonError::new(InvalidParams, Some("Missing 'job_id'".to_string()), id).into()
  229. };
  230. let Some(job_id) = job_id.get::<String>() else {
  231. return JsonError::new(InvalidParams, None, id).into()
  232. };
  233. let Some(nonce) = params.get("nonce") else {
  234. return JsonError::new(InvalidParams, Some("Missing 'nonce'".to_string()), id).into()
  235. };
  236. let Some(nonce) = nonce.get::<String>() else {
  237. return JsonError::new(InvalidParams, None, id).into()
  238. };
  239. // result is the RandomX calculated hash. Useful to verify/debug.
  240. let Some(result) = params.get("result") else {
  241. return JsonError::new(InvalidParams, Some("Missing 'result'".to_string()), id).into()
  242. };
  243. let Some(_result) = result.get::<String>() else {
  244. return JsonError::new(InvalidParams, None, id).into()
  245. };
  246. let Ok(conn_id) = hex::decode(conn_id) else {
  247. return JsonError::new(InvalidParams, None, id).into()
  248. };
  249. if conn_id.len() != 32 {
  250. return JsonError::new(InvalidParams, None, id).into()
  251. }
  252. let Ok(job_id) = hex::decode(job_id) else {
  253. return JsonError::new(InvalidParams, None, id).into()
  254. };
  255. if job_id.len() != 32 {
  256. return JsonError::new(InvalidParams, None, id).into()
  257. }
  258. let conn_id: [u8; 32] = conn_id.try_into().unwrap();
  259. let job_id: [u8; 32] = job_id.try_into().unwrap();
  260. // We should be aware of this conn_id and job_id.
  261. let mut mining_jobs = self.mining_jobs.lock().await;
  262. let Some(jobs) = mining_jobs.get_mut(&conn_id) else {
  263. return JsonError::new(InvalidParams, None, id).into()
  264. };
  265. // Get the blocktemplate.
  266. let Some(blocktemplate) = jobs.get_mut(&job_id) else {
  267. return JsonError::new(InvalidParams, None, id).into()
  268. };
  269. // Parse the nonce into u32.
  270. let Ok(nonce_bytes) = hex::decode(nonce) else {
  271. return JsonError::new(InvalidParams, None, id).into()
  272. };
  273. if nonce_bytes.len() != 4 {
  274. return JsonError::new(InvalidParams, None, id).into()
  275. }
  276. let nonce = u32::from_le_bytes(nonce_bytes.try_into().unwrap());
  277. // We clone the block, update the nonce,
  278. // sign it, and ship it into a proposal.
  279. let mut block = blocktemplate.block.clone();
  280. block.header.nonce = nonce;
  281. block.sign(&blocktemplate.secret);
  282. info!(
  283. target: "darkfid::rpc_stratum::stratum_submit",
  284. "[STRATUM] Proposing new block to network",
  285. );
  286. let proposal = Proposal::new(block);
  287. if let Err(e) = self.validator.append_proposal(&proposal).await {
  288. error!(
  289. target: "darkfid::rpc_stratum::stratum_submit",
  290. "[STRATUM] Error proposing new block: {e}",
  291. );
  292. return JsonError::new(InvalidParams, None, id).into()
  293. }
  294. // Proposal passed. We will now clear the jobs as it's assumed
  295. // a new block needs to be mined.
  296. mining_jobs.insert(conn_id, MiningJobs::default());
  297. // Broadcast to network
  298. let proposals_sub = self.subscribers.get("proposals").unwrap();
  299. let enc_prop = JsonValue::String(base64::encode(&serialize_async(&proposal).await));
  300. proposals_sub.notify(vec![enc_prop].into()).await;
  301. info!(
  302. target: "darkfid::rpc_stratum::stratum_submit",
  303. "[STRATUM] Broadcasting new block to network",
  304. );
  305. let message = ProposalMessage(proposal);
  306. self.p2p_handler.p2p.broadcast(&message).await;
  307. JsonResponse::new(
  308. HashMap::from([("status".to_string(), "OK".to_string().into())]).into(),
  309. id,
  310. )
  311. .into()
  312. }
  313. // RPCAPI:
  314. // Miner sends `keepalived` to prevent connection timeout.
  315. //
  316. // --> {"jsonrpc":"2.0", "method": "keepalived", "id": 1, "params": {"id": "foo"}}
  317. // <-- {"jsonrpc":"2.0", "id": 1, "result": {"status": "KEEPALIVED"}}
  318. pub async fn stratum_keepalived(&self, id: u16, params: JsonValue) -> JsonResult {
  319. let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
  320. return JsonError::new(InvalidParams, None, id).into()
  321. };
  322. let Some(_conn_id) = params.get("id") else {
  323. return JsonError::new(InvalidParams, Some("Missing 'id'".to_string()), id).into()
  324. };
  325. // TODO: This conn_id should likely exist. We should probably check
  326. // that. Otherwise we might not want to reply at all.
  327. JsonResponse::new(
  328. JsonValue::from(HashMap::from([("status".into(), "KEEPALIVED".to_string().into())])),
  329. id,
  330. )
  331. .into()
  332. }
  333. }