minerd/
lib.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{collections::HashSet, sync::Arc};
20
21use log::{error, info};
22use smol::{
23    channel::{Receiver, Sender},
24    lock::Mutex,
25};
26
27use darkfi::{
28    rpc::{
29        server::{listen_and_serve, RequestHandler},
30        settings::RpcSettings,
31    },
32    system::{ExecutorPtr, StoppableTask, StoppableTaskPtr},
33    Error, Result,
34};
35
36/// Daemon error codes
37mod error;
38
39/// JSON-RPC server methods
40mod rpc;
41
42/// Atomic pointer to the DarkFi mining node
43pub type MinerNodePtr = Arc<MinerNode>;
44
45/// Structure representing a DarkFi mining node
46pub struct MinerNode {
47    /// PoW miner number of threads to use
48    threads: usize,
49    /// Stop mining at this height
50    stop_at_height: u32,
51    /// Sender to stop miner threads
52    sender: Sender<()>,
53    /// Receiver to stop miner threads
54    stop_signal: Receiver<()>,
55    /// JSON-RPC connection tracker
56    rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
57}
58
59impl MinerNode {
60    pub fn new(
61        threads: usize,
62        stop_at_height: u32,
63        sender: Sender<()>,
64        stop_signal: Receiver<()>,
65    ) -> MinerNodePtr {
66        Arc::new(Self {
67            threads,
68            stop_at_height,
69            sender,
70            stop_signal,
71            rpc_connections: Mutex::new(HashSet::new()),
72        })
73    }
74}
75
76/// Atomic pointer to the DarkFi mining daemon
77pub type MinerdPtr = Arc<Minerd>;
78
79/// Structure representing a DarkFi mining daemon
80pub struct Minerd {
81    /// Miner node instance conducting the mining operations
82    node: MinerNodePtr,
83    /// JSON-RPC background task
84    rpc_task: StoppableTaskPtr,
85}
86
87impl Minerd {
88    /// Initialize a DarkFi mining daemon.
89    ///
90    /// Corresponding communication channels are setup to generate a new `MinerNode`,
91    /// and a new task is generated to handle the JSON-RPC API.
92    pub fn init(threads: usize, stop_at_height: u32) -> MinerdPtr {
93        info!(target: "minerd::Minerd::init", "Initializing a new mining daemon...");
94
95        // Initialize the smol channels to send signal between the threads
96        let (sender, stop_signal) = smol::channel::bounded(1);
97
98        // Generate the node
99        let node = MinerNode::new(threads, stop_at_height, sender, stop_signal);
100
101        // Generate the JSON-RPC task
102        let rpc_task = StoppableTask::new();
103
104        info!(target: "minerd::Minerd::init", "Mining daemon initialized successfully!");
105
106        Arc::new(Self { node, rpc_task })
107    }
108
109    /// Start the DarkFi mining daemon in the given executor, using the provided JSON-RPC listen url.
110    pub fn start(&self, executor: &ExecutorPtr, rpc_settings: &RpcSettings) {
111        info!(target: "minerd::Minerd::start", "Starting mining daemon...");
112
113        // Start the JSON-RPC task
114        let node_ = self.node.clone();
115        self.rpc_task.clone().start(
116            listen_and_serve(rpc_settings.clone(), self.node.clone(), None, executor.clone()),
117            |res| async move {
118                match res {
119                    Ok(()) | Err(Error::RpcServerStopped) => node_.stop_connections().await,
120                    Err(e) => error!(target: "minerd::Minerd::start", "Failed starting JSON-RPC server: {}", e),
121                }
122            },
123            Error::RpcServerStopped,
124            executor.clone(),
125        );
126
127        info!(target: "minerd::Minerd::start", "Mining daemon started successfully!");
128    }
129
130    /// Stop the DarkFi mining daemon.
131    pub async fn stop(&self) -> Result<()> {
132        info!(target: "minerd::Minerd::stop", "Terminating mining daemon...");
133
134        // Stop the mining node
135        info!(target: "minerd::Minerd::stop", "Stopping miner threads...");
136        self.node.sender.send(()).await?;
137
138        // Stop the JSON-RPC task
139        info!(target: "minerd::Minerd::stop", "Stopping JSON-RPC server...");
140        self.rpc_task.stop().await;
141
142        // Consume channel item so its empty again
143        if self.node.stop_signal.is_full() {
144            self.node.stop_signal.recv().await?;
145        }
146
147        info!(target: "minerd::Minerd::stop", "Mining daemon terminated successfully!");
148        Ok(())
149    }
150}
151
152#[cfg(test)]
153use url::Url;
154
155#[test]
156/// Test the programmatic control of `Minerd`.
157///
158/// First we initialize a daemon, start it and then perform
159/// couple of restarts to verify everything works as expected.
160fn minerd_programmatic_control() -> Result<()> {
161    // Initialize logger
162    let mut cfg = simplelog::ConfigBuilder::new();
163
164    // We check this error so we can execute same file tests in parallel,
165    // otherwise second one fails to init logger here.
166    if simplelog::TermLogger::init(
167        simplelog::LevelFilter::Info,
168        //simplelog::LevelFilter::Debug,
169        //simplelog::LevelFilter::Trace,
170        cfg.build(),
171        simplelog::TerminalMode::Mixed,
172        simplelog::ColorChoice::Auto,
173    )
174    .is_err()
175    {
176        log::debug!(target: "minerd_programmatic_control", "Logger initialized");
177    }
178
179    // Daemon configuration
180    let threads = 4;
181    let rpc_settings =
182        RpcSettings { listen: Url::parse("tcp://127.0.0.1:28467")?, ..RpcSettings::default() };
183
184    // Create an executor and communication signals
185    let ex = Arc::new(smol::Executor::new());
186    let (signal, shutdown) = smol::channel::unbounded::<()>();
187
188    // Generate a dummy mining job
189    let target = darkfi::rpc::util::JsonValue::String(
190        num_bigint::BigUint::from_bytes_be(&[0xFF; 32]).to_string(),
191    );
192    let block = darkfi::rpc::util::JsonValue::String(darkfi::util::encoding::base64::encode(
193        &darkfi_serial::serialize(&darkfi::blockchain::BlockInfo::default()),
194    ));
195    let mining_job = darkfi::rpc::jsonrpc::JsonRequest::new(
196        "mine",
197        darkfi::rpc::util::JsonValue::Array(vec![target, block]),
198    );
199
200    easy_parallel::Parallel::new()
201        .each(0..threads, |_| smol::block_on(ex.run(shutdown.recv())))
202        .finish(|| {
203            smol::block_on(async {
204                // Initialize a daemon
205                let daemon = Minerd::init(threads, 0);
206
207                // Start it
208                daemon.start(&ex, &rpc_settings);
209
210                // Generate a JSON-RPC client to send mining jobs
211                let mut rpc_client =
212                    darkfi::rpc::client::RpcClient::new(rpc_settings.listen.clone(), ex.clone())
213                        .await;
214                while rpc_client.is_err() {
215                    rpc_client = darkfi::rpc::client::RpcClient::new(
216                        rpc_settings.listen.clone(),
217                        ex.clone(),
218                    )
219                    .await;
220                }
221                let rpc_client = rpc_client.unwrap();
222
223                // Send a mining job but stop the daemon after it starts mining
224                smol::future::or(
225                    async {
226                        rpc_client.request(mining_job).await.unwrap();
227                    },
228                    async {
229                        // Wait node to start mining
230                        darkfi::system::sleep(2).await;
231                        daemon.stop().await.unwrap();
232                    },
233                )
234                .await;
235                rpc_client.stop().await;
236
237                // Start it again
238                daemon.start(&ex, &rpc_settings);
239
240                // Stop it
241                daemon.stop().await.unwrap();
242
243                // Shutdown entirely
244                drop(signal);
245            })
246        });
247
248    Ok(())
249}