Browse Source

script/research/minerd: miner daemon skeleton

aggstam 2 years ago
parent
commit
4fffd4ac2c

+ 3 - 0
script/research/minerd/.gitignore

@@ -0,0 +1,3 @@
+/target
+Cargo.lock
+rustfmt.toml

+ 41 - 0
script/research/minerd/Cargo.toml

@@ -0,0 +1,41 @@
+[package]
+name = "minerd"
+version = "0.4.1"
+homepage = "https://dark.fi"
+description = "Darkfi mining daemon"
+authors = ["Dyne.org foundation <foundation@dyne.org>"]
+repository = "https://github.com/darkrenaissance/darkfi"
+license = "AGPL-3.0-only"
+edition = "2021"
+
+[workspace]
+
+[dependencies]
+# Darkfi
+darkfi = {path = "../../../", features = ["async-daemonize", "validator"]}
+darkfi-sdk = {path = "../../../src/sdk"}
+darkfi-serial = {path = "../../../src/serial", features = ["async"]}
+
+# Misc
+log = "0.4.20"
+num-bigint = "0.4.4"
+
+# JSON-RPC
+tinyjson = "2.5.1"
+url = "2.5.0"
+
+# Daemon
+easy-parallel = "3.3.1"
+signal-hook-async-std = "0.2.2"
+signal-hook = "0.3.17"
+simplelog = "0.12.1"
+smol = "1.3.0"
+
+# Argument parsing
+serde = {version = "1.0.195", features = ["derive"]}
+structopt = "0.3.26"
+structopt-toml = "0.5.1"
+
+[patch.crates-io]
+halo2_proofs = {git="https://github.com/parazyd/halo2", branch="v4"}
+halo2_gadgets = {git="https://github.com/parazyd/halo2", branch="v4"}

+ 10 - 0
script/research/minerd/minerd.toml

@@ -0,0 +1,10 @@
+## minerd configuration file
+##
+## Please make sure you go through all the settings so you can configure
+## your daemon properly.
+##
+## The default values are left commented. They can be overridden either by
+## uncommenting, or by using the command-line.
+
+# JSON-RPC listen URL
+#rpc_listen = "tcp://127.0.0.1:28467"

+ 54 - 0
script/research/minerd/src/error.rs

@@ -0,0 +1,54 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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 darkfi::rpc::jsonrpc::{ErrorCode::ServerError, JsonError, JsonResult};
+
+/// Custom RPC errors available for minerd.
+/// Please sort them sensefully.
+pub enum RpcError {
+    // Parsing errors
+    TargetParseError = -32101,
+    BlockParseError = -32102,
+
+    // Miner errors
+    MiningFailed = -32201,
+    HashingFailed = -32202,
+}
+
+fn to_tuple(e: RpcError) -> (i32, String) {
+    let msg = match e {
+        // Parsing errors
+        RpcError::TargetParseError => "Target parse error",
+        RpcError::BlockParseError => "Block parse error",
+        // Miner errors
+        RpcError::MiningFailed => "Mining block failed",
+        RpcError::HashingFailed => "Hashing block failed",
+    };
+
+    (e as i32, msg.to_string())
+}
+
+pub fn server_error(e: RpcError, id: u16, msg: Option<&str>) -> JsonResult {
+    let (code, default_msg) = to_tuple(e);
+
+    if let Some(message) = msg {
+        return JsonError::new(ServerError(code), Some(message.to_string()), id).into()
+    }
+
+    JsonError::new(ServerError(code), Some(default_msg), id).into()
+}

+ 117 - 0
script/research/minerd/src/main.rs

@@ -0,0 +1,117 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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::HashSet, sync::Arc};
+
+use log::{error, info};
+use smol::{channel::Receiver, lock::Mutex, stream::StreamExt, Executor};
+use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
+use url::Url;
+
+use darkfi::{
+    async_daemonize, cli_desc,
+    rpc::server::{listen_and_serve, RequestHandler},
+    system::{StoppableTask, StoppableTaskPtr},
+    Error, Result,
+};
+
+const CONFIG_FILE: &str = "minerd.toml";
+const CONFIG_FILE_CONTENTS: &str = include_str!("../minerd.toml");
+
+/// Daemon error codes
+mod error;
+
+/// JSON-RPC server methods
+mod rpc;
+
+#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
+#[serde(default)]
+#[structopt(name = "minerd", about = cli_desc!())]
+struct Args {
+    #[structopt(short, long)]
+    /// Configuration file to use
+    config: Option<String>,
+
+    #[structopt(short, long, default_value = "tcp://127.0.0.1:28467")]
+    /// JSON-RPC listen URL
+    rpc_listen: Url,
+
+    #[structopt(short, long, default_value = "4")]
+    /// PoW miner number of threads to use
+    threads: usize,
+
+    #[structopt(short, long)]
+    /// Set log file to ouput into
+    log: Option<String>,
+
+    #[structopt(short, parse(from_occurrences))]
+    /// Increase verbosity (-vvv supported)
+    verbose: u8,
+}
+
+/// Daemon structure
+pub struct Minerd {
+    /// PoW miner number of threads to use
+    threads: usize,
+    // Receiver to stop miner threads
+    stop_signal: Receiver<()>,
+    /// JSON-RPC connection tracker
+    rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
+}
+
+impl Minerd {
+    pub fn new(threads: usize, stop_signal: Receiver<()>) -> Self {
+        Self { threads, stop_signal, rpc_connections: Mutex::new(HashSet::new()) }
+    }
+}
+
+async_daemonize!(realmain);
+async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
+    info!(target: "minerd", "Starting DarkFi Mining Daemon...");
+    let (sender, recvr) = smol::channel::bounded(1);
+    let minerd = Arc::new(Minerd::new(args.threads, recvr));
+
+    info!(target: "minerd", "Starting JSON-RPC server on {}", args.rpc_listen);
+    let minerd_ = Arc::clone(&minerd);
+    let rpc_task = StoppableTask::new();
+    rpc_task.clone().start(
+        listen_and_serve(args.rpc_listen, minerd.clone(), None, ex.clone()),
+        |res| async move {
+            match res {
+                Ok(()) | Err(Error::RpcServerStopped) => minerd_.stop_connections().await,
+                Err(e) => error!(target: "minerd", "Failed stopping JSON-RPC server: {}", e),
+            }
+        },
+        Error::RpcServerStopped,
+        ex.clone(),
+    );
+
+    // Signal handling for graceful termination.
+    let (signals_handler, signals_task) = SignalHandler::new(ex)?;
+    signals_handler.wait_termination(signals_task).await?;
+    info!(target: "minerd", "Caught termination signal, cleaning up and exiting");
+
+    info!(target: "minerd", "Stopping miner threads...");
+    sender.send(()).await?;
+
+    info!(target: "minerd", "Stopping JSON-RPC server...");
+    rpc_task.stop().await;
+
+    info!(target: "minerd", "Shut down successfully");
+    Ok(())
+}

+ 106 - 0
script/research/minerd/src/rpc.rs

@@ -0,0 +1,106 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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::HashSet;
+
+use log::{debug, error, info};
+use num_bigint::BigUint;
+use smol::lock::MutexGuard;
+
+use darkfi::{
+    blockchain::BlockInfo,
+    rpc::{
+        jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult},
+        server::RequestHandler,
+        util::JsonValue,
+    },
+    system::StoppableTaskPtr,
+    util::encoding::base64,
+    validator::pow::mine_block,
+};
+use darkfi_sdk::num_traits::Num;
+use darkfi_serial::{async_trait, deserialize, serialize};
+
+use crate::{
+    error::{server_error, RpcError},
+    Minerd,
+};
+
+#[async_trait]
+impl RequestHandler for Minerd {
+    async fn handle_request(&self, req: JsonRequest) -> JsonResult {
+        debug!(target: "minerd::rpc", "--> {}", req.stringify().unwrap());
+
+        match req.method.as_str() {
+            "ping" => self.pong(req.id, req.params).await,
+            "mine" => self.mine(req.id, req.params).await,
+            _ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
+        }
+    }
+
+    async fn connections_mut(&self) -> MutexGuard<'_, HashSet<StoppableTaskPtr>> {
+        self.rpc_connections.lock().await
+    }
+}
+
+impl Minerd {
+    // RPCAPI:
+    // Mine provided block for requested mine target, and return the corresponding nonce value.
+    //
+    // --> {"jsonrpc": "2.0", "method": "mine", "params": ["target", "block"], "id": 42}
+    // --> {"jsonrpc": "2.0", "result": "nonce", "id": 42}
+    async fn mine(&self, id: u16, params: JsonValue) -> JsonResult {
+        // Verify parameters
+        if !params.is_array() {
+            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        }
+        let params = params.get::<Vec<JsonValue>>().unwrap();
+        if params.len() != 2 || !params[0].is_string() || !params[1].is_string() {
+            return JsonError::new(ErrorCode::InvalidParams, None, id).into()
+        }
+
+        // Parse parameters
+        let Ok(target) = BigUint::from_str_radix(params[0].get::<String>().unwrap(), 10) else {
+            error!(target: "minerd::rpc", "Failed to parse target");
+            return server_error(RpcError::TargetParseError, id, None)
+        };
+        let Some(block_bytes) = base64::decode(params[1].get::<String>().unwrap()) else {
+            error!(target: "minerd::rpc", "Failed to parse block bytes");
+            return server_error(RpcError::BlockParseError, id, None)
+        };
+        let Ok(mut block) = deserialize::<BlockInfo>(&block_bytes) else {
+            error!(target: "minerd::rpc", "Failed to parse block");
+            return server_error(RpcError::BlockParseError, id, None)
+        };
+
+        // Mine provided block
+        let Ok(block_hash) = block.hash() else {
+            error!(target: "minerd::rpc", "Failed to hash block");
+            return server_error(RpcError::HashingFailed, id, None)
+        };
+        info!(target: "minerd::rpc", "Mining block {} for target: {}", block_hash, target);
+        if let Err(e) = mine_block(&target, &mut block, self.threads, &self.stop_signal) {
+            error!(target: "minerd::rpc", "Failed mining block {} with error: {}", block_hash, e);
+            return server_error(RpcError::MiningFailed, id, None)
+        }
+
+        // Return block nonce
+        let nonce = base64::encode(&serialize(&block.header.nonce));
+        JsonResponse::new(JsonValue::String(nonce), id).into()
+    }
+}