Procházet zdrojové kódy

mmproxy: Code cleanups

parazyd před 2 roky
rodič
revize
deb8d39342

+ 2 - 24
Cargo.lock

@@ -1757,14 +1757,14 @@ dependencies = [
 name = "darkfi-mmproxy"
 version = "0.4.1"
 dependencies = [
+ "blake3",
+ "bs58",
  "darkfi",
  "darkfi-serial",
  "easy-parallel",
- "epee-encoding",
  "hex",
  "log",
  "monero",
- "rand 0.8.5",
  "serde",
  "signal-hook",
  "signal-hook-async-std",
@@ -2573,28 +2573,6 @@ dependencies = [
  "syn 2.0.38",
 ]
 
-[[package]]
-name = "epee-encoding"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c44bd9e53829522aae4d3b3a42f9be2e33c1030122fa8e28aba9be3b6385d4f7"
-dependencies = [
- "epee-encoding-derive",
- "sealed",
- "thiserror",
-]
-
-[[package]]
-name = "epee-encoding-derive"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "753b6b75774680e9ba43edd3f41f1c30715008a079a31285e06aeffd5dd3fb2f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.38",
-]
-
 [[package]]
 name = "equivalent"
 version = "1.0.1"

+ 2 - 2
bin/darkfi-mmproxy/Cargo.toml

@@ -13,15 +13,15 @@ darkfi = {path = "../../", features = ["async-daemonize", "async-serial", "syste
 darkfi-serial = {path = "../../src/serial", features = ["async"]}
 
 # Misc
+blake3 = "1.5.0"
 log = "0.4.20"
-rand = "0.8.5"
 
 # Monero
-epee-encoding = {version = "0.5.0", features = ["derive"]}
 monero = {version = "0.19.0", features = ["full"]}
 surf = "2.3.2"
 
 # Encoding
+bs58 = "0.5.0"
 hex = "0.4.3"
 url = "2.4.1"
 uuid = {version = "1.5.0", features = ["v4"]}

+ 6 - 2
bin/darkfi-mmproxy/contrib/start-monerod.sh

@@ -1,6 +1,10 @@
 #!/bin/sh
 set -e
 
+# RandomX activation height:
+# Mainnet: 1978433
+# Testnet: 1308737
+
 MONERO_VERSION="0.18"
 
 MONEROD_BIN="./monero/build/Linux/release-v${MONERO_VERSION}/release/bin/monerod"
@@ -10,5 +14,5 @@ if ! [ -f "$MONEROD_BIN" ]; then
 	exit 1
 fi
 
-"${MONEROD_BIN}" --testnet --fixed-difficulty 2 \
-	--offline --hide-my-port --log-level 4
+"${MONEROD_BIN}" --regtest --fixed-difficulty 1 \
+	--offline --log-level 0 --keep-fakechain

+ 3 - 17
bin/darkfi-mmproxy/darkfi_mmproxy.toml

@@ -1,20 +1,6 @@
-# List of worker logins, comment out to allow anything.
-workers = ["x:x"]
-
 [monerod]
-# Testnet seed:
-# sequence atlas unveil summon pebbles tuesday beer rudely snake
-# rockets different fuselage woven tagged bested dented vegan hover
-# rapid fawns obvious muppet randomly seasons randomly
-#
-# monero-wallet-cli \
-#   --testnet \
-#   --generate-new-wallet wallet_01.bin \
-#   --restore-deterministic-wallet \
-#   --electrum-seed="$SEED"
-#   --password "" \
-#   --log-file wallet_01.log
-wallet_address = "9wviCeWe2D8XS82k2ovp5EUYLzBt9pYNW2LXUFsZiv8S3Mt21FZ5qQaAroko1enzw3eGr9qC7X1D7Geoo2RrAotYPwq9Gm"
+# Monero network to use (mainnet/testnet)
+network = "mainnet"
 
 # monerod RPC address
-monerod_rpc = "http://127.0.0.1:28081/json_rpc"
+rpc = "http://127.0.0.1:18081/json_rpc"

+ 32 - 125
bin/darkfi-mmproxy/src/main.rs

@@ -24,9 +24,8 @@ use std::{
 use darkfi::{
     async_daemonize, cli_desc,
     rpc::{
-        jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResult, JsonSubscriber},
+        jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResult},
         server::{listen_and_serve, RequestHandler},
-        util::JsonValue,
     },
     system::{StoppableTask, StoppableTaskPtr},
     Error, Result,
@@ -35,8 +34,8 @@ use darkfi_serial::async_trait;
 use log::{error, info};
 use serde::Deserialize;
 use smol::{
-    channel,
     lock::{Mutex, MutexGuard, RwLock},
+    net::TcpStream,
     stream::StreamExt,
     Executor,
 };
@@ -46,7 +45,6 @@ use url::Url;
 use uuid::Uuid;
 
 mod error;
-mod monero;
 mod stratum;
 
 const CONFIG_FILE: &str = "darkfi_mmproxy.toml";
@@ -77,66 +75,28 @@ struct Args {
     log: Option<String>,
 
     #[structopt(flatten)]
-    monerod: Monerod,
+    monerod: MonerodArgs,
 }
 
 #[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
 #[structopt()]
-struct Monerod {
-    #[structopt(long, default_value = "")]
+struct MonerodArgs {
+    #[structopt(long, default_value = "mainnet")]
     /// Mining reward wallet address
-    wallet_address: String,
+    network: String,
 
     #[structopt(long, default_value = "http://127.0.0.1:28081/json_rpc")]
     /// monerod JSON-RPC server listen URL
-    monerod_rpc: Url,
-}
-
-struct Worker {
-    /// JSON-RPC notification subscriber, used to send job notifications
-    job_sub: JsonSubscriber,
-    /// Current job ID for the worker
-    job_id: Uuid,
-    /// Keepalive sender channel, pinged from stratum keepalived
-    ka_send: channel::Sender<()>,
-    /// Background keepalive task reference
-    ka_task: StoppableTaskPtr,
-}
-
-impl Worker {
-    fn new(
-        job_sub: JsonSubscriber,
-        ka_send: channel::Sender<()>,
-        ka_task: StoppableTaskPtr,
-    ) -> Self {
-        Self { job_sub, job_id: Uuid::new_v4(), ka_send, ka_task }
-    }
-
-    async fn send_job(&mut self, blob: String, target: String) -> Result<()> {
-        // Update job id
-        self.job_id = Uuid::new_v4();
-
-        let params: JsonValue = HashMap::from([
-            ("blob".to_string(), blob.into()),
-            ("job_id".to_string(), self.job_id.to_string().into()),
-            ("target".to_string(), target.into()),
-        ])
-        .into();
-
-        info!("Sending mining job notification to worker");
-        self.job_sub.notify(params).await;
-
-        Ok(())
-    }
+    rpc: Url,
 }
 
 struct MiningProxy {
-    /// monerod settings
-    monerod: Monerod,
-    /// Worker logins
-    logins: HashMap<String, String>,
+    /// monerod network type
+    monerod_network: monero::Network,
+    /// monerod RPC address
+    monerod_rpc: Url,
     /// Workers UUIDs
-    workers: Arc<RwLock<HashMap<Uuid, Worker>>>,
+    workers: Arc<RwLock<HashMap<Uuid, stratum::Worker>>>,
     /// JSON-RPC connection tracker
     rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
     /// Main async executor reference
@@ -144,27 +104,29 @@ struct MiningProxy {
 }
 
 impl MiningProxy {
-    async fn new(
-        monerod: Monerod,
-        logins: HashMap<String, String>,
-        executor: Arc<Executor<'static>>,
-    ) -> Result<Self> {
-        let self_ = Self {
-            monerod,
-            logins,
-            workers: Arc::new(RwLock::new(HashMap::new())),
-            rpc_connections: Mutex::new(HashSet::new()),
-            executor,
+    async fn new(monerod: MonerodArgs, executor: Arc<Executor<'static>>) -> Result<Self> {
+        let monerod_network = match monerod.network.as_str() {
+            "mainnet" => monero::Network::Mainnet,
+            "testnet" => monero::Network::Testnet,
+            _ => {
+                error!("Invalid Monero network \"{}\"", monerod.network);
+                return Err(Error::Custom("Invalid Monero network".to_string()))
+            }
         };
 
-        // Test that monerod is reachable
-        let req = JsonRequest::new("get_block_count", vec![].into());
-        if let Err(e) = self_.oneshot_request(req).await {
-            error!("Could not reach monerod: {}", e);
-            return Err(Error::Custom("Could not reach monerod".to_string()))
+        // Test that monerod RPC is reachable
+        match TcpStream::connect(monerod.rpc.socket_addrs(|| None)?[0]).await {
+            Ok(_) => {}
+            Err(e) => {
+                error!("Failed connecting to monerod RPC: {}", e);
+                return Err(e.into())
+            }
         }
 
-        Ok(self_)
+        let workers = Arc::new(RwLock::new(HashMap::new()));
+        let rpc_connections = Mutex::new(HashSet::new());
+
+        Ok(Self { monerod_network, monerod_rpc: monerod.rpc, workers, rpc_connections, executor })
     }
 }
 
@@ -182,46 +144,6 @@ impl RequestHandler for MiningProxy {
             "submit" => self.stratum_submit(req.id, req.params).await,
             "keepalived" => self.stratum_keepalived(req.id, req.params).await,
 
-            // Monero daemon methods
-            "get_block_count" => self.monero_get_block_count(req.id, req.params).await,
-            "getblockcount" => self.monero_get_block_count(req.id, req.params).await,
-            "on_get_block_hash" => self.monero_on_get_block_hash(req.id, req.params).await,
-            "on_getblockhash" => self.monero_on_get_block_hash(req.id, req.params).await,
-            "get_block_template" => self.monero_get_block_template(req.id, req.params).await,
-            "getblocktemplate" => self.monero_get_block_template(req.id, req.params).await,
-            "submit_block" => self.monero_submit_block(req.id, req.params).await,
-            "submitblock" => self.monero_submit_block(req.id, req.params).await,
-            "generateblocks" => self.monero_generateblocks(req.id, req.params).await,
-
-            /*
-            "get_last_block_header" => self.monero_get_last_block_header(req.id, req.params).await,
-            "get_block_header_by_hash" => self.monero_get_block_header_by_hash(req.id, req.params).await,
-            "get_block_header_by_height" => self.monero_get_block_header_by_height(req.id, req.params).await,
-            "get_block_headers_range" => self.monero_get_block_headers_range(req.id, req.params).await,
-            "get_block" => self.monero_get_block(req.id, req.params).await,
-            "get_connections" => self.monero_get_connections(req.id, req.params).await,
-            "get_info" => self.monero_get_info(req.id, req.params).await,
-            "hard_fork_info" => self.monero_hard_fork_info(req.id, req.params).await,
-            "set_bans" => self.monero_set_bans(req.id, req.params).await,
-            "get_bans" => self.monero_get_bans(req.id, req.params).await,
-            "banned" => self.monero_banned(req.id, req.params).await,
-            "flush_txpool" => self.monero_flush_txpool(req.id, req.params).await,
-            "get_output_histogram" => self.monero_get_output_histogram(req.id, req.params).await,
-            "get_version" => self.monero_get_version(req.id, req.params).await,
-            "get_coinbase_tx_sum" => self.monero_get_coinbase_tx_sum(req.id, req.params).await,
-            "get_fee_estimate" => self.monero_get_fee_estimate(req.id, req.params).await,
-            "get_alternate_chains" => self.monero_get_alternate_chains(req.id, req.params).await,
-            "relay_tx" => self.monero_relay_tx(req.id, req.params).await,
-            "sync_info" => self.monero_sync_info(req.id, req.params).await,
-            "get_txpool_backlog" => self.monero_get_txpool_backlog(req.id, req.params).await,
-            "get_output_distribution" => self.monero_get_output_distribution(req.id, req.params).await,
-            "get_miner_data" => self.monero_get_miner_data(req.id, req.params).await,
-            "prune_blockchain" => self.monero_prune_blockchain(req.id, req.params).await,
-            "calc_pow" => self.monero_calc_pow(req.id, req.params).await,
-            "flush_cache" => self.monero_flush_cache(req.id, req.params).await,
-            "add_aux_pow" => self.monero_add_aux_pow(req.id, req.params).await,
-            */
-
             _ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
         }
     }
@@ -234,23 +156,8 @@ impl RequestHandler for MiningProxy {
 async_daemonize!(realmain);
 async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
     info!("Starting DarkFi x Monero merge mining proxy...");
-    // Parse worker logins
-    let mut logins = HashMap::new();
-    for worker in args.workers {
-        let mut split = worker.split(':');
-        let user = split.next().unwrap().to_string();
-        let pass = split.next().unwrap().to_string();
-        info!("Whitelisting worker \"{}:{}\"", user, pass);
-        logins.insert(user, pass);
-    }
-
-    if args.monerod.wallet_address.is_empty() {
-        error!("Wallet address empty. Please set it in your config.");
-        return Err(Error::Custom("Wallet address empty".to_string()))
-    }
 
-    info!("Instantiating MiningProxy with wallet: {}", args.monerod.wallet_address);
-    let mmproxy = Arc::new(MiningProxy::new(args.monerod, logins, ex.clone()).await?);
+    let mmproxy = Arc::new(MiningProxy::new(args.monerod, ex.clone()).await?);
     let mmproxy_ = Arc::clone(&mmproxy);
 
     info!("Starting JSON-RPC server");

+ 0 - 386
bin/darkfi-mmproxy/src/monero.rs

@@ -1,386 +0,0 @@
-/* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2023 Dyne.org foundation
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-use std::collections::HashMap;
-
-use darkfi::{
-    rpc::{
-        jsonrpc::{
-            ErrorCode::{InternalError, InvalidParams},
-            JsonError, JsonRequest, JsonResponse, JsonResult,
-        },
-        util::JsonValue,
-    },
-    Error, Result,
-};
-use log::{debug, error, info};
-use monero::blockdata::transaction::{ExtraField, RawExtraField, SubField::MergeMining};
-
-use super::MiningProxy;
-
-impl MiningProxy {
-    /// Perform a oneshot HTTP JSON-RPC request to the set monerod endpoint.
-    /// This is a single request-reply which we disconnect after recieving the reply.
-    pub async fn oneshot_request(&self, req: JsonRequest) -> Result<JsonValue> {
-        let client = surf::Client::new();
-
-        let mut response = match client
-            .get(&self.monerod.monerod_rpc)
-            .header("Content-Type", "application/json")
-            .body(req.stringify().unwrap())
-            .send()
-            .await
-        {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "rpc::monero::oneshot_request", "[RPC] Error sending RPC request to monerod: {}", e);
-                return Err(Error::ParseFailed("Failed sending monerod RPC request"))
-            }
-        };
-
-        let response_bytes = match response.body_bytes().await {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "rpc::monero::get_block_count", "[RPC] Error reading monerod RPC response: {}", e);
-                return Err(Error::ParseFailed("Failed reading monerod RPC reponse"))
-            }
-        };
-
-        let response_string = match String::from_utf8(response_bytes) {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "rpc::monero::get_block_count", "[RPC] Error parsing monerod RPC response: {}", e);
-                return Err(Error::ParseFailed("Failed parsing monerod RPC reponse"))
-            }
-        };
-
-        let response_json: JsonValue = match response_string.parse() {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "rpc::monero::get_block_count", "[RPC] Error parsing monerod RPC response: {}", e);
-                return Err(Error::ParseFailed("Failed parsing monerod RPC reponse"))
-            }
-        };
-
-        Ok(response_json)
-    }
-
-    /// Look up how many blocks are in the longest chain known to the node.
-    /// <https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_block_count>
-    pub async fn monero_get_block_count(&self, id: u16, _params: JsonValue) -> JsonResult {
-        debug!(target: "rpc::monero", "get_block_count()");
-
-        // This request can just passthrough
-        let req = JsonRequest::new("get_block_count", vec![].into());
-        let rep = match self.oneshot_request(req).await {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "rpc::monero::get_block_count", "[RPC] {}", e);
-                return JsonError::new(InternalError, Some(e.to_string()), id).into()
-            }
-        };
-
-        JsonResponse::new(rep, id).into()
-    }
-
-    /// Look up a block's hash by its height.
-    /// <https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#on_get_block_hash>
-    pub async fn monero_on_get_block_hash(&self, id: u16, params: JsonValue) -> JsonResult {
-        debug!(target: "rpc::monero", "on_get_block_hash()");
-
-        let Some(params) = params.get::<Vec<JsonValue>>() else {
-            return JsonError::new(InvalidParams, None, id).into()
-        };
-
-        if !params.len() != 1 || params[0].is_number() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let Some(block_height) = params[0].get::<f64>() else {
-            return JsonError::new(InvalidParams, None, id).into()
-        };
-
-        let req =
-            JsonRequest::new("on_get_block_hash", vec![JsonValue::Number(*block_height)].into());
-        let rep = match self.oneshot_request(req).await {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "rpc::monero::get_block_count", "[RPC] {}", e);
-                return JsonError::new(InternalError, Some(e.to_string()), id).into()
-            }
-        };
-
-        JsonResponse::new(rep, id).into()
-    }
-
-    /// Get a block template on which mining a new block.
-    /// <https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#get_block_template>
-    pub async fn monero_get_block_template(&self, id: u16, params: JsonValue) -> JsonResult {
-        debug!(target: "rpc::monero", "get_block_template()");
-
-        if !params.is_object() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let params = params.get::<HashMap<String, JsonValue>>().unwrap();
-
-        if !params.contains_key("wallet_address") || !params.contains_key("reserve_size") {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        let Some(wallet_address) = params["wallet_address"].get::<String>() else {
-            return JsonError::new(InvalidParams, None, id).into()
-        };
-
-        let Some(_reserve_size) = params["reserve_size"].get::<f64>() else {
-            return JsonError::new(InvalidParams, None, id).into()
-        };
-
-        // The MergeMining tag and anything else going into ExtraField should
-        // be done here, so we can pass the correct reserve_size.
-
-        // Create the Merge Mining Tag: (`depth`, `merkle_root`)
-        let mm_tag = MergeMining(Some(monero::VarInt(32)), monero::Hash([0_u8; 32]));
-
-        // Construct tx_extra from all the extra fields we have to add to the coinbase
-        // transaction in the block we're mining.
-        let tx_extra: RawExtraField = ExtraField(vec![mm_tag]).into();
-
-        // Create request. Usually, xmrig will just request a job, so this endpoint
-        // isn't really used through JSON-RPC. We use it from other methods, which
-        // should then include the proper wallet address to plug in. The wallet
-        // address can be set in mmproxy's config or via CLI flags.
-        //
-        // `reserve_size` is overridden with the size of `tx_extra` created above.
-        let req = JsonRequest::new(
-            "get_block_template",
-            HashMap::from([
-                ("wallet_address".to_string(), (*wallet_address).clone().into()),
-                ("reserve_size".to_string(), (tx_extra.0.len() as f64).into()),
-            ])
-            .into(),
-        );
-
-        // Get block template from monerod
-        let mut rep = match self.oneshot_request(req).await {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "rpc::monero::get_block_template", "[RPC] {}", e);
-                return JsonError::new(InternalError, Some(e.to_string()), id).into()
-            }
-        };
-
-        // Now we have to modify the block template:
-        // * reserve_size has to be the size of the data we want to put in the block
-        // * blocktemplate_blob has the reserved bytes, they're in the tx_extra field
-        //   of the coinbase tx, which is then hashed with other txs into Merkle root
-        //   which is what's in the blockhashing_blob
-
-        // Deserialize the block template
-        let mut block_template = monero::consensus::deserialize::<monero::Block>(
-            &hex::decode(rep["result"]["blocktemplate_blob"].get::<String>().unwrap()).unwrap(),
-        )
-        .unwrap();
-
-        // Modify the coinbase tx with our additional merge mining data
-        block_template.miner_tx.prefix.extra = tx_extra;
-
-        // Replace the blocktemplate blob
-        rep["result"]["blocktemplate_blob"] =
-            JsonValue::String(hex::encode(monero::consensus::serialize(&block_template)));
-
-        // Replace the blockhashing blob
-        rep["result"]["blockhashing_blob"] =
-            JsonValue::String(hex::encode(&block_template.serialize_hashable()));
-
-        // Pass the modified response to the client
-        JsonResponse::new(rep, id).into()
-    }
-
-    /// Submit a mined block to the network
-    /// <https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#submit_block>
-    pub async fn monero_submit_block(&self, id: u16, params: JsonValue) -> JsonResult {
-        debug!(target: "rpc::monero", "submit_block()");
-
-        let Some(params_vec) = params.get::<Vec<JsonValue>>() else {
-            return JsonError::new(InvalidParams, None, id).into()
-        };
-
-        if params_vec.is_empty() {
-            return JsonError::new(InvalidParams, None, id).into()
-        }
-
-        // Deserialize the block blob(s) to make sure it's a valid block
-        for element in params_vec.iter() {
-            let Some(block_hex) = element.get::<String>() else {
-                return JsonError::new(InvalidParams, None, id).into()
-            };
-
-            let Ok(block_bytes) = hex::decode(block_hex) else {
-                return JsonError::new(InvalidParams, None, id).into()
-            };
-
-            let Ok(block) = monero::consensus::deserialize::<monero::Block>(&block_bytes) else {
-                return JsonError::new(InvalidParams, None, id).into()
-            };
-
-            info!("[RPC] Got submitted Monero block id {}", block.id());
-        }
-
-        // Now when all the blocks submitted are valid, we'll just forward them to
-        // monerod to submit onto the network.
-        let req = JsonRequest::new("submit_block", params);
-        let rep = match self.oneshot_request(req).await {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "rpc::monero::submit_block", "[RPC] {}", e);
-                return JsonError::new(InternalError, Some(e.to_string()), id).into()
-            }
-        };
-
-        JsonResponse::new(rep, id).into()
-    }
-
-    /// Generate a block and specify the address to receive the coinbase reward.
-    /// <https://www.getmonero.org/resources/developer-guides/daemon-rpc.html#generateblocks>
-    pub async fn monero_generateblocks(&self, id: u16, params: JsonValue) -> JsonResult {
-        debug!(target: "rpc::monero", "generateblocks()");
-
-        // This request can just passthrough
-        let req = JsonRequest::new("generateblocks", params);
-        let rep = match self.oneshot_request(req).await {
-            Ok(v) => v,
-            Err(e) => {
-                error!(target: "rpc::monero::generateblocks", "[RPC] {}", e);
-                return JsonError::new(InternalError, Some(e.to_string()), id).into()
-            }
-        };
-
-        JsonResponse::new(rep, id).into()
-    }
-
-    /*
-    pub async fn monero_get_last_block_header(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_block_header_by_hash(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_block_header_by_height(
-        &self,
-        id: u16,
-        params: JsonValue,
-    ) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_block_headers_range(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_block(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_connections(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_info(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_hard_fork_info(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_set_bans(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_bans(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_banned(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_flush_txpool(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_output_histogram(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_version(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_coinbase_tx_sum(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_fee_estimate(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_alternate_chains(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_relay_tx(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_sync_info(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_txpool_backlog(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_output_distribution(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_get_miner_data(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_prune_blockchain(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_calc_pow(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_flush_cache(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-
-    pub async fn monero_add_aux_pow(&self, id: u16, params: JsonValue) -> JsonResult {
-        todo!()
-    }
-    */
-}

+ 575 - 112
bin/darkfi-mmproxy/src/stratum.rs

@@ -16,52 +16,250 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{collections::HashMap, sync::Arc, time::Duration};
+use std::{collections::HashMap, io, str::FromStr, sync::Arc, time::Duration};
 
 use darkfi::{
     rpc::{
-        jsonrpc::{ErrorCode, JsonError, JsonResponse, JsonResult, JsonSubscriber},
+        jsonrpc::{
+            ErrorCode::{InternalError, InvalidParams, ServerError},
+            JsonError, JsonRequest, JsonResponse, JsonResult, JsonSubscriber,
+        },
         util::JsonValue,
     },
-    system::{timeout::timeout, StoppableTask},
+    system::{sleep, timeout::timeout, StoppableTask, StoppableTaskPtr},
     Error, Result,
 };
 use log::{debug, error, info, warn};
-use rand::{rngs::OsRng, Rng};
+use monero::blockdata::transaction::{ExtraField, RawExtraField, SubField::MergeMining};
 use smol::{channel, lock::RwLock};
+use url::Url;
 use uuid::Uuid;
 
-use super::{error::RpcError, MiningProxy, Worker};
+use super::{error::RpcError, MiningProxy};
 
 /// Algo string representing Monero's RandomX
 pub const RANDOMX_ALGO: &str = "rx/0";
 
+/// A mining job instance
+#[derive(Clone)]
+struct MiningJob {
+    /// Current job ID for the worker
+    pub job_id: blake3::Hash,
+    /// Full block being mined
+    pub block: monero::Block,
+    /// Difficulty target,
+    pub target: String,
+    /// Block height
+    pub height: f64,
+    /// RandomX seed hash
+    pub seed_hash: String,
+}
+
+/// Single worker connected to the mining proxy
+pub struct Worker {
+    /// Wallet address
+    addr: monero::Address,
+    /// Miner useragent
+    _agent: String,
+    /// JSON-RPC notification subscriber, used to send new job notifications
+    job_sub: JsonSubscriber,
+    /// Background keepalive task reference
+    _ka_task: StoppableTaskPtr,
+    /// Keepalive sender channel, pinged from Stratum keepalived
+    ka_send: channel::Sender<()>,
+    /// Background mining job task reference
+    _job_task: StoppableTaskPtr,
+    /// Block submit trigger sender channel, pinged from Stratum submit
+    submit_send: channel::Sender<()>,
+    /// Current mining job
+    mining_job: MiningJob,
+}
+
+impl Worker {
+    async fn notify_job(&mut self, mining_job: MiningJob) -> Result<()> {
+        // Update the mining job
+        self.mining_job = mining_job.clone();
+
+        // Build notification params
+        let params: JsonValue = JsonValue::Object(HashMap::from([
+            ("blob".to_string(), hex::encode(mining_job.block.serialize_hashable()).into()),
+            ("job_id".to_string(), mining_job.job_id.to_string().into()),
+            ("target".to_string(), mining_job.target.into()),
+            ("height".to_string(), mining_job.height.into()),
+            ("seed_hash".to_string(), mining_job.seed_hash.into()),
+            ("algo".to_string(), RANDOMX_ALGO.to_string().into()),
+        ]));
+
+        info!(
+            target: "worker::notify_job",
+            "[STRATUM] Sending mining job notification to worker",
+        );
+        self.job_sub.notify(params).await;
+        Ok(())
+    }
+}
+
+/// Send a HTTP JSON-RPC request to the given monerod RPC endpoint
+async fn monerod_request(endpoint: &Url, req: JsonRequest) -> Result<JsonValue> {
+    let client = surf::Client::new();
+
+    let mut response = match client
+        .get(endpoint)
+        .header("Content-Type", "application/json")
+        .body(req.stringify().unwrap())
+        .send()
+        .await
+    {
+        Ok(v) => v,
+        Err(e) => {
+            error!(
+                target: "stratum::monerod_request",
+                "[STRATUM] Failed sending RPC request to monerod: {}", e,
+            );
+            return Err(io::Error::new(io::ErrorKind::Other, e).into())
+        }
+    };
+
+    let response_bytes = match response.body_bytes().await {
+        Ok(v) => v,
+        Err(e) => {
+            error!(
+                target: "stratum::monerod_request",
+                "[STRATUM] Failed reading monerod RPC response: {}", e,
+            );
+            return Err(io::Error::new(io::ErrorKind::Other, e).into())
+        }
+    };
+
+    let response_string = match String::from_utf8(response_bytes) {
+        Ok(v) => v,
+        Err(e) => {
+            error!(
+                target: "stratum::monerod_request",
+                "[STRATUM] Failed parsing monerod RPC response: {}", e,
+            );
+            return Err(io::Error::new(io::ErrorKind::Other, e).into())
+        }
+    };
+
+    let response_json: JsonValue = match response_string.parse() {
+        Ok(v) => v,
+        Err(e) => {
+            error!(
+                target: "stratum::monerod_request",
+                "[STRATUM] Failed parsing monerod RPC response JSON: {}", e,
+            );
+            return Err(io::Error::new(io::ErrorKind::Other, e).into())
+        }
+    };
+
+    Ok(response_json)
+}
+
+/// Perform getblocktemplate from monerod and inject it with the
+/// necessary merge mining data.
+/// Returns data necessary to create a mining job
+async fn getblocktemplate(endpoint: &Url, wallet_address: &monero::Address) -> Result<MiningJob> {
+    // Create the Merge Mining Tag: (`depth`, `merkle_root`)
+    let mm_tag = MergeMining(Some(monero::VarInt(32)), monero::Hash([0_u8; 32]));
+
+    // Construct `tx_extra` from all the extra fields we have to
+    // add to the coinbase transaction in the block we're mining
+    let tx_extra: RawExtraField = ExtraField(vec![mm_tag]).into();
+
+    // Create the monerod JSON-RPC request. `reserve_size` is the space
+    // we need to create for the `tx_extra` field created above.
+    let req = JsonRequest::new(
+        "get_block_template",
+        HashMap::from([
+            ("wallet_address".to_string(), wallet_address.to_string().into()),
+            ("reserve_size".to_string(), (tx_extra.0.len() as f64).into()),
+        ])
+        .into(),
+    );
+
+    // Get block template from monerod
+    let rep = match monerod_request(endpoint, req).await {
+        Ok(v) => v,
+        Err(e) => {
+            error!(
+                target: "stratum::getblocktemplate",
+                "[STRATUM] Failed sending getblocktemplate to monerod: {}", e,
+            );
+            return Err(io::Error::new(io::ErrorKind::Other, e).into())
+        }
+    };
+
+    // Now we have to modify the block template:
+    // * Update the coinbase tx with our tx_extra field
+    // * Update the `blockhashing_blob` in order to perform correct PoW
+
+    // Deserialize the block template
+    let mut block_template = monero::consensus::deserialize::<monero::Block>(
+        &hex::decode(rep["result"]["blocktemplate_blob"].get::<String>().unwrap()).unwrap(),
+    )
+    .unwrap();
+
+    // Modify the coinbase tx with our additional merge mining data
+    block_template.miner_tx.prefix.extra = tx_extra;
+
+    // TODO: Get the difficulty target
+    let target = "b88d0600".to_string();
+
+    // Get the remaining metadata
+    let height = *rep["result"]["height"].get::<f64>().unwrap();
+    let seed_hash = rep["result"]["seed_hash"].get::<String>().unwrap().to_string();
+
+    // Create a deterministic job id
+    let mut hasher = blake3::Hasher::new();
+    hasher.update(&wallet_address.as_bytes());
+    hasher.update(&height.to_le_bytes());
+    hasher.update(seed_hash.as_bytes());
+    let job_id = hasher.finalize();
+
+    // Return the necessary data
+    Ok(MiningJob { job_id, block: block_template, target, height, seed_hash })
+}
+
 impl MiningProxy {
-    /// Background task listening for keepalives from a worker, if timeout is reached
-    /// the worker will be dropped.
+    /// Background task listening for keepalives from a worker.
+    /// If timeout is reached, the worker will be dropped.
     async fn keepalive_task(
         workers: Arc<RwLock<HashMap<Uuid, Worker>>>,
         uuid: Uuid,
         ka_recv: channel::Receiver<()>,
     ) -> Result<()> {
-        debug!("Spawned keepalive_task for worker {}", uuid);
-        const TIMEOUT: Duration = Duration::from_secs(60);
+        debug!(target: "stratum::keepalive_task", "Spawned keepalive_task for worker {}", uuid);
+        const TIMEOUT: Duration = Duration::from_secs(65);
 
         loop {
             let Ok(r) = timeout(TIMEOUT, ka_recv.recv()).await else {
                 // Timeout, remove worker
-                warn!("keepalive_task {} worker timed out", uuid);
+                warn!(
+                    target: "stratum::keepalive_task",
+                    "keepalive_task for worker {} timed out", uuid,
+                );
                 workers.write().await.remove(&uuid);
                 break
             };
 
             match r {
                 Ok(()) => {
-                    debug!("keepalive_task {} got ping", uuid);
+                    debug!(
+                        target: "stratum::keepalive_task",
+                        "keepalive_task for worker {} got ping", uuid,
+                    );
                     continue
                 }
                 Err(e) => {
-                    error!("keepalive_task {} channel recv error: {}", uuid, e);
+                    error!(
+                        target: "stratum::keepalive_task",
+                        "keepalive_task for worker {} channel recv error: {}", uuid, e,
+                    );
+                    warn!(
+                        target: "stratum::keepalive_task",
+                        "Dropping worker {}", uuid,
+                    );
                     workers.write().await.remove(&uuid);
                     break
                 }
@@ -71,84 +269,207 @@ impl MiningProxy {
         Ok(())
     }
 
-    /// Stratum login method. `darkfi-mmproxy` will check that it is a valid worker
-    /// login, and will also search for `RANDOMX_ALGO`.
-    /// TODO: More proper error codes
-    pub async fn stratum_login(&self, id: u16, params: JsonValue) -> JsonResult {
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 1 || !params[0].is_object() {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+    /// Background task used to notify a worker about new mining jobs.
+    /// `keepalive_task` iis able to remove workers from the worker pool,
+    /// so this task can easily exit if the worker is not found.
+    async fn job_task(
+        workers: Arc<RwLock<HashMap<Uuid, Worker>>>,
+        uuid: Uuid,
+        endpoint: Url,
+        submit_recv: channel::Receiver<()>,
+    ) -> Result<()> {
+        debug!(target: "stratum::job_task", "Spawned job_task for worker {}", uuid);
+        const POLL_INTERVAL: Duration = Duration::from_secs(60);
+
+        // Comfy wait for settling the Stratum login RPC call
+        sleep(2).await;
+
+        // In this loop, we'll be getting the block template for mining.
+        // At the beginning of the loop, we'll perform a getblocktemplate,
+        // and then inject our Merge Mining stuff, and forward it to the
+        // miner. After the notification, we'll either poll or wait for a
+        // trigger for a submitted block and reiterate the loop again in
+        // order to get the next mining job.
+        loop {
+            // Get the workers lock and the worker reference
+            let mut workers_ptr = workers.write().await;
+            let Some(worker) = workers_ptr.get_mut(&uuid) else {
+                info!(
+                    target: "stratum::job_task",
+                    "[STRATUM] Worker {} disconnected, exiting job_task", uuid,
+                );
+                break
+            };
+
+            // Get the next mining job
+            let mining_job = match getblocktemplate(&endpoint, &worker.addr).await {
+                Ok(v) => v,
+                Err(e) => {
+                    error!(
+                        target: "stratum::job_task",
+                        "[STRATUM] Failed fetching getblocktemplate for worker {}: {}", uuid, e,
+                    );
+                    warn!(
+                        target: "stratum::job_task",
+                        "[STRATUM] Exiting job_task for worker {}", uuid,
+                    );
+                    break
+                }
+            };
+
+            // In case it's the same job, we'll wait and try again
+            if worker.mining_job.job_id == mining_job.job_id {
+                match timeout(POLL_INTERVAL, submit_recv.recv()).await {
+                    Ok(_) => continue,
+                    Err(_) => continue,
+                }
+            }
+
+            // Notify the worker about the new job
+            if let Err(e) = worker.notify_job(mining_job).await {
+                error!(
+                    target: "stratum::job_task",
+                    "[STRATUM] Failed sending job to worker {}: {}", uuid, e,
+                );
+                warn!(
+                    target: "stratum::job_task",
+                    "[STRATUM] Exiting job_task for worker {}", uuid,
+                );
+                break
+            }
+
+            drop(workers_ptr);
+
+            // Now poll or wait for a trigger for a new job.
+            match timeout(POLL_INTERVAL, submit_recv.recv()).await {
+                Ok(_) => continue,
+                Err(_) => continue,
+            }
         }
 
-        let params = params[0].get::<HashMap<String, JsonValue>>().unwrap();
+        Ok(())
+    }
+
+    /// Stratum login method
+    ///
+    /// `darkfi-mmproxy` will check that the worker provided a valid
+    /// address as the username, and will enforce `RANDOMX_ALGO` to
+    /// be supported. Upon success, we will fetch the block template
+    /// from monerod, inject it with our necessary merge mining info,
+    /// and forward it to the worker.
+    /// Additionally, we will spawn background tasks for new job and
+    /// keepalive notifications for this worker.
+    pub async fn stratum_login(&self, id: u16, params: JsonValue) -> JsonResult {
+        let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
+            return JsonError::new(InvalidParams, None, id).into()
+        };
 
         if !params.contains_key("login") ||
             !params.contains_key("pass") ||
             !params.contains_key("agent") ||
             !params.contains_key("algo")
         {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+            return JsonError::new(InvalidParams, None, id).into()
         }
 
         let Some(login) = params["login"].get::<String>() else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+            return JsonError::new(InvalidParams, Some("Invalid \"login\" object".to_string()), id)
+                .into()
         };
 
-        let Some(pass) = params["pass"].get::<String>() else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        let Some(_pass) = params["pass"].get::<String>() else {
+            return JsonError::new(InvalidParams, Some("Invalid \"pass\" object".to_string()), id)
+                .into()
         };
 
-        let Some(_agent) = params["agent"].get::<String>() else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        let Some(agent) = params["agent"].get::<String>() else {
+            return JsonError::new(InvalidParams, Some("Invalid \"agent\" object".to_string()), id)
+                .into()
         };
 
         let Some(algos) = params["algo"].get::<Vec<JsonValue>>() else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+            return JsonError::new(InvalidParams, Some("Invalid \"algo\" object".to_string()), id)
+                .into()
         };
 
-        // We'll only support rx/0 algo.
-        let mut found_xmr_algo = false;
-        for algo in algos {
+        // We'll only support `RANDOMX_ALGO`
+        let mut found_randomx_algo = false;
+        for algo in algos.iter() {
             if !algo.is_string() {
-                return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+                return JsonError::new(InvalidParams, Some("Algo is not a string".to_string()), id)
+                    .into()
             }
 
             if algo.get::<String>().unwrap() == RANDOMX_ALGO {
-                found_xmr_algo = true;
+                found_randomx_algo = true;
                 break
             }
         }
 
-        if !found_xmr_algo {
+        if !found_randomx_algo {
             return JsonError::new(
                 RpcError::UnsupportedMiningAlgo.into(),
-                Some("Unsupported mining algo".to_string()),
+                Some("Unsupported mining algos".to_string()),
                 id,
             )
             .into()
         }
 
-        // Check valid login
-        let Some(known_pass) = self.logins.get(login) else {
+        // Check valid login. We will parse the username as a Monero
+        // address, and validate that it corresponds to the network
+        // we're mining on.
+        let addr_bytes = match bs58::decode(login).into_vec() {
+            Ok(v) => v,
+            Err(e) => {
+                return JsonError::new(
+                    RpcError::InvalidWorkerLogin.into(),
+                    Some(format!("Invalid Monero address login: {}", e)),
+                    id,
+                )
+                .into()
+            }
+        };
+
+        let addr = match monero::Address::from_bytes(&addr_bytes) {
+            Ok(v) => v,
+            Err(e) => {
+                return JsonError::new(
+                    RpcError::InvalidWorkerLogin.into(),
+                    Some(format!("Invalid Monero address login: {}", e)),
+                    id,
+                )
+                .into()
+            }
+        };
+
+        if addr.network != self.monerod_network {
             return JsonError::new(
                 RpcError::InvalidWorkerLogin.into(),
-                Some("Unknown worker username".to_string()),
+                Some(format!(
+                    "Invalid Monero address network, expected \"{:?}\"",
+                    self.monerod_network
+                )),
                 id,
             )
             .into()
-        };
+        }
 
-        if known_pass != pass {
+        if addr.addr_type != monero::AddressType::Standard {
             return JsonError::new(
                 RpcError::InvalidWorkerLogin.into(),
-                Some("Invalid worker password".to_string()),
+                Some(format!(
+                    "Invalid Monero address type, expected \"{}\"",
+                    monero::AddressType::Standard
+                )),
                 id,
             )
             .into()
         }
 
-        // Login success, generate UUID
-        let uuid = Uuid::new_v4();
+        // Now we have a valid address for mining.
+        // Create a new UUID for the worker, and initialize the `Worker`
+        // struct that will live throughout the miner's lifetime.
+        let worker_uuid = Uuid::new_v4();
 
         // Create job subscriber
         let job_sub = JsonSubscriber::new("job");
@@ -156,127 +477,269 @@ impl MiningProxy {
         // Create keepalive channel
         let (ka_send, ka_recv) = channel::unbounded();
 
+        // Create submit trigger channel
+        let (submit_send, submit_recv) = channel::unbounded();
+
         // Create background keepalive task
         let ka_task = StoppableTask::new();
 
+        // Create background job task
+        let job_task = StoppableTask::new();
+
+        // Get the current mining job for the worker
+        let mining_job = match getblocktemplate(&self.monerod_rpc, &addr).await {
+            Ok(v) => v,
+            Err(e) => {
+                error!(
+                    target: "stratum::login",
+                    "[STRATUM] Failed fetching block template for worker: {}", e,
+                );
+                return JsonError::new(InternalError, None, id).into()
+            }
+        };
+
         // Create worker
-        let worker = Worker::new(job_sub.clone(), ka_send, ka_task.clone());
+        let worker = Worker {
+            addr,
+            _agent: agent.clone(),
+            job_sub: job_sub.clone(),
+            _ka_task: ka_task.clone(),
+            ka_send,
+            _job_task: job_task.clone(),
+            submit_send,
+            mining_job: mining_job.clone(),
+        };
 
-        // Insert into connections map
-        self.workers.write().await.insert(uuid, worker);
+        // Insert the worker into connections map
+        self.workers.write().await.insert(worker_uuid, worker);
 
-        // Spawn background task
+        // Spawn keepalive background task
         ka_task.start(
-            Self::keepalive_task(self.workers.clone(), uuid, ka_recv),
-            move |_| async move { debug!("keepalive_task for {} exited", uuid) },
+            Self::keepalive_task(self.workers.clone(), worker_uuid, ka_recv),
+            move |_| async move { debug!("keepalive_task for {} exited", worker_uuid) },
             Error::DetachedTaskStopped,
             self.executor.clone(),
         );
 
-        info!("Added worker {} ({})", login, uuid);
-
-        // Get block template for mining
-        let gbt_params: JsonValue = HashMap::from([
-            ("wallet_address".to_string(), self.monerod.wallet_address.clone().into()),
-            ("reserve_size".to_string(), (0_f64).into()),
-        ])
-        .into();
-
-        let block_template = match self.monero_get_block_template(OsRng.gen(), gbt_params).await {
-            JsonResult::Response(resp) => {
-                resp.result.get::<HashMap<String, JsonValue>>().unwrap().clone()
-            }
-            _ => {
-                error!("[STRATUM] Failed getting block template from monerod");
-                return JsonError::new(ErrorCode::InternalError, None, id).into()
-            }
-        };
+        // Spawn job notification background task
+        job_task.start(
+            Self::job_task(
+                self.workers.clone(),
+                worker_uuid,
+                self.monerod_rpc.clone(),
+                submit_recv,
+            ),
+            move |_| async move { debug!("job_task for {} exited", worker_uuid) },
+            Error::DetachedTaskStopped,
+            self.executor.clone(),
+        );
 
-        // Send the job to the worker
-        let mut workers = self.workers.write().await;
-        let worker = workers.get_mut(&uuid).unwrap();
-        let blob = block_template["blockhashing_blob"].get::<String>().unwrap();
-        let target = block_template["wide_difficulty"].get::<String>().unwrap();
-        if let Err(e) = worker.send_job(blob.clone(), target.clone()).await {
-            error!("[STRATUM] Failed sending job to {}: {}", uuid, e);
-            return JsonError::new(ErrorCode::InternalError, None, id).into()
-        }
+        info!("[STRATUM] Added worker {}", worker_uuid);
+
+        // Finally, we return the job notification subscriber, along with the
+        // initial job response as noted in:
+        // https://github.com/xmrig/xmrig-proxy/blob/master/doc/STRATUM.md#example-success-reply
+        let blob = hex::encode(mining_job.block.serialize_hashable());
+        let response = JsonResponse::new(
+            HashMap::from([
+                ("status".to_string(), "OK".to_string().into()),
+                ("id".to_string(), worker_uuid.to_string().into()),
+                (
+                    "extensions".to_string(),
+                    vec!["algo".to_string().into(), "keepalive".to_string().into()].into(),
+                ),
+                (
+                    "job".to_string(),
+                    HashMap::from([
+                        ("blob".to_string(), blob.into()),
+                        ("job_id".to_string(), mining_job.job_id.to_string().into()),
+                        ("target".to_string(), mining_job.target.to_string().into()),
+                        ("height".to_string(), mining_job.height.into()),
+                        ("seed_hash".to_string(), mining_job.seed_hash.to_string().into()),
+                        ("algo".to_string(), RANDOMX_ALGO.to_string().into()),
+                    ])
+                    .into(),
+                ),
+            ])
+            .into(),
+            id,
+        );
 
-        job_sub.into()
+        JsonResult::SubscriberWithReply(job_sub, response)
     }
 
+    /// Stratum submit method
+    ///
+    /// The miner submits the request after a share was found.
     pub async fn stratum_submit(&self, id: u16, params: JsonValue) -> JsonResult {
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 1 || !params[0].is_object() {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
-        }
-
-        let params = params[0].get::<HashMap<String, JsonValue>>().unwrap();
+        let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
+            return JsonError::new(InvalidParams, None, id).into()
+        };
 
         if !params.contains_key("id") ||
             !params.contains_key("job_id") ||
             !params.contains_key("nonce") ||
             !params.contains_key("result")
         {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+            return JsonError::new(InvalidParams, None, id).into()
         }
 
-        let Some(_uuid) = params["id"].get::<String>() else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        // Validate all the parameters
+        let Some(worker_uuid) = params["id"].get::<String>() else {
+            return JsonError::new(InvalidParams, Some("Invalid \"id\" field".to_string()), id)
+                .into()
+        };
+
+        let Ok(worker_uuid) = Uuid::try_from(worker_uuid.as_str()) else {
+            return JsonError::new(InvalidParams, Some("Invalid \"id\" field".to_string()), id)
+                .into()
+        };
+
+        let Some(job_id) = params["job_id"].get::<String>() else {
+            return JsonError::new(InvalidParams, Some("Invalid \"job_id\" field".to_string()), id)
+                .into()
+        };
+
+        let Ok(job_id) = blake3::Hash::from_str(job_id) else {
+            return JsonError::new(InvalidParams, Some("Invalid \"job_id\" field".to_string()), id)
+                .into()
         };
 
-        let Some(_job_id) = params["job_id"].get::<String>() else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        let Some(nonce) = params["nonce"].get::<String>() else {
+            return JsonError::new(InvalidParams, Some("Invalid \"nonce\" field".to_string()), id)
+                .into()
         };
 
-        let Some(_nonce) = params["nonce"].get::<String>() else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        let Ok(nonce) = u32::from_str_radix(nonce, 16) else {
+            return JsonError::new(InvalidParams, Some("Invalid \"nonce\" field".to_string()), id)
+                .into()
         };
 
         let Some(_result) = params["result"].get::<String>() else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+            return JsonError::new(InvalidParams, Some("Invalid \"result\" field".to_string()), id)
+                .into()
         };
 
-        todo!()
-    }
+        // Get the worker reference and confirm this is submitted for the current job
+        let workers_ptr = self.workers.read().await;
+        let Some(worker) = workers_ptr.get(&worker_uuid) else {
+            return JsonError::new(InvalidParams, Some("Unknown worker UUID".to_string()), id).into()
+        };
 
-    /// Non standard but widely supported protocol extension. Miner sends `keepalived`
-    /// to prevent connection timeout.
-    pub async fn stratum_keepalived(&self, id: u16, params: JsonValue) -> JsonResult {
-        let params = params.get::<Vec<JsonValue>>().unwrap();
-        if params.len() != 1 || !params[0].is_object() {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        if worker.mining_job.job_id != job_id {
+            return JsonError::new(InvalidParams, Some("Job ID mismatch".to_string()), id).into()
         }
 
-        let params = params[0].get::<HashMap<String, JsonValue>>().unwrap();
+        // Get the block template from the worker reference and update the nonce
+        let mut block_template = worker.mining_job.block.clone();
+        block_template.header.nonce = nonce;
+
+        // Submit the block to monerod
+        let block = monero::consensus::serialize_hex(&block_template);
+        let params: JsonValue = vec![block.into()].into();
+        let req = JsonRequest::new("submit_block", params);
+
+        let resp = match monerod_request(&self.monerod_rpc, req).await {
+            Ok(v) => v,
+            Err(e) => {
+                error!(
+                    target: "stratum::submit",
+                    "[STRATUM] Failed submitting block to monerod: {}", e,
+                );
+                return JsonError::new(
+                    InternalError,
+                    Some("Failed submitting block".to_string()),
+                    id,
+                )
+                .into()
+            }
+        };
 
-        if !params.contains_key("id") {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        // Ping the job_task to reiterate.
+        // We don't release the lock after this, so that we can hopefully first
+        // return the result of the `submit` call, and then unlock job_task for
+        // the new notification.
+        let _ = worker.submit_send.send(()).await;
+
+        match JsonResult::try_from_value(&resp) {
+            Ok(JsonResult::Response(r)) => {
+                info!(
+                    target: "stratum::submit",
+                    "[STRATUM] Sucessfully submitted block to monerod: {:?}", r,
+                );
+
+                let result = HashMap::from([("status".to_string(), "OK".to_string().into())]);
+                JsonResponse::new(result.into(), id).into()
+            }
+            Ok(JsonResult::Error(e)) => {
+                error!(
+                    target: "stratum::submit",
+                    "[STRATUM] Failed submitting block to monerod: {:?}", e,
+                );
+                JsonError::new(ServerError(e.error.code), Some(e.error.message), id).into()
+            }
+            Ok(x) => {
+                error!(
+                    target: "stratum::submit",
+                    "[STRATUM] Unexpected RPC reply from monerod: {:?}", x,
+                );
+                JsonError::new(InternalError, Some("Failed submitting block".to_string()), id)
+                    .into()
+            }
+            Err(e) => {
+                error!(
+                    target: "stratum::submit",
+                    "[STRATUM] Unexpected RPC reply from monerod: {}", e,
+                );
+                JsonError::new(InternalError, Some("Failed submitting block".to_string()), id)
+                    .into()
+            }
         }
+    }
 
-        let Some(uuid) = params["id"].get::<String>() else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+    /// Nonstandard, but widely supported protocol extension.
+    /// The miner sends `keepalived` to prevent connection timeout.
+    /// `darkfi-mmproxy` makes having keepalived mandatory.
+    pub async fn stratum_keepalived(&self, id: u16, params: JsonValue) -> JsonResult {
+        let Some(params) = params.get::<HashMap<String, JsonValue>>() else {
+            return JsonError::new(InvalidParams, None, id).into()
         };
 
-        let Ok(uuid) = Uuid::try_from(uuid.as_str()) else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        if !params.contains_key("id") {
+            return JsonError::new(InvalidParams, Some("Missing \"id\" field".to_string()), id)
+                .into()
         };
 
-        // Ping the keepalive task
-        let workers = self.workers.read().await;
-        let Some(worker) = workers.get(&uuid) else {
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        let Some(worker_uuid) = params["id"].get::<String>() else {
+            return JsonError::new(InvalidParams, Some("Invalid \"id\" field".to_string()), id)
+                .into()
+        };
+
+        let Ok(worker_uuid) = Uuid::try_from(worker_uuid.as_str()) else {
+            return JsonError::new(InvalidParams, Some("Invalid \"id\" field".to_string()), id)
+                .into()
         };
 
+        // Get the worker reference
+        let workers_ptr = self.workers.read().await;
+        let Some(worker) = workers_ptr.get(&worker_uuid) else {
+            return JsonError::new(InvalidParams, Some("Invalid \"id\" field".to_string()), id)
+                .into()
+        };
+
+        // Ping the keepalive task
         if let Err(e) = worker.ka_send.send(()).await {
-            error!("stratum_keepalived: keepalive task ping error: {}", e);
-            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+            error!(
+                target: "stratum::keepalived",
+                "[STRATUM] Keepalive task ping error for {}: {}", worker_uuid, e,
+            );
+            return JsonError::new(InternalError, None, id).into()
         }
 
         JsonResponse::new(
             JsonValue::Object(HashMap::from([(
                 "status".to_string(),
-                JsonValue::String("KEEPALIVED".to_string()),
+                "KEEPALIVED".to_string().into(),
             )])),
             id,
         )

+ 1 - 3
src/rpc/server.rs

@@ -121,9 +121,7 @@ pub async fn accept(
             JsonResult::SubscriberWithReply(subscriber, reply) => {
                 // Write the response
                 debug!(target: "rpc::server", "{} <-- {}", addr, reply.stringify()?);
-                if let Err(e) = write_to_stream(&mut stream, &reply.into()).await {
-                    return Err(e)
-                }
+                write_to_stream(&mut stream, &reply.into()).await?;
 
                 // Start the subscriber loop
                 let subscription = subscriber.sub.subscribe().await;