1use std::collections::{HashMap, HashSet};
20
21use async_trait::async_trait;
22use smol::lock::MutexGuard;
23use tinyjson::JsonValue;
24use tracing::{debug, error, info};
25
26use darkfi::{
27 rpc::{
28 jsonrpc::{
29 ErrorCode, ErrorCode::InvalidParams, JsonError, JsonRequest, JsonResponse, JsonResult,
30 },
31 server::RequestHandler,
32 },
33 system::StoppableTaskPtr,
34};
35
36use crate::{
37 error::{miner_status_response, server_error, RpcError},
38 registry::model::MinerRewardsRecipientConfig,
39 DarkfiNode,
40};
41
42pub struct StratumRpcHandler;
47
48#[async_trait]
49#[rustfmt::skip]
50impl RequestHandler<StratumRpcHandler> for DarkfiNode {
51 async fn handle_request(&self, req: JsonRequest) -> JsonResult {
52 debug!(target: "darkfid::rpc::stratum_rpc", "--> {}", req.stringify().unwrap());
53
54 match req.method.as_str() {
55 "login" => self.stratum_login(req.id, req.params).await,
59 "submit" => self.stratum_submit(req.id, req.params).await,
60 "keepalived" => self.stratum_keepalived(req.id, req.params).await,
61 _ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
62 }
63 }
64
65 async fn connections_mut(&self) -> MutexGuard<'life0, HashSet<StoppableTaskPtr>> {
66 self.registry.stratum_rpc_connections.lock().await
67 }
68}
69
70impl DarkfiNode {
71 pub async fn stratum_login(&self, id: u16, params: JsonValue) -> JsonResult {
124 let validator = self.validator.read().await;
126 if !validator.synced {
127 return JsonResponse::new(JsonValue::from(HashMap::new()), id).into()
128 }
129
130 let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
132 return JsonError::new(InvalidParams, None, id).into()
133 };
134
135 let Some(wallet) = params.get("login") else {
137 return server_error(RpcError::MinerMissingLogin, id, None)
138 };
139 let Some(wallet) = wallet.get::<String>() else {
140 return server_error(RpcError::MinerInvalidLogin, id, None)
141 };
142 let config =
143 match MinerRewardsRecipientConfig::from_str(&self.registry.network, wallet).await {
144 Ok(c) => c,
145 Err(e) => return server_error(e, id, None),
146 };
147
148 let Some(pass) = params.get("pass") else {
150 return server_error(RpcError::MinerMissingPassword, id, None)
151 };
152 let Some(_pass) = pass.get::<String>() else {
153 return server_error(RpcError::MinerInvalidPassword, id, None)
154 };
155
156 let Some(agent) = params.get("agent") else {
158 return server_error(RpcError::MinerMissingAgent, id, None)
159 };
160 let Some(agent) = agent.get::<String>() else {
161 return server_error(RpcError::MinerInvalidAgent, id, None)
162 };
163
164 let Some(algo) = params.get("algo") else {
166 return server_error(RpcError::MinerMissingAlgo, id, None)
167 };
168 let Some(algo) = algo.get::<Vec<JsonValue>>() else {
169 return server_error(RpcError::MinerInvalidAlgo, id, None)
170 };
171
172 let mut found_rx0 = false;
175 for i in algo {
176 let Some(algo) = i.get::<String>() else {
177 return server_error(RpcError::MinerInvalidAlgo, id, None)
178 };
179 if algo == "rx/0" {
180 found_rx0 = true;
181 break
182 }
183 }
184 if !found_rx0 {
185 return server_error(RpcError::MinerRandomXNotSupported, id, None)
186 }
187
188 info!(
190 target: "darkfid::rpc::rpc_stratum::stratum_login",
191 "[RPC-STRATUM] Got login from {wallet} ({agent})",
192 );
193 let (client_id, job_id, job, publisher) = match self
194 .registry
195 .state
196 .write()
197 .await
198 .register_miner(&validator, wallet, &config)
199 .await
200 {
201 Ok(p) => p,
202 Err(e) => {
203 error!(
204 target: "darkfid::rpc::rpc_stratum::stratum_login",
205 "[RPC-STRATUM] Failed to register miner: {e}",
206 );
207 return JsonResponse::new(JsonValue::from(HashMap::new()), id).into()
208 }
209 };
210
211 info!(
213 target: "darkfid::rpc::rpc_stratum::stratum_login",
214 "[RPC-STRATUM] Created new mining job for client {client_id}: {job_id}"
215 );
216 let response = JsonValue::from(HashMap::from([
217 ("id".to_string(), JsonValue::from(client_id)),
218 ("job".to_string(), job),
219 ("status".to_string(), JsonValue::from(String::from("OK"))),
220 ]));
221 (publisher, JsonResponse::new(response, id)).into()
222 }
223
224 pub async fn stratum_submit(&self, id: u16, params: JsonValue) -> JsonResult {
249 let mut validator = self.validator.write().await;
251 if !validator.synced {
252 return miner_status_response(id, "rejected")
253 }
254
255 let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
257 return JsonError::new(InvalidParams, None, id).into()
258 };
259
260 let Some(client_id) = params.get("id") else {
262 return server_error(RpcError::MinerMissingClientId, id, None)
263 };
264 let Some(client_id) = client_id.get::<String>() else {
265 return server_error(RpcError::MinerInvalidClientId, id, None)
266 };
267
268 let mut registry = self.registry.state.write().await;
270 let Some(client) = registry.jobs.get(client_id) else {
271 return miner_status_response(id, "rejected")
272 };
273
274 let Some(job_id) = params.get("job_id") else {
276 return server_error(RpcError::MinerMissingJobId, id, None)
277 };
278 let Some(job_id) = job_id.get::<String>() else {
279 return server_error(RpcError::MinerInvalidJobId, id, None)
280 };
281
282 if &client.job != job_id {
285 return miner_status_response(id, "rejected")
286 }
287 let wallet = client.wallet.clone();
288
289 let Some(block_template) = registry.block_templates.get(&wallet) else {
292 return miner_status_response(id, "rejected")
293 };
294
295 if block_template.submitted {
298 return miner_status_response(id, "rejected")
299 }
300
301 let Some(nonce) = params.get("nonce") else {
303 return server_error(RpcError::MinerMissingNonce, id, None)
304 };
305 let Some(nonce) = nonce.get::<String>() else {
306 return server_error(RpcError::MinerInvalidNonce, id, None)
307 };
308 let Ok(nonce_bytes) = hex::decode(nonce) else {
309 return server_error(RpcError::MinerInvalidNonce, id, None)
310 };
311 if nonce_bytes.len() != 4 {
312 return server_error(RpcError::MinerInvalidNonce, id, None)
313 }
314 let nonce = u32::from_le_bytes(nonce_bytes.try_into().unwrap());
315
316 let Some(result) = params.get("result") else {
318 return server_error(RpcError::MinerMissingResult, id, None)
319 };
320 let Some(_result) = result.get::<String>() else {
321 return server_error(RpcError::MinerInvalidResult, id, None)
322 };
323
324 info!(
325 target: "darkfid::rpc::rpc_stratum::stratum_submit",
326 "[RPC-STRATUM] Got solution submission from client {client_id} for job: {job_id}",
327 );
328
329 let mut block = block_template.block.clone();
331 block.header.nonce = nonce;
332 block.sign(&block_template.secret);
333
334 let mut block_template = block_template.clone();
337
338 if let Err(e) =
340 registry.submit(&mut validator, &self.subscribers, &self.p2p_handler, block).await
341 {
342 error!(
343 target: "darkfid::rpc::rpc_stratum::stratum_submit",
344 "[RPC-STRATUM] Error submitting new block: {e}",
345 );
346
347 if let Err(e) = registry.refresh(&validator).await {
349 error!(
350 target: "darkfid::rpc::rpc_stratum::stratum_submit",
351 "[RPC-STRATUM] Error refreshing registry jobs: {e}",
352 );
353 }
354
355 return miner_status_response(id, "rejected")
356 }
357
358 block_template.submitted = true;
360 registry.block_templates.insert(wallet, block_template);
361
362 miner_status_response(id, "OK")
363 }
364
365 pub async fn stratum_keepalived(&self, id: u16, params: JsonValue) -> JsonResult {
377 let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
379 return JsonError::new(InvalidParams, None, id).into()
380 };
381
382 let Some(client_id) = params.get("id") else {
384 return server_error(RpcError::MinerMissingClientId, id, None)
385 };
386 let Some(client_id) = client_id.get::<String>() else {
387 return server_error(RpcError::MinerInvalidClientId, id, None)
388 };
389
390 if !self.registry.state.read().await.jobs.contains_key(client_id) {
392 return server_error(RpcError::MinerUnknownClient, id, None)
393 };
394
395 miner_status_response(id, "KEEPALIVED")
397 }
398}