From 9f966ed712e7704931f6cc689f9f370c1e480492 Mon Sep 17 00:00:00 2001 From: Priceless-P Date: Thu, 24 Apr 2025 10:05:58 +0100 Subject: [PATCH 1/3] support solo mode --- Cargo.lock | 91 ++++-- Cargo.toml | 28 +- README.md | 38 ++- config.toml | 9 + src/api/mod.rs | 4 +- src/api/routes.rs | 47 +-- src/jd_client/mining_downstream/mod.rs | 21 +- src/jd_client/mod.rs | 379 ++++++++++++++----------- src/jd_client/utils.rs | 99 +++++++ src/main.rs | 239 ++++++++-------- src/shared/utils.rs | 73 +++++ 11 files changed, 651 insertions(+), 377 deletions(-) create mode 100644 config.toml create mode 100644 src/jd_client/utils.rs diff --git a/Cargo.lock b/Cargo.lock index a822e96a..d9b48b6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,12 +102,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -190,9 +190,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -238,9 +238,9 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.5" +version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" +checksum = "ad8929a18b8e33ea6b3c09297b687baaa71fb1b97353243a3f1029fad5c59c5b" dependencies = [ "base58ck", "bech32", @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bitvec" @@ -388,9 +388,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.21" +version = "1.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" dependencies = [ "shlex", ] @@ -438,9 +438,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.37" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" dependencies = [ "clap_builder", "clap_derive", @@ -448,9 +448,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" dependencies = [ "anstream", "anstyle", @@ -612,7 +612,7 @@ dependencies = [ [[package]] name = "demand-cli" -version = "0.1.5" +version = "0.1.6" dependencies = [ "async-recursion", "axum", @@ -641,6 +641,7 @@ dependencies = [ "sysinfo", "tokio", "tokio-util", + "toml", "tracing", "tracing-subscriber", ] @@ -997,9 +998,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" dependencies = [ "bytes", "futures-util", @@ -1250,6 +1251,15 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2611b99ab098a31bdc8be48b4f1a285ca0ced28bd5b4f23e45efa8c63b09efa5" +dependencies = [ + "once_cell", +] + [[package]] name = "opaque-debug" version = "0.3.1" @@ -1264,9 +1274,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parity-scale-codec" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ "arrayvec", "bitvec", @@ -1280,9 +1290,9 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1653,6 +1663,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1884,11 +1903,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -1897,10 +1931,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "tower" version = "0.5.2" @@ -2214,9 +2257,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index c7c679d3..c52e4289 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "demand-cli" -version = "0.1.5" +version = "0.1.6" edition = "2021" [dependencies] @@ -18,29 +18,12 @@ tracing-subscriber = { version = "*", features = ["env-filter"]} tokio = {version="^1.36.0",features = ["full","tracing", "macros","rt-multi-thread"]} key-utils = "1.0.0" pid = { version = "4.0.0"} -clap = {version = "4.5.31", features = ["derive"]} -axum = {version = "0.8.1"} +clap = { version = "4.5.31", features = ["derive"]} +axum = { version = "0.8.1"} serde = { version = "1.0.219", features = ["derive"] } sysinfo = {version = "0.33.1"} primitive-types = { version = "0.13.1" } -#roles_logic_sv2 = "1.2.1" -#sv1_api = "1.0.1" -#demand-sv2-connection = "0.0.3" -#framing_sv2 = "^2.0.0" -#binary_sv2 = "1.1.0" -#demand-share-accounting-ext = "0.0.10" - -#noise_sv2 = "1.1.0" -#codec_sv2 = { version = "1.2.1", features = ["noise_sv2","with_buffer_pool"]} - -#demand-share-accounting-ext = {path = "../demand-share-accounting-ext"} -#demand-sv2-connection = { path = "../demand-sv2-connection"} -#roles_logic_sv2 = { path = "../stratum/protocols/v2/roles-logic-sv2"} -#framing_sv2 = { path = "../stratum/protocols/v2/framing-sv2"} -#binary_sv2 = { path = "../stratum/protocols/v2/binary-sv2/binary-sv2"} -#noise_sv2 = {path ="../stratum/protocols/v2/noise-sv2"} -#codec_sv2 = {features = ["noise_sv2","with_buffer_pool"], path = "../stratum/protocols/v2/codec-sv2" } -#sv1_api = {path = "../stratum/protocols/v1" } +toml = { version = "0.8" } demand-share-accounting-ext = { git = "https://github.com/demand-open-source/share-accounting-ext"} demand-sv2-connection = {git = "https://github.com/demand-open-source/demand-sv2-connection"} @@ -52,9 +35,6 @@ codec_sv2 = { git = "https://github.com/demand-open-source/stratum", branch="Imp sv1_api = { git = "https://github.com/demand-open-source/stratum", branch = "ImproveCoinbase",subdirectory = "protocols/v1"} - - - [dev-dependencies] rand = "0.8.5" sha2 = "0.10.8" diff --git a/README.md b/README.md index 2a746c94..40e17833 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,17 @@ [![Forks](https://img.shields.io/github/forks/demand-open-source/demand-cli?style=social)](https://github.com/demand-open-source/demand-cli) ![Release](https://img.shields.io/github/v/release/demand-open-source/demand-cli) -**Demand CLI** is a proxy that let miners to connect to and mine with [Demand Pool](https://dmnd.work). It serves two primary purposes: +**Demand CLI** is a proxy that let miners to connect to and mine with [Demand Pool](https://dmnd.work). It serves three primary purposes: 1. Translation: Enables miners using StratumV1 to connect to the Demand Pool without requiring firmware updates. Sv1 messages gets translated to Sv2. 2. Job Declaration (JD): Allows miners to declare custom jobs to the pool using StratumV2. + 3. Solo Mining: Skip pool entirely and mine solo ## Features - **Stratum V2 Support**: Uses the secure and efficient Stratum V2 protocol for communication with the pool. - **Job Declaration**: Enables miners to propose custom block templates to the pool. This helps make mining more decentralized by allowing miners to pick the transactions they want to include in a block. - **Stratum V1 Translation**: Acts as a bridge, allowing StratumV1 miners to connect to the Demand Pool without firmware updates. +- **Solo Mining Mode**: Allows miners to mine independently without a pool. - **Flexible Configuration**: Provides options for customization. This allows users to optimize the tool for their specific mining environment. - **Monitoring API**: Provides HTTP endpoints to monitor proxy health, pool connectivity, miner performace, and system resource usage in real-time. @@ -64,10 +66,20 @@ Before running the CLI, set up the necessary environment variables. ``` Note: if `TP_ADDRESS` id not set, job declaration is disabled, and the proxy uses templates provided by the pool. +### Config File (for Solo Mining) +Create config.toml with your coinbase outputs. Supported script types include P2PKH, P2SH, P2WPKH, P2WSH, and P2TR +Example config file: +```toml + coinbase_outputs = [ + { output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" } +] +withhold = false +``` + ## Running the CLI Depending on whether you built from source or downloaded a binary, the command to run the proxy is slightly different. There are also different options you can use. -Below are two example setups to get you started. +Below are 3 example setups to get you started. #### Example 1: Built from Source with Job Declaration @@ -75,7 +87,7 @@ This example assumes you’ve built `demand-cli` from source and want to enable ```bash export TOKEN=abc123xyz - export TP_ADDRESS=192.168.1.100:8442 + export TP_ADDRESS=127.0.0.1:8442 ./target/release/demand-cli -d 100T --loglevel debug --nc on ``` @@ -92,6 +104,15 @@ Set Environment Variable: Point your Stratum V1 miners to :32767. +#### Example 3: Solo Mining (No Pool, No TOKEN) +```bash + export TP_ADDRESS=127.0.0.1:8442 +./demand-cli-linux-x64 --solo -c path/to/config.toml --loglevel debug +``` +This runs in solo mode, using your config.toml to define the coinbase output and other parameters. if `config.toml` is the directory as your binary `-c` is not required. Ensure Bitcoin node is running. + +Point your Stratum V1 miners to :32767 + ### Options - **`--test`**: Connects to test endpoint @@ -99,6 +120,8 @@ Point your Stratum V1 miners to :32767. This helps the pool adjust to your hashrate. - **`--loglevel`**: Logging verbosity (`info`, `debug`, `error`, `warn`). Default is `info`. - **`--nc`**: Noise connection logging verbosity (`info`, `debug`, `error`, `warn`). Default is `off`. + - **`--solo`**: Enable solo mining mode. Requires `TP_ADDRESS` and a config file. + - **`-c, --config`**: Path to your solo mining config.toml. Default is config.toml ## Monitoring API: @@ -156,6 +179,15 @@ Point your Stratum V1 miners to :32767. } } ``` + - **200 OK** (Solo mining): + ```json + { + "success": true, + "data": { + "pool": "Not connected to pool. Mining solo :)", + } + } + ``` - **400 NOT_FOUND** (Not connected to Pool): ```json { "success": true, "data": { "address": null, "latency": null } } diff --git a/config.toml b/config.toml new file mode 100644 index 00000000..47dbb814 --- /dev/null +++ b/config.toml @@ -0,0 +1,9 @@ +coinbase_outputs = [ + # { output_script_type = "P2PK", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, + { output_script_type = "P2PKH", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, + #{ output_script_type = "P2SH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, + #{ output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, + # { output_script_type = "P2WPKH", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, + #{ output_script_type = "P2TR", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, +] +withhold = false diff --git a/src/api/mod.rs b/src/api/mod.rs index 3d8d51ed..d2895900 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -9,11 +9,11 @@ use stats::StatsSender; // Holds shared state (like the router) that so that it can be accessed in all routes. #[derive(Clone)] pub struct AppState { - router: Router, + router: Option, stats_sender: StatsSender, } -pub(crate) async fn start(router: Router, stats_sender: StatsSender) { +pub(crate) async fn start(router: Option, stats_sender: StatsSender) { let state = AppState { router, stats_sender, diff --git a/src/api/routes.rs b/src/api/routes.rs index 250e06f6..76963092 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -66,26 +66,37 @@ impl Api { // Retrieves the current pool information pub async fn get_pool_info(State(state): State) -> impl IntoResponse { - let current_pool_address = state.router.current_pool; - let latency = *state.router.latency_rx.borrow(); + if let Some(router) = state.router { + let current_pool_address = router.current_pool; + let latency = *router.latency_rx.borrow(); - match (current_pool_address, latency) { - (Some(address), Some(latency)) => { - let response_data = serde_json::json!({ - "address": address.to_string(), - "latency": latency.as_millis().to_string() - }); - ( - StatusCode::OK, - Json(APIResponse::success(Some(response_data))), - ) + match (current_pool_address, latency) { + (Some(address), Some(latency)) => { + let response_data = serde_json::json!({ + "address": address.to_string(), + "latency": latency.as_millis().to_string() + }); + ( + StatusCode::OK, + Json(APIResponse::success(Some(response_data))), + ) + } + (_, _) => ( + StatusCode::NOT_FOUND, + Json(APIResponse::error(Some( + "Pool information unavailable".to_string(), + ))), + ), } - (_, _) => ( - StatusCode::NOT_FOUND, - Json(APIResponse::error(Some( - "Pool information unavailable".to_string(), - ))), - ), + } else { + let response_data = serde_json::json!({ + "pool": "Not connected to pool. Mining solo :)".to_string(), + + }); + ( + StatusCode::OK, + Json(APIResponse::success(Some(response_data))), + ) } } diff --git a/src/jd_client/mining_downstream/mod.rs b/src/jd_client/mining_downstream/mod.rs index 0fb67457..3ec4e578 100644 --- a/src/jd_client/mining_downstream/mod.rs +++ b/src/jd_client/mining_downstream/mod.rs @@ -374,17 +374,15 @@ impl DownstreamMiningNode { let to_send = to_send.into_values(); for message in to_send { let message = if let Mining::NewExtendedMiningJob(job) = message { - let jd = self_mutex + if let Some(jd) = self_mutex .safe_lock(|s| s.jd.clone()) .map_err(|_| JdClientError::JobDeclaratorMutexCorrupted)? - .ok_or({ - // Propagate error. The caller will restart proxy - JdClientError::JdMissing - })?; - jd.safe_lock(|jd| jd.coinbase_tx_prefix = job.coinbase_tx_prefix.clone()) - .map_err(|_| JdClientError::JobDeclaratorMutexCorrupted)?; - jd.safe_lock(|jd| jd.coinbase_tx_suffix = job.coinbase_tx_suffix.clone()) - .map_err(|_| JdClientError::JobDeclaratorMutexCorrupted)?; + { + jd.safe_lock(|jd| jd.coinbase_tx_prefix = job.coinbase_tx_prefix.clone()) + .map_err(|_| JdClientError::JobDeclaratorMutexCorrupted)?; + jd.safe_lock(|jd| jd.coinbase_tx_suffix = job.coinbase_tx_suffix.clone()) + .map_err(|_| JdClientError::JobDeclaratorMutexCorrupted)?; + } Mining::NewExtendedMiningJob(job) } else { @@ -497,7 +495,7 @@ impl // The channel factory is created here so that we are sure that if we have a channel // open we have a factory and if we have a factory we have a channel open. This allowto // not change the semantic of Status beween solo and pooled modes - let extranonce_len = 32; + let extranonce_len = 28; // 28 + 4 (additional_coinbase_script_data) = 32 (max allowed) let range_0 = std::ops::Range { start: 0, end: 0 }; let range_1 = std::ops::Range { start: 0, end: 16 }; let range_2 = std::ops::Range { @@ -555,8 +553,7 @@ impl .ok_or(Error::NoUpstreamsConnected)?, )) } else { - error!("Solo Mining currently Unsupported"); - std::process::exit(1) + Ok(SendTo::None(None)) } } diff --git a/src/jd_client/mod.rs b/src/jd_client/mod.rs index 42a4c842..2cbbb85c 100644 --- a/src/jd_client/mod.rs +++ b/src/jd_client/mod.rs @@ -6,14 +6,18 @@ pub mod mining_downstream; pub mod mining_upstream; mod task_manager; mod template_receiver; +mod utils; +use bitcoin::TxOut; use job_declarator::JobDeclarator; use key_utils::Secp256k1PublicKey; use mining_downstream::DownstreamMiningNode; -use std::sync::atomic::AtomicBool; +use roles_logic_sv2::template_distribution_sv2::SubmitSolution; +use std::{path::PathBuf, sync::atomic::AtomicBool}; use task_manager::TaskManager; use template_receiver::TemplateRx; use tracing::{error, info}; +use utils::{get_coinbase_output, parse_tp_address, retry_connection, Config}; /// Is used by the template receiver and the downstream. When a NewTemplate is received the context /// that is running the template receiver set this value to false and then the message is sent to @@ -40,7 +44,7 @@ pub static IS_NEW_TEMPLATE_HANDLED: AtomicBool = AtomicBool::new(true); pub static IS_CUSTOM_JOB_SET: AtomicBool = AtomicBool::new(true); -use crate::proxy_state::{DownstreamType, ProxyState, TpState}; +use crate::proxy_state::{DownstreamType, ProxyState}; use roles_logic_sv2::{parsers::Mining, utils::Mutex}; use std::{ net::{IpAddr, SocketAddr}, @@ -64,204 +68,241 @@ pub async fn start( initialize_jd(receiver, sender, up_receiver, up_sender).await } +/// Initializes JD in pool mining mode. async fn initialize_jd( receiver: tokio::sync::mpsc::Receiver>, sender: tokio::sync::mpsc::Sender>, up_receiver: tokio::sync::mpsc::Receiver>, up_sender: tokio::sync::mpsc::Sender>, ) -> Option { - let task_manager = TaskManager::initialize(); - let abortable = match task_manager.safe_lock(|t| t.get_aborter()) { - Ok(abortable) => abortable?, - Err(e) => { - error!("Jdc task manager mutex corrupt: {e}"); - return None; - } - }; - let test_only_do_not_send_solution_to_tp = false; - - // When Downstream receive a share that meets bitcoin target it transformit in a - // SubmitSolution and send it to the TemplateReceiver - let (send_solution, recv_solution) = tokio::sync::mpsc::channel(10); + let (jd, recv_solution) = JdSetUp::new() + .await? + .start_upstream_and_jd(up_sender, up_receiver) + .await? + .start_downstream(receiver, sender, None, vec![]) + .await?; + jd.start_template_receiver(recv_solution, vec![], false) + .await +} - // Instantiate a new `Upstream` (SV2 Pool) - let upstream = match mining_upstream::Upstream::new(crate::MIN_EXTRANONCE_SIZE, up_sender).await - { - Ok(upstream) => upstream, - Err(e) => { - error!("Failed to instantiate new Upstream: {e}"); - drop(abortable); - return None; - } - }; +/// Initializes JD in solo mining mode. +pub async fn initialize_jd_as_solo_miner( + receiver: tokio::sync::mpsc::Receiver>, + sender: tokio::sync::mpsc::Sender>, + config: PathBuf, +) -> Option { + info!("Starting solo mining..."); + let config_str = std::fs::read_to_string(config).expect("Failed to read config file"); + let config: Config = toml::from_str(&config_str).expect("Failed to parse config file"); + let miner_coinbase_output = get_coinbase_output(&config).unwrap(); - // Initialize JD part - let tp_address = match crate::TP_ADDRESS.safe_lock(|tp| tp.clone()) { - Ok(tp_address) => tp_address - .expect("Unreachable code, jdc is not instantiated when TP_ADDRESS not present"), - Err(e) => { - error!("TP_ADDRESS mutex corrupted: {e}"); - drop(abortable); - return None; - } - }; + let (jd_solo, recv_solution) = JdSetUp::new() + .await? + .start_downstream( + receiver, + sender, + config.withhold(), + miner_coinbase_output.clone(), + ) + .await?; - let mut parts = tp_address.split(':'); - let ip_tp = parts.next().expect("The passed value for TP address is not valid. Terminating.... TP_ADDRESS should be in this format `127.0.0.1:8442`").to_string(); - let port_tp = parts.next().expect("The passed value for TP address is not valid. Terminating.... TP_ADDRESS should be in this format `127.0.0.1:8442`").parse::().expect("This operation should not fail because a valid port_tp should always be converted to U16"); + jd_solo + .start_template_receiver(recv_solution, miner_coinbase_output, false) + .await +} - let auth_pub_k: Secp256k1PublicKey = crate::AUTH_PUB_KEY.parse().expect("Invalid public key"); - let address = crate::POOL_ADDRESS - .to_socket_addrs() - .expect("The passed Pool Address is not valid") - .next()?; +struct JdSetUp { + task_manager: Arc>, + abortable: AbortOnDrop, + downstream: Option>>, + upstream: Option>>, + jd: Option>>, +} - let (jd, jd_abortable) = - match JobDeclarator::new(address, auth_pub_k.into_bytes(), upstream.clone(), true).await { - Ok(c) => c, +impl JdSetUp { + async fn new() -> Option { + let task_manager = TaskManager::initialize(); + let abortable = match task_manager.safe_lock(|t| t.get_aborter()) { + Ok(abortable) => abortable?, Err(e) => { - error!("Failed to intialize Jd: {e}"); - drop(abortable); + error!("Jdc task manager mutex corrupt: {e}"); return None; } }; + Some(Self { + task_manager, + abortable, + downstream: None, + upstream: None, + jd: None, + }) + } - if TaskManager::add_job_declarator_task(task_manager.clone(), jd_abortable) - .await - .is_err() - { - error!( - "Task manager failed while trying to add job declarator task{}", - error::Error::TaskManagerFailed - ); - drop(abortable); - return None; - }; + async fn start_upstream_and_jd( + mut self, + up_sender: tokio::sync::mpsc::Sender>, + up_receiver: tokio::sync::mpsc::Receiver>, + ) -> Option { + let upstream = + match mining_upstream::Upstream::new(crate::MIN_EXTRANONCE_SIZE, up_sender).await { + Ok(upstream) => upstream, + Err(e) => { + error!("Failed to instantiate new Upstream: {e}"); + drop(self.abortable); + return None; + } + }; - let donwstream = Arc::new(Mutex::new(DownstreamMiningNode::new( - sender, - Some(upstream.clone()), - send_solution, - false, - vec![], - Some(jd.clone()), - ))); - let downstream_abortable = match DownstreamMiningNode::start(donwstream.clone(), receiver).await - { - Ok(abortable) => abortable, - Err(e) => { - error!("Can not start downstream mining node: {e}"); - ProxyState::update_downstream_state(DownstreamType::JdClientMiningDownstream); + let upstream_abortable = + match mining_upstream::Upstream::parse_incoming(upstream.clone(), up_receiver).await { + Ok(abortable) => abortable, + Err(e) => { + error!("Failed to get jdc upstream abortable: {e}"); + drop(self.abortable); + return None; + } + }; + + if TaskManager::add_mining_upstream_task(self.task_manager.clone(), upstream_abortable) + .await + .is_err() + { + error!("Failed to add mining upstream task"); + drop(self.abortable); return None; - } - }; - if TaskManager::add_mining_downtream_task(task_manager.clone(), downstream_abortable) - .await - .is_err() - { - error!( - "Task manager failed while trying to add mining downstream task{}", - error::Error::TaskManagerFailed - ); - drop(abortable); - return None; - }; - if upstream - .safe_lock(|u| u.downstream = Some(donwstream.clone())) - .is_err() - { - error!("Upstream mutex failed"); - drop(abortable); // drop all tasks initailzed upto this point - return None; - }; + }; + let address = crate::POOL_ADDRESS + .to_socket_addrs() + .expect("The passed Pool Address is not valid") + .next()?; + let auth_pub_k: Secp256k1PublicKey = + crate::AUTH_PUB_KEY.parse().expect("Invalid public key"); - // Start receiving messages from the SV2 Upstream role - let upstream_abortable = - match mining_upstream::Upstream::parse_incoming(upstream.clone(), up_receiver).await { - Ok(abortable) => abortable, + let (jd, jd_abortable) = match JobDeclarator::new( + address, + auth_pub_k.into_bytes(), + upstream.clone(), + true, + ) + .await + { + Ok(c) => c, Err(e) => { - error!("Failed to get jdc upstream abortable: {e}"); - drop(abortable); // drop all tasks initailzed upto this point + error!("Failed to initialize Jd: {e}"); + drop(self.abortable); return None; } }; - if TaskManager::add_mining_upstream_task(task_manager.clone(), upstream_abortable) - .await - .is_err() - { - error!( - "Task manager failed while trying to add mining upstream task{}", - error::Error::TaskManagerFailed - ); - drop(abortable); // drop all tasks initailzed upto this point - return None; - }; - let ip = IpAddr::from_str(ip_tp.as_str()) - .expect("Infallable Operation: Failed tp can always be converted into IpAddr"); - let tp_abortable = match TemplateRx::connect( - SocketAddr::new(ip, port_tp), - recv_solution, - Some(jd.clone()), - donwstream.clone(), - vec![], - None, - test_only_do_not_send_solution_to_tp, - ) - .await - { - Ok(abortable) => abortable, - Err(_) => { - info!("Dropping jd abortable"); - eprintln!("TP is unreachable, the proxy is in not in JD mode"); - drop(abortable); - // Temporaily set TP_ADDRESS to None so that proxy can restart without it. - // that means we will start mining without jd - if crate::TP_ADDRESS.safe_lock(|tp| *tp = None).is_err() { - error!("TP_ADDRESS mutex corrupt"); - return None; - }; - tokio::spawn(retry_connection(tp_address)); + + if TaskManager::add_job_declarator_task(self.task_manager.clone(), jd_abortable) + .await + .is_err() + { + error!("Failed to add job declarator task"); + drop(self.abortable); return None; - } - }; + }; + self.upstream = Some(upstream); + self.jd = Some(jd); + Some(self) + } - if TaskManager::add_template_receiver_task(task_manager, tp_abortable) - .await - .is_err() - { - error!( - "Task manager failed while trying to add template receiver task{}", - error::Error::TaskManagerFailed - ); - drop(abortable); - return None; - }; - Some(abortable) -} + async fn start_downstream( + mut self, + receiver: tokio::sync::mpsc::Receiver>, + sender: tokio::sync::mpsc::Sender>, + withhold: Option, + miner_coinbase_output: Vec, + ) -> Option<(Self, tokio::sync::mpsc::Receiver>)> { + let (send_solution, recv_solution) = tokio::sync::mpsc::channel(10); + + let downstream = Arc::new(Mutex::new(DownstreamMiningNode::new( + sender, + self.upstream.clone(), + send_solution, + withhold.unwrap_or(false), + miner_coinbase_output, + self.jd.clone(), + ))); + + let downstream_abortable = + match DownstreamMiningNode::start(downstream.clone(), receiver).await { + Ok(abortable) => abortable, + Err(e) => { + error!("Cannot start downstream mining node: {e}"); + ProxyState::update_downstream_state(DownstreamType::JdClientMiningDownstream); + drop(self.abortable); + return None; + } + }; -// Used when tp is down or connection was unsuccessful to retry connection. -async fn retry_connection(address: String) { - let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); - loop { - info!("TP Retrying connection...."); - interval.tick().await; - if tokio::net::TcpStream::connect(address.clone()) + if TaskManager::add_mining_downtream_task(self.task_manager.clone(), downstream_abortable) .await - .is_ok() + .is_err() { - info!("Successfully reconnected to TP: Restarting Proxy..."); - if crate::TP_ADDRESS - .safe_lock(|tp| *tp = Some(address)) + error!("Failed to add mining downstream task"); + drop(self.abortable); + return None; + }; + + if let Some(upstream) = &self.upstream { + if upstream + .safe_lock(|u| u.downstream = Some(downstream.clone())) .is_err() { - error!("TP_ADDRESS Mutex failed"); - std::process::exit(1); + error!("Upstream mutex failed"); + drop(self.abortable); + return None; }; - // This force the proxy to restart. If we use Up the proxy just ignore it. - // So updating it to Down and setting the TP_ADDRESS to Some(address) will make the - // proxy restart with TP, the the TpState will be set to Up. - ProxyState::update_tp_state(TpState::Down); - break; + } + + self.downstream = Some(downstream); + Some((self, recv_solution)) + } + + async fn start_template_receiver( + self, + recv_solution: tokio::sync::mpsc::Receiver>, + miner_coinbase_tx: Vec, + test_only_do_not_send_solution_to_tp: bool, + ) -> Option { + let (ip_tp, port_tp, tp_address) = parse_tp_address()?; + + let ip = IpAddr::from_str(ip_tp.as_str()).expect("Invalid IP address"); + + match TemplateRx::connect( + SocketAddr::new(ip, port_tp), + recv_solution, + self.jd, + self.downstream.expect("Downstream must be initialized"), + miner_coinbase_tx, + None, + test_only_do_not_send_solution_to_tp, + ) + .await + { + Ok(tp_abortable) => { + if TaskManager::add_template_receiver_task(self.task_manager, tp_abortable) + .await + .is_err() + { + error!("Failed to add template receiver task"); + drop(self.abortable); + return None; + } + Some(self.abortable) + } + Err(_) => { + info!("Dropping jd abortable"); + eprintln!("TP is unreachable, the proxy is in not in JD mode"); + drop(self.abortable); + if crate::TP_ADDRESS.safe_lock(|tp| *tp = None).is_err() { + error!("TP_ADDRESS mutex corrupt"); + return None; + }; + tokio::spawn(retry_connection(tp_address)); + None + } } } } diff --git a/src/jd_client/utils.rs b/src/jd_client/utils.rs new file mode 100644 index 00000000..479a508f --- /dev/null +++ b/src/jd_client/utils.rs @@ -0,0 +1,99 @@ +use bitcoin::{Amount, TxOut}; +use roles_logic_sv2::{utils::CoinbaseOutput, Error}; +use serde::Deserialize; +use tracing::{error, info}; + +use crate::proxy_state::{ProxyState, TpState}; + +// Used when tp is down or connection was unsuccessful to retry connection. +pub async fn retry_connection(address: String) { + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); + loop { + info!("TP Retrying connection...."); + interval.tick().await; + if tokio::net::TcpStream::connect(address.clone()) + .await + .is_ok() + { + info!("Successfully reconnected to TP: Restarting Proxy..."); + if crate::TP_ADDRESS + .safe_lock(|tp| *tp = Some(address)) + .is_err() + { + error!("TP_ADDRESS Mutex failed"); + std::process::exit(1); + }; + // This force the proxy to restart. If we use Up the proxy just ignore it. + // So updating it to Down and setting the TP_ADDRESS to Some(address) will make the + // proxy restart with TP, the the TpState will be set to Up. + ProxyState::update_tp_state(TpState::Down); + break; + } + } +} + +#[derive(Debug, Deserialize, Clone)] +struct Output { + output_script_type: String, + output_script_value: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Config { + coinbase_outputs: Vec, + withhold: Option, +} +impl Config { + pub fn withhold(self) -> Option { + self.withhold + } +} + +impl TryFrom<&Output> for CoinbaseOutput { + type Error = roles_logic_sv2::errors::Error; + + fn try_from(pool_output: &Output) -> Result { + match pool_output.output_script_type.as_str() { + "TEST" | "P2PK" | "P2PKH" | "P2WPKH" | "P2SH" | "P2WSH" | "P2TR" => { + Ok(CoinbaseOutput { + output_script_type: pool_output.clone().output_script_type, + output_script_value: pool_output.clone().output_script_value, + }) + } + _ => Err(roles_logic_sv2::Error::UnknownOutputScriptType), + } + } +} + +pub fn get_coinbase_output(config: &Config) -> Result, Error> { + let mut result = Vec::new(); + for coinbase_output_pool in &config.coinbase_outputs { + let coinbase_output: CoinbaseOutput = coinbase_output_pool.try_into()?; + let output_script = coinbase_output.try_into()?; + result.push(TxOut { + value: Amount::from_sat(0), + script_pubkey: output_script, + }); + } + match result.is_empty() { + true => Err(Error::EmptyCoinbaseOutputs), + _ => Ok(result), + } +} + +pub fn parse_tp_address() -> Option<(String, u16, String)> { + let tp_address = match crate::TP_ADDRESS.safe_lock(|tp| tp.clone()) { + Ok(tp_address) => tp_address + .expect("Unreachable code, jdc is not instantiated when TP_ADDRESS not present"), + Err(e) => { + error!("TP_ADDRESS mutex corrupted: {e}"); + return None; + } + }; + + let mut parts = tp_address.split(':'); + let ip_tp = parts.next().expect("The passed value for TP address is not valid. Terminating.... TP_ADDRESS should be in this format `127.0.0.1:8442`").to_string(); + let port_tp = parts.next().expect("The passed value for TP address is not valid. Terminating.... TP_ADDRESS should be in this format `127.0.0.1:8442`").parse::().expect("This operation should not fail because a valid port_tp should always be converted to U16"); + + Some((ip_tp, port_tp, tp_address)) +} diff --git a/src/main.rs b/src/main.rs index c45ebcd9..6eca2e26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use clap::Parser; #[cfg(not(target_os = "windows"))] use jemallocator::Jemalloc; use router::Router; +use shared::utils::HashUnit; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[cfg(not(target_os = "windows"))] #[global_allocator] @@ -41,17 +42,13 @@ const TEST_AUTH_PUB_KEY: &str = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7 const DEFAULT_LISTEN_ADDRESS: &str = "0.0.0.0:32767"; lazy_static! { + static ref ARGS: Args = Args::parse(); static ref SV1_DOWN_LISTEN_ADDR: String = std::env::var("SV1_DOWN_LISTEN_ADDR").unwrap_or(DEFAULT_LISTEN_ADDRESS.to_string()); static ref TP_ADDRESS: roles_logic_sv2::utils::Mutex> = roles_logic_sv2::utils::Mutex::new(std::env::var("TP_ADDRESS").ok()); - static ref EXPECTED_SV1_HASHPOWER: f32 = Args::parse() - .downstream_hashrate - .unwrap_or(DEFAULT_SV1_HASHPOWER); -} - -lazy_static! { - static ref ARGS: Args = Args::parse(); + static ref EXPECTED_SV1_HASHPOWER: f32 = + ARGS.downstream_hashrate.unwrap_or(DEFAULT_SV1_HASHPOWER); pub static ref POOL_ADDRESS: &'static str = if ARGS.test { TEST_POOL_ADDRESS } else { @@ -68,7 +65,7 @@ struct Args { // Use test enpoint if test flag is provided #[clap(long)] test: bool, - #[clap(long ="d", short ='d', value_parser = parse_hashrate)] + #[clap(long ="d", short ='d', value_parser = shared::utils::parse_hashrate)] downstream_hashrate: Option, #[clap(long = "loglevel", short = 'l', default_value = "info")] loglevel: String, @@ -78,6 +75,10 @@ struct Args { delay: u64, #[clap(long = "interval", short = 'i', default_value = "120000")] adjustment_interval: u64, + #[clap(long = "solo", short = 's')] + solo: bool, + #[arg(short = 'c', long, default_value = "config.toml")] + config: std::path::PathBuf, } #[tokio::main] @@ -116,41 +117,46 @@ async fn main() { log_level, noise_connection_log_level ))) .init(); - std::env::var("TOKEN").expect("Missing TOKEN environment variable"); - let hashpower = *EXPECTED_SV1_HASHPOWER; - - if args.downstream_hashrate.is_some() { - info!( - "Using downstream hashrate: {}h/s", - HashUnit::format_value(hashpower) - ); + if args.solo { + initialize_proxy_solo(args.config).await; } else { - warn!( - "No downstream hashrate provided, using default value: {}h/s", - HashUnit::format_value(hashpower) - ); - } - if args.test { - info!("Connecting to test endpoint..."); - } + std::env::var("TOKEN").expect("Missing TOKEN environment variable"); + + let hashpower = *EXPECTED_SV1_HASHPOWER; + + if args.downstream_hashrate.is_some() { + info!( + "Using downstream hashrate: {}h/s", + HashUnit::format_value(hashpower) + ); + } else { + warn!( + "No downstream hashrate provided, using default value: {}h/s", + HashUnit::format_value(hashpower) + ); + } + if args.test { + info!("Connecting to test endpoint..."); + } - let auth_pub_k: Secp256k1PublicKey = AUTH_PUB_KEY.parse().expect("Invalid public key"); - let address = POOL_ADDRESS - .to_socket_addrs() - .expect("Invalid pool address") - .next() - .expect("Invalid pool address"); - - // We will add upstream addresses here - let pool_addresses = vec![address]; - - let mut router = router::Router::new(pool_addresses, auth_pub_k, None, None); - let epsilon = Duration::from_millis(10); - let best_upstream = router.select_pool_connect().await; - initialize_proxy(&mut router, best_upstream, epsilon).await; - info!("exiting"); - tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; + let auth_pub_k: Secp256k1PublicKey = AUTH_PUB_KEY.parse().expect("Invalid public key"); + let address = POOL_ADDRESS + .to_socket_addrs() + .expect("Invalid pool address") + .next() + .expect("Invalid pool address"); + + // We will add upstream addresses here + let pool_addresses = vec![address]; + + let mut router = router::Router::new(pool_addresses, auth_pub_k, None, None); + let epsilon = Duration::from_millis(10); + let best_upstream = router.select_pool_connect().await; + initialize_proxy(&mut router, best_upstream, epsilon).await; + info!("exiting"); + tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; + } } async fn initialize_proxy( @@ -266,8 +272,8 @@ async fn initialize_proxy( if let Some(jdc_handle) = jdc_abortable { abort_handles.push((jdc_handle, "jdc".to_string())); } - let server_handle = tokio::spawn(api::start(router.clone(), stats_sender)); - match monitor(router, abort_handles, epsilon, server_handle).await { + let server_handle = tokio::spawn(api::start(Some(router.clone()), stats_sender)); + match monitor(Some(router), abort_handles, Some(epsilon), server_handle).await { Reconnect::NewUpstream(new_pool_addr) => { ProxyState::update_proxy_state_up(); pool_addr = Some(new_pool_addr); @@ -282,10 +288,63 @@ async fn initialize_proxy( } } +async fn initialize_proxy_solo(config: std::path::PathBuf) { + loop { + let (downs_sv1_tx, downs_sv1_rx) = channel(10); + let sv1_ingress_abortable = ingress::sv1_ingress::start_listen_for_downstream(downs_sv1_tx); + let stats_sender = api::stats::StatsSender::new(); + let (translator_up_tx, mut translator_up_rx) = channel(10); + let translator_abortable = + match translator::start(downs_sv1_rx, translator_up_tx, stats_sender.clone()).await { + Ok(abortable) => abortable, + Err(e) => { + error!("Impossible to initialize translator: {e}"); + ProxyState::update_translator_state(TranslatorState::Down); + tokio::time::sleep(Duration::from_secs(10)).await; + continue; + } + }; + + let (jdc_to_translator_sender, jdc_from_translator_receiver, _) = translator_up_rx + .recv() + .await + .expect("Translator failed before initialization"); + + let jdc_abortable = match jd_client::initialize_jd_as_solo_miner( + jdc_from_translator_receiver, + jdc_to_translator_sender, + config.clone(), + ) + .await + { + Some(abortable) => abortable, + None => { + error!("Impossible to initialize JDC"); + ProxyState::update_tp_state(TpState::Down); + tokio::time::sleep(Duration::from_secs(10)).await; + continue; + } + }; + + let abort_handles = vec![ + (sv1_ingress_abortable, "sv1_ingress".to_string()), + (translator_abortable, "translator".to_string()), + (jdc_abortable, "jdc".to_string()), + ]; + + let server_handle = tokio::spawn(api::start(None, stats_sender)); + if monitor(None, abort_handles, None, server_handle).await == Reconnect::NoUpstream { + ProxyState::update_proxy_state_up(); + tokio::time::sleep(Duration::from_secs(10)).await; + continue; + } + } +} + async fn monitor( - router: &mut Router, + mut router: Option<&mut Router>, abort_handles: Vec<(AbortOnDrop, std::string::String)>, - epsilon: Duration, + epsilon: Option, server_handle: tokio::task::JoinHandle<()>, ) -> Reconnect { let mut should_check_upstreams_latency = 0; @@ -293,14 +352,16 @@ async fn monitor( // Check if a better upstream exist every 100 seconds if should_check_upstreams_latency == 10 * 100 { should_check_upstreams_latency = 0; - if let Some(new_upstream) = router.monitor_upstream(epsilon).await { - info!("Faster upstream detected. Reinitializing proxy..."); - drop(abort_handles); - server_handle.abort(); // abort server - - // Needs a little to time to drop - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - return Reconnect::NewUpstream(new_upstream); + if let (Some(router), Some(epsilon)) = (router.as_mut(), epsilon) { + if let Some(new_upstream) = router.monitor_upstream(epsilon).await { + info!("Faster upstream detected. Reinitializing proxy..."); + drop(abort_handles); + server_handle.abort(); // abort server + + // Needs a little to time to drop + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + return Reconnect::NewUpstream(new_upstream); + } } } @@ -346,80 +407,8 @@ async fn monitor( } } -/// Parses a hashrate string (e.g., "10T", "2.5P", "500E") into an f32 value in h/s. -fn parse_hashrate(hashrate_str: &str) -> Result { - let hashrate_str = hashrate_str.trim(); - if hashrate_str.is_empty() { - return Err("Hashrate cannot be empty. Expected format: '' (e.g., '10T', '2.5P', '5E'".to_string()); - } - - let unit = hashrate_str.chars().last().unwrap_or(' ').to_string(); - let num = &hashrate_str[..hashrate_str.len().saturating_sub(1)]; - - let num: f32 = num.parse().map_err(|_| { - format!( - "Invalid number '{}'. Expected format: '' (e.g., '10T', '2.5P', '5E')", - num - ) - })?; - - let multiplier = HashUnit::from_str(&unit) - .map(|unit| unit.multiplier()) - .ok_or_else(|| format!( - "Invalid unit '{}'. Expected 'T' (Terahash), 'P' (Petahash), or 'E' (Exahash). Example: '10T', '2.5P', '5E'", - unit - ))?; - - let hashrate = num * multiplier; - - if hashrate.is_infinite() || hashrate.is_nan() { - return Err("Hashrate too large or invalid".to_string()); - } - - Ok(hashrate) -} - +#[derive(PartialEq)] pub enum Reconnect { NewUpstream(std::net::SocketAddr), // Reconnecting with a new upstream NoUpstream, // Reconnecting without upstream } - -enum HashUnit { - Tera, - Peta, - Exa, -} - -impl HashUnit { - /// Returns the multiplier for each unit in h/s - fn multiplier(&self) -> f32 { - match self { - HashUnit::Tera => 1e12, - HashUnit::Peta => 1e15, - HashUnit::Exa => 1e18, - } - } - - // Converts a unit string (e.g., "T") to a HashUnit variant - fn from_str(s: &str) -> Option { - match s.to_uppercase().as_str() { - "T" => Some(HashUnit::Tera), - "P" => Some(HashUnit::Peta), - "E" => Some(HashUnit::Exa), - _ => None, - } - } - - /// Formats a hashrate value (f32) into a string with the appropriate unit - fn format_value(hashrate: f32) -> String { - if hashrate >= 1e18 { - format!("{:.2}E", hashrate / 1e18) - } else if hashrate >= 1e15 { - format!("{:.2}P", hashrate / 1e15) - } else if hashrate >= 1e12 { - format!("{:.2}T", hashrate / 1e12) - } else { - format!("{:.2}", hashrate) - } - } -} diff --git a/src/shared/utils.rs b/src/shared/utils.rs index 476cbbf0..c18e210f 100644 --- a/src/shared/utils.rs +++ b/src/shared/utils.rs @@ -58,3 +58,76 @@ impl Display for UserId { write!(f, "{}", self.0) } } + +/// Parses a hashrate string (e.g., "10T", "2.5P", "500E") into an f32 value in h/s. +pub fn parse_hashrate(hashrate_str: &str) -> Result { + let hashrate_str = hashrate_str.trim(); + if hashrate_str.is_empty() { + return Err("Hashrate cannot be empty. Expected format: '' (e.g., '10T', '2.5P', '5E'".to_string()); + } + + let unit = hashrate_str.chars().last().unwrap_or(' ').to_string(); + let num = &hashrate_str[..hashrate_str.len().saturating_sub(1)]; + + let num: f32 = num.parse().map_err(|_| { + format!( + "Invalid number '{}'. Expected format: '' (e.g., '10T', '2.5P', '5E')", + num + ) + })?; + + let multiplier = HashUnit::from_str(&unit) + .map(|unit| unit.multiplier()) + .ok_or_else(|| format!( + "Invalid unit '{}'. Expected 'T' (Terahash), 'P' (Petahash), or 'E' (Exahash). Example: '10T', '2.5P', '5E'", + unit + ))?; + + let hashrate = num * multiplier; + + if hashrate.is_infinite() || hashrate.is_nan() { + return Err("Hashrate too large or invalid".to_string()); + } + + Ok(hashrate) +} + +pub enum HashUnit { + Tera, + Peta, + Exa, +} + +impl HashUnit { + /// Returns the multiplier for each unit in h/s + fn multiplier(&self) -> f32 { + match self { + HashUnit::Tera => 1e12, + HashUnit::Peta => 1e15, + HashUnit::Exa => 1e18, + } + } + + // Converts a unit string (e.g., "T") to a HashUnit variant + fn from_str(s: &str) -> Option { + match s.to_uppercase().as_str() { + "T" => Some(HashUnit::Tera), + "P" => Some(HashUnit::Peta), + "E" => Some(HashUnit::Exa), + _ => None, + } + } + + /// Formats a hashrate value (f32) into a string with the appropriate unit + pub fn format_value(hashrate: f32) -> String { + if hashrate >= 1e18 { + format!("{:.2}E", hashrate / 1e18) + } else if hashrate >= 1e15 { + format!("{:.2}P", hashrate / 1e15) + } else if hashrate >= 1e12 { + format!("{:.2}T", hashrate / 1e12) + } else { + format!("{:.2}", hashrate) + } + } +} From f01a9f9cc1e7c80320b6f9d4b156b235812348af Mon Sep 17 00:00:00 2001 From: Priceless-P Date: Mon, 2 Jun 2025 16:31:27 +0100 Subject: [PATCH 2/3] update --- README.md | 28 ++++++++------ config.toml | 12 ++---- src/jd_client/error.rs | 7 ++++ src/jd_client/utils.rs | 86 +++++++++++++++++++++++------------------- src/main.rs | 6 +++ 5 files changed, 80 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 40e17833..242fc2bd 100644 --- a/README.md +++ b/README.md @@ -67,15 +67,19 @@ Before running the CLI, set up the necessary environment variables. Note: if `TP_ADDRESS` id not set, job declaration is disabled, and the proxy uses templates provided by the pool. ### Config File (for Solo Mining) -Create config.toml with your coinbase outputs. Supported script types include P2PKH, P2SH, P2WPKH, P2WSH, and P2TR -Example config file: -```toml - coinbase_outputs = [ - { output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" } -] -withhold = false -``` +Create config.toml with payout address, network and withhold. + Example config file: + ```toml + payout_address = "bc1qn2ckg6c2329e3g7w8sqlwqrg7f0hgcrv2fp2hv" + withhold = false + network = "bitcoin" + ``` + - payout_address: Bitcoin address for coinbase output (where your reward will be sent) + - withhold: specifies whether or not to withhold mining rewards (default: false). + - network : Bitcoin network (bitcoin, testnet, regtest, signet; default: bitcoin) + These can also be passed as CLI args. Supported script types include P2PKH, P2SH, P2WPKH, P2WSH, and P2TR + ## Running the CLI Depending on whether you built from source or downloaded a binary, the command to run the proxy is slightly different. There are also different options you can use. @@ -107,9 +111,9 @@ Point your Stratum V1 miners to :32767. #### Example 3: Solo Mining (No Pool, No TOKEN) ```bash export TP_ADDRESS=127.0.0.1:8442 -./demand-cli-linux-x64 --solo -c path/to/config.toml --loglevel debug +./demand-cli-linux-x64 --solo --payout_address bc1qn2ckg6c2329e3g7w8sqlwqrg7f0hgcrv2fp2hv --network bitcoin ``` -This runs in solo mode, using your config.toml to define the coinbase output and other parameters. if `config.toml` is the directory as your binary `-c` is not required. Ensure Bitcoin node is running. +This runs in solo mode, you can specify the payout_address and network in `toml` file as pass its path with `-c path/to/config.toml`. if `config.toml` is the directory as your binary `-c` is not required. Ensure Bitcoin node is running. Point your Stratum V1 miners to :32767 @@ -126,9 +130,9 @@ Point your Stratum V1 miners to :32767 ## Monitoring API: - The proxy exposes REST API enspoints to monitor its health, pool connectivity, connected mining devices performance and system resource usage. All endpoints are served on `http://0.0.0.0:3001` and return JSON responses in the format: + The proxy exposes REST API enspoints to monitor its health, pool connectivity, connected mining devices performance and system resource usage. All endpoints are served on `http://0.0.0.0:3001` by default and return JSON responses in the format: - ```json + ``` { "success": boolean, "message": string | null, diff --git a/config.toml b/config.toml index 47dbb814..8603b951 100644 --- a/config.toml +++ b/config.toml @@ -1,9 +1,3 @@ -coinbase_outputs = [ - # { output_script_type = "P2PK", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - { output_script_type = "P2PKH", output_script_value = "0372c47307e5b75ce365daf835f226d246c5a7a92fe24395018d5552123354f086" }, - #{ output_script_type = "P2SH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - #{ output_script_type = "P2WSH", output_script_value = "00142ef89234bc95136eb9e6fee9d32722ebd8c1f0ab" }, - # { output_script_type = "P2WPKH", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, - #{ output_script_type = "P2TR", output_script_value = "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075" }, -] -withhold = false +payout_address="bc1qn2ckg6c2329e3g7w8sqlwqrg7f0hgcrv2fp2hv" +network="bitcoin" +withhold=false diff --git a/src/jd_client/error.rs b/src/jd_client/error.rs index 011fda3b..92feab29 100644 --- a/src/jd_client/error.rs +++ b/src/jd_client/error.rs @@ -47,6 +47,10 @@ pub enum Error { TemplateRxMutexCorrupted, TemplateRxTaskManagerFailed, TpMissing, + // Payout address & network spepcif errors (solo mining) + InvalidAddress(String), + NetworkMismatch(String), + InvalidNetwork(String), } impl fmt::Display for Error { @@ -94,6 +98,9 @@ impl fmt::Display for Error { write!(f, "Failed to add Task in TemplateRx TaskManager") } TpMissing => write!(f, "Failed to connect to TP"), + InvalidAddress(ref e) => write!(f, "Invalid Bitcoin address: {}", e), + NetworkMismatch(ref e) => write!(f, "Network mismatch: {}", e), + InvalidNetwork(ref e) => write!(f, "Invalid network specified: {}", e), } } } diff --git a/src/jd_client/utils.rs b/src/jd_client/utils.rs index 479a508f..6e76728b 100644 --- a/src/jd_client/utils.rs +++ b/src/jd_client/utils.rs @@ -1,5 +1,7 @@ -use bitcoin::{Amount, TxOut}; -use roles_logic_sv2::{utils::CoinbaseOutput, Error}; +use std::str::FromStr; + +use super::error::Error; +use bitcoin::{Address, Amount, Network, ScriptBuf, TxOut}; use serde::Deserialize; use tracing::{error, info}; @@ -32,53 +34,61 @@ pub async fn retry_connection(address: String) { } } -#[derive(Debug, Deserialize, Clone)] -struct Output { - output_script_type: String, - output_script_value: String, -} - #[derive(Debug, Deserialize, Clone)] pub struct Config { - coinbase_outputs: Vec, + payout_address: String, withhold: Option, + network: String, } impl Config { pub fn withhold(self) -> Option { - self.withhold + crate::ARGS.withhold.or(self.withhold) } } -impl TryFrom<&Output> for CoinbaseOutput { - type Error = roles_logic_sv2::errors::Error; - - fn try_from(pool_output: &Output) -> Result { - match pool_output.output_script_type.as_str() { - "TEST" | "P2PK" | "P2PKH" | "P2WPKH" | "P2SH" | "P2WSH" | "P2TR" => { - Ok(CoinbaseOutput { - output_script_type: pool_output.clone().output_script_type, - output_script_value: pool_output.clone().output_script_value, - }) - } - _ => Err(roles_logic_sv2::Error::UnknownOutputScriptType), +pub fn get_coinbase_output(config: &Config) -> Result, Error> { + let network = match crate::ARGS + .network + .as_ref() + .unwrap_or(&config.network) + .to_lowercase() + .as_str() + { + "bitcoin" => Network::Bitcoin, + "testnet" => Network::Testnet, + "regtest" => Network::Regtest, + "signet" => Network::Signet, + _ => { + return Err(Error::InvalidNetwork(format!( + "Unknown network: {}", + config.network + ))) } - } -} + }; -pub fn get_coinbase_output(config: &Config) -> Result, Error> { - let mut result = Vec::new(); - for coinbase_output_pool in &config.coinbase_outputs { - let coinbase_output: CoinbaseOutput = coinbase_output_pool.try_into()?; - let output_script = coinbase_output.try_into()?; - result.push(TxOut { - value: Amount::from_sat(0), - script_pubkey: output_script, - }); - } - match result.is_empty() { - true => Err(Error::EmptyCoinbaseOutputs), - _ => Ok(result), - } + let payout_address = crate::ARGS + .payout_address + .as_ref() + .unwrap_or(&config.payout_address); + + let addr = + Address::from_str(payout_address).map_err(|e| Error::InvalidAddress(e.to_string()))?; + + let address = addr + .require_network(network) + .map_err(|e| Error::NetworkMismatch(e.to_string()))?; + + // Get the script pubkey from the address + let script_pubkey: ScriptBuf = address.script_pubkey(); + + info!("Detected script type: {:?}", address.address_type()); + println!("Script type: {:?}", script_pubkey); + let tx_out = TxOut { + value: Amount::from_sat(0), + script_pubkey, + }; + + Ok(vec![tx_out]) } pub fn parse_tp_address() -> Option<(String, u16, String)> { diff --git a/src/main.rs b/src/main.rs index 6eca2e26..5ac4d285 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,12 @@ struct Args { solo: bool, #[arg(short = 'c', long, default_value = "config.toml")] config: std::path::PathBuf, + #[clap(long)] + payout_address: Option, + #[clap(long)] + withhold: Option, + #[clap(long, help = "Bitcoin network (bitcoin, testnet, regtest, signet)")] + network: Option, } #[tokio::main] From 70a78932c20311fd2ed87e1c342e4f5ca0b85dfe Mon Sep 17 00:00:00 2001 From: Priceless-P Date: Mon, 2 Jun 2025 16:34:03 +0100 Subject: [PATCH 3/3] update build title --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9394cf11..72555e43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build demand-cli binaries for Linux and macOS +name: Build demand-cli binaries on: push: