add tari-gbt(rust)

This commit is contained in:
lzx
2025-09-04 16:09:36 +08:00
parent 953a76439a
commit 8cc56a489e
65 changed files with 4851 additions and 14397 deletions

View File

@@ -0,0 +1,68 @@
[package]
name = "minotari_miner"
authors = ["The Tari Development Community"]
description = "The tari miner implementation"
repository = "https://github.com/tari-project/tari"
license = "BSD-3-Clause"
version = "4.9.0"
edition = "2018"
[dependencies]
tari_core = { path = "../../base_layer/core", default-features = false }
tari_common = { path = "../../common" }
tari_common_types = { path = "../../base_layer/common_types" }
tari_max_size = { path = "../../infrastructure/max_size" }
minotari_app_utilities = { path = "../minotari_app_utilities", features = [
"miner_input",
] }
minotari_app_grpc = { path = "../minotari_app_grpc" }
tari_utilities = { version = "0.8" }
base64 = "0.13.0"
borsh = "1.5.7"
bufstream = "0.1"
chrono = { version = "0.4.39", default-features = false }
clap = { version = "3.2", features = ["derive"] }
crossbeam = "0.8"
crossterm = { version = "0.28" }
derivative = "2.2.0"
futures = "0.3"
hex = "0.4.2"
log = { version = "0.4", features = ["std"] }
log4rs = { version = "1.3.0", default-features = false, features = [
"config_parsing",
"threshold_filter",
"yaml_format",
"console_appender",
"rolling_file_appender",
"compound_policy",
"size_trigger",
"fixed_window_roller",
] }
native-tls = "0.2"
num_cpus = "1.13"
rand = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = "1.0.57"
thiserror = "1.0"
tokio = { version = "1.44", default-features = false, features = [
"rt-multi-thread",
] }
tonic = { version = "0.13.1", features = ["tls-ring", "tls-native-roots"] }
zmq = "0.10"
anyhow = "1.0"
[dev-dependencies]
prost-types = "0.13.3"
chrono = { version = "0.4.39", default-features = false }
config = "0.14.0"
zmq = "0.10"
anyhow = "1.0"
[package.metadata.cargo-machete]
ignored = [
# We need to specify extra features for log4rs even though it is not used directly in this crate
"log4rs",
"prost",
]

View File

@@ -0,0 +1,39 @@
# Standalone miner for the Minotari network
The Minotari Miner application should be running connected to the Minotari Base Node and the Minotari Console Wallet,
allowing it insert the coinbase transactions created by the Minotari Wallet and to to mine blocks matching the
required network difficulty. This also serves as a reference implementation for custom miners that which can be derived
from it.
### Installation
Please refer to the relevant section in the main
[README installation section](https://github.com/tari-project/tari/blob/development/README.md#install-and-run).
### Configuration
When running with local versions of the Minotari Base Node and the Minotari Wallet, no additional configuration other
than what is described in the main
[README configuration section](https://github.com/tari-project/tari/blob/development/README.md#tari-sha3-mining)
is required. The Minotari Miner can also be located on a remote workstation.
Configuration options for the Minotari Miner are as follows:
- `base_node_grpc_address` - this is IPv4/IPv6 address including port number, by which the Minotari Base Node can be found;
- `num_mining_threads` - the number of mining threads, which defaults to the number of CPU cores;
- `mine_on_tip_only` - mining will only start when the Minotari Base Node reports it is in the bootstrapped state;
- `proof_of_work_algo` - The proof of work algorithm to use
- `validate_tip_timeout_sec` - the interval at which the current block height will be checked to determine if mining
must be restarted, whereby the tip might have advanced passed the block height that is in use in the current template.
- `mining_pool_address` - Stratum Mode configuration - mining pool address
- `mining_wallet_address` - `Stratum Mode configuration - mining wallet address/public key`
- `mining_worker_name` - `Stratum Mode configuration - mining worker name`
- `coinbase_extra` - Note that this data is publicly readable, but it is suggested you populate it so that pool
dominance can be seen before any one party has more than 51%.
- `network` - "Selected network"
- `wait_timeout_on_error` - "Base node reconnect timeout after any gRPC or miner error"
- `wallet_payment_address` - "The Tari wallet address where the mining funds will be sent to"
### Caveats
Currently, the Minotari Miner only supports SHA3 mining; this is adequate for the current Tari protocol.

View File

@@ -0,0 +1,33 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#[cfg(windows)]
fn main() {
use std::env;
println!("cargo:rerun-if-changed=icon.res");
let mut path = env::current_dir().unwrap();
path.push("icon.res");
println!("cargo:rustc-link-arg={}", path.into_os_string().into_string().unwrap());
}
#[cfg(not(windows))]
fn main() {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

View File

@@ -0,0 +1 @@
miner ICON "icon.ico"

Binary file not shown.

View File

@@ -0,0 +1,53 @@
#!/bin/bash
#
# Copyright 2024. The Tari Project
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
# following disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
# products derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#
echo
echo "Starting Miner"
echo
# Initialize
if [ -z "${use_parent_paths}" ]
then
base_path="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
config_path="${base_path}/config"
exe_path="${base_path}/runtime"
fi
"${exe_path}/start_tor.sh"
if [ ! -f "${config_path}/log4rs_miner.yml" ]
then
echo Creating new "${config_path}/log4rs_miner.yml";
init_flag="--init"
else
echo Using existing "${config_path}/log4rs_miner.yml";
init_flag=""
fi
echo
# Run
echo Spawning Miner into new terminal..
echo
gnome-terminal --working-directory="$PWD" -- "${exe_path}/minotari_miner" ${init_flag} --config "${config_path}/config.toml" --log_config "${config_path}/log4rs_miner.yml" --base-path ${base_path}

View File

@@ -0,0 +1 @@
./runtime/start_minotari_miner.sh

View File

@@ -0,0 +1,62 @@
# A sample log configuration file for running in release mode. By default, this configuration splits up log messages to
# three destinations:
# * Console: For log messages with level INFO and higher
# * log/miner/network.log: INFO-level logs related to the comms crate. This file will be quite busy since there
# are lots of P2P debug messages, and so this traffic is segregated from the application log messages
#
# See https://docs.rs/log4rs/0.8.3/log4rs/encode/pattern/index.html for deciphering the log pattern. The log format
# used in this sample configuration prints messages as:
# timestamp [target] LEVEL message
refresh_rate: 30 seconds
appenders:
# An appender named "stdout" that writes to stdout
stdout:
kind: console
encoder:
pattern: "{d(%H:%M)} {h({l}):5} {m}{n}"
filters:
- kind: threshold
level: info
# An appender named "base_layer" that writes to a file with a custom pattern encoder
miner:
kind: rolling_file
path: "{{log_dir}}/log/miner/miner.log"
policy:
kind: compound
trigger:
kind: size
limit: 10mb
roller:
kind: fixed_window
base: 1
count: 5
pattern: "{{log_dir}}/log/miner/miner.{}.log"
encoder:
pattern: "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] [Thread:{I}] {l:5} {m}{n}"
# Set the default logging level to "warn" and attach the "stdout" appender to the root
root:
level: warn
appenders:
- stdout
loggers:
# miner
minotari::application:
level: debug
appenders:
- miner
- stdout
additive: false
minotari::miner:
level: debug
appenders:
- miner
- stdout
additive: false
minotari_miner:
level: debug
appenders:
- miner
- stdout
additive: false

View File

@@ -0,0 +1,63 @@
#!/bin/bash
#
# Copyright 2024. The Tari Project
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
# following disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
# products derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#
echo
echo "Starting Miner"
echo
# Initialize
if [ -z "${use_parent_paths}" ]
then
base_path="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
config_path="${base_path}/config"
exe_path="${base_path}/runtime"
fi
tor_running=$(lsof -nP -iTCP:9050)
if [ -z "${tor_running}" ]
then
echo "Starting Tor"
open -a Terminal.app "${exe_path}/start_tor.sh"
ping -c 15 localhost > /dev/null
fi
if [ ! -f "${config_path}/log4rs_miner.yml" ]
then
echo Creating new "${config_path}/log4rs_miner.yml";
init_flag="--init"
else
echo Using existing "${config_path}/log4rs_miner.yml";
init_flag=""
fi
echo
# Run
echo Spawning Console Wallet into new terminal..
echo "${exe_path}/minotari_miner" ${init_flag} --config="${config_path}/config.toml" --log_config="${config_path}/log4rs_miner.yml" --base-path=${base_path} > $exe_path/tari_miner_command.sh
chmod +x $exe_path/minotari_miner_command.sh
open -a terminal $exe_path/minotari_miner_command.sh
echo

View File

@@ -0,0 +1 @@
./runtime/start_minotari_miner.sh

View File

@@ -0,0 +1,70 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use clap::Parser;
use minotari_app_utilities::common_cli_args::CommonCliArgs;
use tari_common::configuration::{ConfigOverrideProvider, Network};
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
#[clap(propagate_version = true)]
pub struct Cli {
#[clap(flatten)]
pub common: CommonCliArgs,
#[clap(long, alias = "mine-until-height")]
pub mine_until_height: Option<u64>,
#[clap(long, alias = "max-blocks")]
pub miner_max_blocks: Option<u64>,
#[clap(long, alias = "min-difficulty")]
pub miner_min_diff: Option<u64>,
#[clap(long, alias = "max-difficulty")]
pub miner_max_diff: Option<u64>,
#[clap(short, long, alias = "non-interactive", env = "TARI_NON_INTERACTIVE")]
pub non_interactive_mode: bool,
#[clap(long, alias = "zmq-pub-port", default_value = "11113")]
zmq_pub_port: u16,
#[clap(long, alias = "zmq-sub-port", default_value = "11112")]
zmq_sub_port: u16,
}
impl ConfigOverrideProvider for Cli {
/// Get the configuration property overrides for the given network. In case of duplicates, the final override
/// added to the list will have preference.
fn get_config_property_overrides(&self, network: &Network) -> Vec<(String, String)> {
// Config file overrides
let mut overrides = vec![("miner.network".to_string(), network.to_string())];
// Command-line overrides
let command_line_overrides = self.common.get_config_property_overrides(network);
command_line_overrides.iter().for_each(|(k, v)| {
replace_or_add_override(&mut overrides, k, v);
});
overrides
}
}
fn replace_or_add_override(overrides: &mut Vec<(String, String)>, key: &str, value: &str) {
if let Some(index) = overrides.iter().position(|(k, _)| k == key) {
overrides.remove(index);
}
overrides.push((key.to_string(), value.to_string()));
}

View File

@@ -0,0 +1,184 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Minotari Miner Node derives all
// configuration management
//! from [tari_common] crate, also extending with few
//! specific options:
//! - base_node_grpc_address - is IPv4/IPv6 address including port number, by which Minotari Base Node can be found
//! - num_mining_threads - number of mining threads, defaults to number of cpu cores
//! - mine_on_tip_only - will start mining only when node is reporting bootstrapped state
//! - validate_tip_timeout_sec - will check tip with node every N seconds to validate that still mining on a tip All
//! miner options configured under `[miner]` section of Minotari's `config.toml`.
use std::{
path::{Path, PathBuf},
time::Duration,
};
use minotari_app_grpc::tari_rpc::{pow_algo::PowAlgos, NewBlockTemplateRequest, PowAlgo};
use serde::{Deserialize, Serialize};
use tari_common::{
configuration::{utils::deserialize_string_or_struct, Network},
SubConfigPath,
};
use tari_common_types::{grpc_authentication::GrpcAuthentication, tari_address::TariAddress};
use tari_core::{proof_of_work::PowAlgorithm, transactions::transaction_components::RangeProofType};
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct MinerConfig {
/// gRPC address of base node
pub base_node_grpc_address: Option<String>,
/// GRPC authentication for base node
pub base_node_grpc_authentication: GrpcAuthentication,
/// GRPC domain name for node TLS validation
pub base_node_grpc_tls_domain_name: Option<String>,
/// GRPC ca cert name for TLS
pub base_node_grpc_ca_cert_filename: String,
/// Number of mining threads
pub num_mining_threads: usize,
/// Start mining only when base node is bootstrapped and current block height is on the tip of network
pub mine_on_tip_only: bool,
/// The proof of work algorithm to use (Sha3x or RandomXT)
#[serde(deserialize_with = "deserialize_string_or_struct")]
pub proof_of_work_algo: PowAlgorithm,
/// Will check tip with node every N seconds and restart mining if height already taken and option
/// `mine_on_tip_only` is set to true
pub validate_tip_timeout_sec: u64,
/// Stratum Mode configuration - mining pool address
pub stratum_mining_pool_address: String,
/// Stratum Mode configuration - mining wallet address/public key
pub stratum_mining_wallet_address: String,
/// Stratum Mode configuration - mining worker name
pub mining_worker_name: String,
/// The extra data to store in the coinbase, usually some data about the mining pool.
/// Note that this data is publicly readable, but it is suggested you populate it so that
/// pool dominance can be seen before any one party has more than 51%.
pub coinbase_extra: String,
/// Selected network
pub network: Network,
/// Base node reconnect timeout after any gRPC or miner error
pub wait_timeout_on_error: u64,
/// The relative path to store persistent config
pub config_dir: PathBuf,
/// The Tari wallet address (valid address in hex) where the mining funds will be sent to - must be assigned
pub wallet_payment_address: String,
/// Range proof type - revealed_value or bullet_proof_plus: (default = revealed_value)
pub range_proof_type: RangeProofType,
/// SHA based p2pool decentralized mining enabled or not
pub sha_p2pool_enabled: bool,
pub zmq_publisher_port: u16,
pub zmq_subscriber_port: u16,
}
impl SubConfigPath for MinerConfig {
fn main_key_prefix() -> &'static str {
"miner"
}
}
impl Default for MinerConfig {
fn default() -> Self {
Self {
base_node_grpc_address: None,
base_node_grpc_authentication: GrpcAuthentication::default(),
base_node_grpc_tls_domain_name: None,
base_node_grpc_ca_cert_filename: "node_ca.pem".to_string(),
num_mining_threads: num_cpus::get(),
mine_on_tip_only: true,
proof_of_work_algo: PowAlgorithm::Sha3x,
validate_tip_timeout_sec: 30,
stratum_mining_pool_address: String::new(),
stratum_mining_wallet_address: String::new(),
mining_worker_name: String::new(),
coinbase_extra: "m2pool.com".to_string(),
network: Default::default(),
wait_timeout_on_error: 10,
config_dir: PathBuf::from("config/miner"),
wallet_payment_address: TariAddress::default().to_base58(),
range_proof_type: RangeProofType::RevealedValue,
sha_p2pool_enabled: false,
zmq_publisher_port: 11112,
zmq_subscriber_port: 11113,
}
}
}
impl MinerConfig {
pub fn pow_algo_request(&self) -> NewBlockTemplateRequest {
let algo = match self.proof_of_work_algo {
PowAlgorithm::Sha3x => Some(PowAlgo {
pow_algo: PowAlgos::Sha3x.into(),
}),
PowAlgorithm::RandomXM => Some(PowAlgo {
pow_algo: PowAlgos::Randomxm.into(),
}),
PowAlgorithm::RandomXT => Some(PowAlgo {
pow_algo: PowAlgos::Randomxt.into(),
}),
};
NewBlockTemplateRequest { algo, max_weight: 0 }
}
pub fn wait_timeout(&self) -> Duration {
Duration::from_secs(self.wait_timeout_on_error)
}
pub fn validate_tip_interval(&self) -> Duration {
Duration::from_secs(self.validate_tip_timeout_sec)
}
pub fn set_base_path<P: AsRef<Path>>(&mut self, base_path: P) {
if !self.config_dir.is_absolute() {
self.config_dir = base_path.as_ref().join(self.config_dir.as_path());
}
}
}
#[cfg(test)]
mod test {
use tari_common::DefaultConfigLoader;
use crate::config::MinerConfig;
#[test]
fn miner_configuration() {
const CONFIG: &str = r#"
[miner]
num_mining_threads=2
base_node_grpc_address = "http://my_base_node:1234"
mine_on_tip_only = false
"#;
let mut cfg: config::Config = config::Config::default();
#[allow(deprecated)]
cfg.merge(config::File::from_str(CONFIG, config::FileFormat::Toml))
.unwrap();
let config = MinerConfig::load_from(&cfg).expect("Failed to load config");
assert_eq!(config.num_mining_threads, 2);
assert_eq!(
config.base_node_grpc_address,
Some("http://my_base_node:1234".to_string())
);
assert!(!config.mine_on_tip_only);
}
}

View File

@@ -0,0 +1,163 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::convert::TryInto;
use minotari_app_grpc::tari_rpc::BlockHeader as grpc_header;
use tari_common_types::types::FixedHash;
use tari_core::{
blocks::BlockHeader,
proof_of_work::{randomx_factory::RandomXFactory, sha3x_difficulty, tari_randomx_difficulty},
};
use tari_utilities::epoch_time::EpochTime;
use crate::errors::MinerError;
pub type Difficulty = u64;
#[derive(Clone)]
pub struct BlockHeaderSha3 {
pub header: BlockHeader,
pub hashes: u64,
pub vm_key: FixedHash,
pub rx_factory: Option<RandomXFactory>,
}
impl BlockHeaderSha3 {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn new(header: grpc_header, vm_key: FixedHash, rx_factory: Option<RandomXFactory>) -> Result<Self, MinerError> {
let header: BlockHeader = header.try_into().map_err(MinerError::BlockHeader)?;
Ok(Self {
header,
hashes: 0,
vm_key,
rx_factory,
})
}
/// This function will update the timestamp of the header, but only if the new timestamp is greater than the current
/// one.
pub fn set_forward_timestamp(&mut self, timestamp: u64) {
// if the timestamp has been advanced by the base_node due to the median time we should not reverse it but we
// should only change the timestamp if we move it forward.
if timestamp > self.header.timestamp.as_u64() {
self.header.timestamp = EpochTime::from(timestamp);
}
}
pub fn random_nonce(&mut self) {
use rand::{rngs::OsRng, RngCore};
self.header.nonce = OsRng.next_u64();
}
#[inline]
pub fn inc_nonce(&mut self) {
self.header.nonce = self.header.nonce.wrapping_add(1);
}
#[inline]
pub fn difficulty_sha3(&mut self) -> Result<Difficulty, MinerError> {
self.hashes = self.hashes.saturating_add(1);
Ok(sha3x_difficulty(&self.header)?.as_u64())
}
#[inline]
pub fn difficulty_rx(&mut self) -> Result<Difficulty, MinerError> {
self.hashes = self.hashes.saturating_add(1);
let randomx_factory = self.rx_factory.as_ref().ok_or(MinerError::RandomXFactoryNotSet)?;
Ok(tari_randomx_difficulty(&self.header, randomx_factory, &self.vm_key)?.as_u64())
}
#[allow(clippy::cast_possible_wrap)]
pub fn create_header(&self) -> grpc_header {
self.header.clone().into()
}
#[inline]
pub fn height(&self) -> u64 {
self.header.height
}
}
#[cfg(test)]
pub mod test {
use chrono::{DateTime, NaiveDate, Utc};
use tari_core::proof_of_work::sha3x_difficulty as core_sha3x_difficulty;
use super::*;
#[allow(clippy::cast_sign_loss)]
pub fn get_header() -> (grpc_header, BlockHeader) {
let mut header = BlockHeader::new(0);
header.timestamp = (DateTime::<Utc>::from_naive_utc_and_offset(
NaiveDate::from_ymd_opt(2000, 1, 1)
.unwrap()
.and_hms_opt(1, 1, 1)
.unwrap(),
Utc,
)
.timestamp() as u64)
.into();
header.pow.pow_algo = tari_core::proof_of_work::PowAlgorithm::Sha3x;
(header.clone().into(), header)
}
#[test]
fn validate_nonce_difficulty() {
let (mut header, mut core_header) = get_header();
header.nonce = 1;
core_header.nonce = 1;
let mut hasher = BlockHeaderSha3::new(header, FixedHash::zero(), None).unwrap();
for _ in 0..1000 {
assert_eq!(
hasher.difficulty_sha3().unwrap(),
core_sha3x_difficulty(&core_header).unwrap().as_u64(),
"with nonces = {}:{}",
hasher.header.nonce,
core_header.nonce
);
core_header.nonce += 1;
hasher.inc_nonce();
}
}
#[test]
fn validate_timestamp_difficulty() {
let (mut header, mut core_header) = get_header();
header.nonce = 1;
core_header.nonce = 1;
let mut hasher = BlockHeaderSha3::new(header, FixedHash::zero(), None).unwrap();
let mut timestamp = core_header.timestamp;
for _ in 0..1000 {
assert_eq!(
hasher.difficulty_sha3().unwrap(),
core_sha3x_difficulty(&core_header).unwrap().as_u64(),
"with timestamp = {}",
timestamp
);
timestamp = timestamp.checked_add(EpochTime::from(1)).unwrap();
core_header.timestamp = timestamp;
hasher.set_forward_timestamp(timestamp.as_u64());
}
}
}

View File

@@ -0,0 +1,72 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use minotari_app_grpc::authentication::BasicAuthError;
use minotari_app_utilities::parse_miner_input::ParseInputError;
use tari_max_size::MaxSizeBytesError;
use thiserror::Error;
use tonic::codegen::http::uri::InvalidUri;
#[derive(Debug, Error)]
pub enum MinerError {
#[error("I/O error")]
IOError(#[from] std::io::Error),
#[error("gRPC error: {0}")]
GrpcStatus(#[from] tonic::Status),
#[error("Connection error: {0}")]
GrpcConnection(#[from] tonic::transport::Error),
#[error("Node not ready")]
NodeNotReady,
#[error("Blockchain reached specified height {0}, mining will be stopped")]
MineUntilHeightReached(u64),
#[error("Block height {0} already mined")]
MinerLostBlock(u64),
#[error("Expected non empty {0}")]
EmptyObject(String),
#[error("Invalid block header {0}")]
BlockHeader(String),
#[error("Conversion error: {0}")]
Conversion(String),
#[error("Invalid gRPC credentials: {0}")]
BasicAuthError(#[from] BasicAuthError),
#[error("Invalid gRPC url: {0}")]
InvalidUri(#[from] InvalidUri),
#[error("TLS connection error: {0}")]
TlsConnectionError(String),
#[error("Coinbase error: {0}")]
CoinbaseError(String),
#[error("Consensus build error: {0}")]
ParseInputError(#[from] ParseInputError),
#[error("Base node not responding to gRPC requests: {0}")]
BaseNodeNotResponding(String),
#[error("Limit error {0}")]
MaxSizeBytesError(#[from] MaxSizeBytesError),
#[error("Miner is set to use Rx, but not RX factory is set")]
RandomXFactoryNotSet,
#[error("RandomX error: {0}")]
MergeMineError(#[from] tari_core::proof_of_work::monero_rx::MergeMineError),
#[error("Difficulty error: {0}")]
DifficultyError(#[from] tari_core::proof_of_work::DifficultyError),
}
pub fn err_empty(name: &str) -> MinerError {
MinerError::EmptyObject(name.to_string())
}

View File

@@ -0,0 +1,39 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// non-64-bit not supported
minotari_app_utilities::deny_non_64_bit_archs!();
mod cli;
pub use cli::Cli;
use tari_common::exit_codes::ExitError;
mod run_miner;
use run_miner::start_miner;
mod config;
mod difficulty;
mod errors;
mod miner;
mod stratum;
pub async fn run_miner(cli: Cli) -> Result<(), ExitError> {
start_miner(cli).await
}

View File

@@ -0,0 +1,76 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::io::stdout;
use clap::Parser;
use crossterm::{execute, terminal::SetTitle};
use log::*;
use minotari_app_utilities::consts;
use run_miner::start_miner;
use tari_common::{exit_codes::ExitError, initialize_logging};
use crate::cli::Cli;
pub const LOG_TARGET: &str = "minotari::miner::main";
pub const LOG_TARGET_FILE: &str = "minotari::logging::miner::main";
mod cli;
mod config;
mod difficulty;
mod errors;
mod miner;
mod run_miner;
mod stratum;
/// Application entry point
#[tokio::main]
async fn main() {
let terminal_title = format!("Minotari Miner - Version {}", consts::APP_VERSION);
if let Err(e) = execute!(stdout(), SetTitle(terminal_title.as_str())) {
println!("Error setting terminal title. {}", e)
}
match main_inner().await {
Ok(_) => std::process::exit(0),
Err(err) => {
error!(target: LOG_TARGET, "Fatal error: {}", err);
let exit_code = err.exit_code;
error!(target: LOG_TARGET, "Exiting with code: {}", exit_code);
std::process::exit(exit_code as i32)
},
}
}
async fn main_inner() -> Result<(), ExitError> {
let cli = Cli::parse();
initialize_logging(
&cli.common.log_config_path("miner"),
&cli.common.get_base_path(),
include_str!("../log4rs_sample.yml"),
)?;
info!(
target: LOG_TARGET,
"Starting Minotari Miner version: {}",
consts::APP_VERSION
);
start_miner(cli).await
}

View File

@@ -0,0 +1,505 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::{
panic::panic_any,
pin::Pin,
task::{Context, Poll, Waker},
thread,
time::{Duration, Instant},
};
use chrono::Utc;
use crossbeam::channel::{bounded, Select, Sender, TrySendError};
use futures::Stream;
use log::*;
use minotari_app_grpc::tari_rpc::BlockHeader;
use tari_common_types::types::FixedHash;
use tari_core::proof_of_work::randomx_factory::RandomXFactory;
use thread::JoinHandle;
use super::difficulty::BlockHeaderSha3;
use zmq::{Socket};
use anyhow::{Result};
use serde::{Serialize, Deserialize};
use std::sync::{Arc};
use tokio::sync::{Mutex, };
use minotari_app_utilities::parse_miner_input::BaseNodeGrpcClient;
use crate::errors::MinerError;
pub const LOG_TARGET: &str = "minotari::miner::standalone";
// Identify how often mining thread is reporting / checking context
// ~400_000 hashes per second
const REPORTING_FREQUENCY_RX: u64 = 300;
const REPORTING_FREQUENCY_SHA3: u64 = 3_000_000;
// Thread's stack size, ideally we would fit all thread's data in the CPU L1 cache
const STACK_SIZE: usize = 320_000;
/// Miner will send regular reports from every mining threads
#[derive(Debug)]
pub struct MiningReport {
pub miner: usize,
pub target_difficulty: u64,
pub difficulty: u64,
pub hashes: u64,
pub elapsed: Duration,
/// Will be set for when mined header is matching required difficulty
pub header: Option<BlockHeader>,
pub height: u64,
}
/// Miner is starting number of mining threads and implements Stream for async reports polling
/// Communication with async world is performed via channel and waker so should be quite efficient
pub struct Miner {
threads: Vec<JoinHandle<()>>,
channels: Vec<crossbeam::channel::Receiver<MiningReport>>,
num_threads: usize,
header: BlockHeader,
target_difficulty: u64,
share_mode: bool,
vm_key: FixedHash,
rx_factory: Option<RandomXFactory>,
publisher_socket: Arc<Mutex<Socket>>,
subscriber_socket: Arc<Mutex<Socket>>,
base_node_client: BaseNodeGrpcClient,
}
impl Miner {
pub fn init_mining(
base_node_client: BaseNodeGrpcClient,
header: BlockHeader,
target_difficulty: u64,
num_threads: usize,
share_mode: bool,
vm_key: FixedHash,
rx_factory: Option<RandomXFactory>,
publisher_socket: Arc<Mutex<Socket>>,
subscriber_socket: Arc<Mutex<Socket>>,
) -> Self {
Self {
threads: vec![],
channels: vec![],
header,
num_threads,
target_difficulty,
share_mode,
vm_key,
rx_factory,
publisher_socket,
subscriber_socket,
base_node_client,
}
}
// this will kill all mining threads currently active and attached to this miner
pub fn kill_threads(&mut self) {
self.channels.clear();
}
// Start mining threads with async context waker
fn start_threads(&mut self, ctx: &Context<'_>) {
let miners = (0..self.num_threads)
.map(|i| {
(
thread::Builder::new()
.name(format!("cpu-miner-{}", i))
.stack_size(STACK_SIZE),
i,
)
})
.map(|(thread, i)| {
let (tx, rx) = bounded(1);
let header = self.header.clone();
let waker = ctx.waker().clone();
let difficulty = self.target_difficulty;
let share_mode = self.share_mode;
let vm_key = self.vm_key;
let rx_factory = self.rx_factory.clone();
let pub_socket = Arc::clone(&self.publisher_socket);
let sub_socket = Arc::clone(&self.subscriber_socket);
let base_node_client_clone = self.base_node_client.clone();
let handle = thread
.spawn(move || mining_task(base_node_client_clone, header, difficulty, tx, waker, i, share_mode, vm_key, rx_factory, pub_socket, sub_socket))
.expect("Failed to create mining thread");
(handle, rx)
});
let (threads, channels) = miners.unzip();
self.threads = threads;
self.channels = channels;
}
}
impl Stream for Miner {
type Item = MiningReport;
fn poll_next(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
trace!(target: LOG_TARGET, "Polling Miner");
// First poll would start all the threads passing async context waker
if self.threads.is_empty() && self.num_threads > 0 {
debug!(
target: LOG_TARGET,
"Starting {} mining threads for target difficulty {}", self.num_threads, self.target_difficulty
);
self.start_threads(ctx);
return Poll::Pending;
} else if self.num_threads == 0 {
error!(target: LOG_TARGET, "Cannot mine: no mining threads");
return Poll::Ready(None);
} else if self.channels.is_empty() {
debug!(target: LOG_TARGET, "Finished mining");
return Poll::Ready(None);
} else {
// do nothing
}
// Non blocking select from all miner's receiver channels
let mut sel = Select::new();
for rx in &self.channels {
sel.recv(rx);
}
let report = match sel.try_select() {
Ok(oper) => {
let idx = oper.index();
match oper.recv(&self.channels[idx]) {
Ok(report) => report,
Err(_) => {
// Received error would mean thread is disconnected already
trace!("Thread {} disconnected.", idx);
return Poll::Ready(None);
},
}
},
Err(_) => {
// No reports
return Poll::Pending;
},
};
if report.header.is_some() && !self.share_mode {
// Dropping recipients would stop miners next time they try to report
self.channels.clear();
}
Poll::Ready(Some(report))
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GbtMsg {
pub height: u64,
pub header: String,
pub u64target: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SubmitRequest {
pub id: u64,
pub user: String,
pub miner: String,
pub index: String,
pub header: String,
pub nonce: u64,
pub pow: String,
pub subidx: i64,
pub height: u64,
pub submitidx: u64,
}
fn send_gbt_message(socket: &Arc<Mutex<Socket>>, msg: &GbtMsg) -> Result<(), Box<dyn std::error::Error>> {
let msg_json = serde_json::to_string(msg)?;
if let Ok(pub_socket) = socket.try_lock() {
pub_socket.send_multipart(&["jobsha3x".as_bytes(), msg_json.as_bytes()], 0)?;
println!("send_gbt_message");
Ok(())
} else {
Err("Socket lock busy".into())
}
}
fn try_receive_nonce(socket: &Arc<Mutex<Socket>>) -> Option<u64> {
let sub_socket = match socket.try_lock() {
Ok(sub_socket) => sub_socket,
Err(_) => return None,
};
sub_socket.set_rcvtimeo(100).expect("Set timeout failed");
let frames = match sub_socket.recv_multipart(zmq::DONTWAIT) {
Ok(frames) => frames,
Err(zmq::Error::EAGAIN) => return None,
Err(e) => {
error!("ZMQ receive error: {}", e);
return None;
}
};
drop(sub_socket);
if frames.len() < 2 {
warn!("Invalid message format: expected at least 2 frames");
return None;
}
//println!("First frame: {:?}", frames.first().unwrap().as_slice());
//println!("Nonce frame: {:?}", &frames[1]);
if frames.first().map(Vec::as_slice) != Some(b"blksha3x") {
return None;
}
match serde_json::from_slice::<SubmitRequest>(&frames[1]) {
Ok(request) => Some(request.nonce),
Err(e) => {
warn!("Failed to parse nonce: {}", e);
None
}
}
}
/// Miner starts with a random nonce and iterates until it finds a header hash that meets the desired
/// target
pub fn mining_task(
mut node_conn: BaseNodeGrpcClient,
header: BlockHeader,
target_difficulty: u64,
sender: Sender<MiningReport>,
waker: Waker,
miner: usize,
share_mode: bool,
vm_key: FixedHash,
rx_factory: Option<RandomXFactory>,
publisher_socket: Arc<Mutex<Socket>>,
subscriber_socket: Arc<Mutex<Socket>>,
) {
let mining_algorithm = if rx_factory.is_some() { "RandomXT" } else { "Sha3X" };
let start = Instant::now();
let mut hasher = match BlockHeaderSha3::new(header, vm_key, rx_factory) {
Ok(hasher) => hasher,
Err(err) => {
let err = format!(
"Miner {} on {} failed to create hasher: {:?}",
miner, mining_algorithm, err
);
error!(target: LOG_TARGET, "{}", err);
panic_any(err);
},
};
hasher.random_nonce();
let gbt_msg = GbtMsg {
height: hasher.header.height,
header: hasher.header.mining_hash().to_string(),
u64target: target_difficulty,
};
if let Err(e) = send_gbt_message(&publisher_socket, &gbt_msg) {
error!(
target: LOG_TARGET,
"Miner {} failed to send GBT message: {}", miner, e
);
}
// We're mining over here!
trace!(target: LOG_TARGET, "Mining thread {} started for {}", miner, mining_algorithm);
let mut check_count:u64 = 0;
// Mining work
loop {
if let Some(nonce) = try_receive_nonce(&subscriber_socket) {
check_count = 0;
hasher.header.nonce = nonce;
println!("nonce {} {}", nonce, hasher.header.nonce);
trace!(
target: LOG_TARGET,
"Miner {} received new nonce: {}", miner, nonce
);
} else {
check_count += 1;
if check_count > 10 {
check_count = 0;
let result = futures::executor::block_on(validate_tip(&mut node_conn, hasher.header.height));
match result {
Ok(()) => {}
Err(e) => {
error!("Tip validation error: {:?}", e);
let difficulty = 0;
let res = sender.try_send(MiningReport {
miner,
difficulty,
hashes: hasher.hashes,
elapsed: start.elapsed(),
header: None,
height: hasher.height(),
target_difficulty,
});
waker.wake_by_ref();
trace!(target: LOG_TARGET, "Reporting from {} on {} result {:?}", miner, mining_algorithm, res);
if let Err(TrySendError::Disconnected(_)) = res {
info!(target: LOG_TARGET, "Mining thread {} on {} disconnected", miner, mining_algorithm);
return;
}
return;
}
}
}
/*check_count += 1;
let reporting_frequency = if hasher.rx_factory.is_some() {
REPORTING_FREQUENCY_RX
} else {
REPORTING_FREQUENCY_SHA3
};
if check_count > reporting_frequency {
check_count = 0;
let difficulty = 0;
let res = sender.try_send(MiningReport {
miner,
difficulty,
hashes: hasher.hashes,
elapsed: start.elapsed(),
header: None,
height: hasher.height(),
target_difficulty,
});
waker.wake_by_ref();
trace!(target: LOG_TARGET, "Reporting from {} on {} result {:?}", miner, mining_algorithm, res);
if let Err(TrySendError::Disconnected(_)) = res {
info!(target: LOG_TARGET, "Mining thread {} on {} disconnected", miner, mining_algorithm);
return;
}
}*/
continue;
}
println!("get nonce {}", hasher.header.nonce);
let hashed = if hasher.rx_factory.is_some() {
hasher.difficulty_rx()
} else {
println!("calc diff sha3x");
hasher.difficulty_sha3()
};
println!("check diff");
let difficulty = match hashed {
Ok(difficulty) => difficulty,
Err(err) => {
let err = format!(
"Miner {} failed to calculate difficulty on {}: {:?}",
miner, mining_algorithm, err
);
error!(target: LOG_TARGET, "{}", err);
panic_any(err);
},
};
println!("difficulty {},{}", difficulty, target_difficulty);
if difficulty >= target_difficulty {
debug!(
target: LOG_TARGET,
"Miner {} found nonce {} with matching difficulty {} on {} height {}",
miner, hasher.header.nonce, difficulty, mining_algorithm, hasher.header.height
);
println!(
"Miner {} found nonce {} with matching difficulty {} on {} height {}",
miner, hasher.header.nonce, difficulty, mining_algorithm, hasher.header.height
);
if let Err(err) = sender.try_send(MiningReport {
miner,
difficulty,
hashes: hasher.hashes,
elapsed: start.elapsed(),
height: hasher.height(),
header: Some(hasher.create_header()),
target_difficulty,
}) {
error!(target: LOG_TARGET, "Miner {} on {} failed to send report: {}", miner, mining_algorithm, err);
}
// If we are mining in share mode, this share might not be a block, so we need to keep mining till we get a
// new job
if share_mode {
waker.wake_by_ref();
} else {
waker.wake();
trace!(target: LOG_TARGET, "Mining thread {} on {} stopped", miner, mining_algorithm);
println!("Mining thread {} on {} stopped", miner, mining_algorithm);
return;
}
}
let reporting_frequency = if hasher.rx_factory.is_some() {
REPORTING_FREQUENCY_RX
} else {
REPORTING_FREQUENCY_SHA3
};
if hasher.header.nonce % reporting_frequency == 0 {
let res = sender.try_send(MiningReport {
miner,
difficulty,
hashes: hasher.hashes,
elapsed: start.elapsed(),
header: None,
height: hasher.height(),
target_difficulty,
});
waker.wake_by_ref();
trace!(target: LOG_TARGET, "Reporting from {} on {} result {:?}", miner, mining_algorithm, res);
if let Err(TrySendError::Disconnected(_)) = res {
info!(target: LOG_TARGET, "Mining thread {} on {} disconnected", miner, mining_algorithm);
return;
}
/*if !(share_mode) {
hasher.set_forward_timestamp(Utc::now().timestamp() as u64);
}*/
}
//hasher.inc_nonce();
}
}
async fn validate_tip(
node_conn: &mut BaseNodeGrpcClient,
height: u64,
) -> Result<(), MinerError> {
let tip = node_conn
.get_tip_info(minotari_app_grpc::tari_rpc::Empty {})
.await?
.into_inner();
let longest_height = tip.clone().metadata.unwrap().best_block_height;
if height <= longest_height {
return Err(MinerError::MinerLostBlock(height));
}
if !tip.initial_sync_achieved || tip.metadata.is_none() {
return Err(MinerError::NodeNotReady);
}
if height <= longest_height {
return Err(MinerError::MinerLostBlock(height));
}
Ok(())
}

View File

@@ -0,0 +1,735 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::{convert::TryFrom, sync::Arc, thread, time::Instant};
use futures::stream::StreamExt;
use log::*;
use minotari_app_grpc::{
authentication::ClientAuthenticationInterceptor,
conversions::transaction_output::grpc_output_with_payref,
tari_rpc::{
base_node_client::BaseNodeClient,
pow_algo::PowAlgos,
sha_p2_pool_client::ShaP2PoolClient,
Block,
GetNewBlockRequest,
PowAlgo,
SubmitBlockRequest,
SubmitBlockResponse,
},
};
use minotari_app_utilities::parse_miner_input::{
prompt_for_base_node_address,
prompt_for_p2pool_address,
verify_base_node_grpc_mining_responses,
wallet_payment_address,
BaseNodeGrpcClient,
ShaP2PoolGrpcClient,
};
use tari_common::{
exit_codes::{ExitCode, ExitError},
load_configuration,
DefaultConfigLoader,
MAX_GRPC_MESSAGE_SIZE,
};
use tari_common_types::{
tari_address::TariAddress,
types::{FixedHash, UncompressedPublicKey},
};
use tari_core::{
blocks::BlockHeader,
consensus::ConsensusManager,
proof_of_work::{randomx_factory::RandomXFactory, PowAlgorithm},
transactions::{
generate_coinbase,
tari_amount::MicroMinotari,
transaction_components::{
payment_id::{PaymentId, TxType},
CoinBaseExtra,
},
transaction_key_manager::{create_memory_db_key_manager, MemoryDbKeyManager},
},
};
use tari_utilities::hex::Hex;
use tokio::{sync::Mutex, time::sleep};
use tonic::transport::{Certificate, ClientTlsConfig, Endpoint};
use zmq::{Context as ZmqContext, Socket, SUB};
use anyhow::anyhow;
use anyhow::{Context, Result};
use crate::{
cli::Cli,
config::MinerConfig,
errors::{err_empty, MinerError},
miner::{Miner, MiningReport},
stratum::stratum_controller::controller::Controller,
};
pub const LOG_TARGET: &str = "minotari::miner::main";
pub const LOG_TARGET_FILE: &str = "minotari::logging::miner::main";
#[allow(clippy::too_many_lines)]
pub async fn start_miner(cli: Cli) -> Result<(), ExitError> {
let config_path = cli.common.config_path();
let cfg = load_configuration(
config_path.as_path(),
true,
cli.non_interactive_mode,
&cli,
cli.common.network,
)?;
let mut config = MinerConfig::load_from(&cfg).expect("Failed to load config");
config.set_base_path(cli.common.get_base_path());
debug!(target: LOG_TARGET_FILE, "{:?}", config);
let key_manager = create_memory_db_key_manager().map_err(|err| {
ExitError::new(
ExitCode::KeyManagerServiceError,
"'wallet_payment_address' ".to_owned() + &err.to_string(),
)
})?;
let wallet_payment_address = wallet_payment_address(config.wallet_payment_address.clone(), config.network)
.map_err(|err| {
ExitError::new(
ExitCode::WalletPaymentAddress,
"'wallet_payment_address' ".to_owned() + &err.to_string(),
)
})?;
debug!(target: LOG_TARGET_FILE, "wallet_payment_address: {}", wallet_payment_address);
let consensus_manager = ConsensusManager::builder(config.network)
.build()
.map_err(|err| ExitError::new(ExitCode::ConsensusManagerBuilderError, err.to_string()))?;
if !config.stratum_mining_wallet_address.is_empty() && !config.stratum_mining_pool_address.is_empty() {
let url = config.stratum_mining_pool_address.clone();
let mut miner_address = config.stratum_mining_wallet_address.clone();
let _unused = UncompressedPublicKey::from_hex(&miner_address).map_err(|_| {
ExitError::new(
ExitCode::ConfigError,
"Miner is not configured with a valid wallet address.",
)
})?;
if !config.mining_worker_name.is_empty() {
miner_address += &format!("{}{}", ".", config.mining_worker_name);
}
let mut mc = Controller::new(config.num_mining_threads).map_err(|e| {
debug!(target: LOG_TARGET_FILE, "Error loading mining controller: {}", e);
ExitError::new(
ExitCode::UnknownError,
format!("Error loading mining controller: {}", e),
)
})?;
let cc = crate::stratum::controller::Controller::new(&url, Some(miner_address), None, None, mc.tx.clone())
.map_err(|e| {
debug!(
target: LOG_TARGET_FILE,
"Error loading stratum client controller: {:?}", e
);
ExitError::new(
ExitCode::UnknownError,
format!("Error loading mining controller: {}", e),
)
})?;
mc.set_client_tx(cc.tx.clone());
let _join_handle = thread::Builder::new()
.name("client_controller".to_string())
.spawn(move || {
cc.run();
});
mc.run()
.await
.map_err(|err| ExitError::new(ExitCode::UnknownError, format!("Stratum error: {:?}", err)))?;
Ok(())
} else {
let node_clients = connect(&config)
.await
.map_err(|e| ExitError::new(ExitCode::GrpcError, e.to_string()))?;
let mut base_node_client = node_clients.base_node_client;
let mut p2pool_node_client = node_clients.p2pool_node_client;
if let Err(e) = verify_base_node_responses(&mut base_node_client, &config).await {
if let MinerError::BaseNodeNotResponding(_) = e {
error!(target: LOG_TARGET, "{}", e);
println!();
let msg = "Could not connect to the base node. \nAre the base node's gRPC mining methods allowed in \
its 'config.toml'? Please ensure these methods are enabled in:\n \
'grpc_server_allow_methods': \"get_new_block_template\", \"get_tip_info\", \
\"get_new_block\", \"submit_block\"";
println!("{}", msg);
println!();
return Err(ExitError::new(ExitCode::GrpcError, e.to_string()));
}
}
config.zmq_publisher_port = 11113;
config.zmq_subscriber_port = 11112;
println!("pub port:{}, sub port {}", config.zmq_publisher_port, config.zmq_subscriber_port);
let zmq_context = ZmqContext::new();
let publisher_socket = zmq_context
.socket(zmq::PUB)
.map_err(|e| anyhow!("ZMQ publisher error: {}", e))?;
let publisher_addr = format!("tcp://0.0.0.0:{}", config.zmq_publisher_port);
println!("publisher_addr:{}",publisher_addr);
publisher_socket
.bind(&publisher_addr)
.map_err(|e| anyhow!("ZMQ bind error: {}", e))?;
let subscriber_socket = zmq_context
.socket(SUB)
.context("Failed to create SUB socket")?;
let subscriber_addr = format!("tcp://172.17.0.1:{}", config.zmq_subscriber_port);
println!("subscriber_addr:{}",subscriber_addr);
subscriber_socket
.connect(&subscriber_addr)
.context("Failed to connect to PUB server")?;
subscriber_socket
.set_subscribe(b"")
.context("Failed to subscribe to all messages")?;
let publisher = Arc::new(Mutex::new(publisher_socket));
let subscriber = Arc::new(Mutex::new(subscriber_socket));
let mut blocks_found: u64 = 0;
loop {
debug!(target: LOG_TARGET, "Starting new mining cycle");
match mining_cycle(
&mut base_node_client,
p2pool_node_client.clone(),
&config,
&cli,
&key_manager,
&wallet_payment_address,
&consensus_manager,
publisher.clone(),
subscriber.clone(),
)
.await
{
err @ Err(MinerError::GrpcConnection(_)) | err @ Err(MinerError::GrpcStatus(_)) => {
// Any GRPC error we will try to reconnect with a standard delay
error!(target: LOG_TARGET, "Connection error: {:?}", err);
loop {
info!(target: LOG_TARGET, "Holding for {:?}", config.wait_timeout());
sleep(config.wait_timeout()).await;
match connect(&config).await {
Ok(nc) => {
base_node_client = nc.base_node_client;
p2pool_node_client = nc.p2pool_node_client;
break;
},
Err(err) => {
error!(target: LOG_TARGET, "Connection error: {:?}", err);
continue;
},
}
}
},
Err(MinerError::MineUntilHeightReached(h)) => {
warn!(
target: LOG_TARGET,
"Prescribed blockchain height {} reached. Aborting ...", h
);
return Ok(());
},
Err(MinerError::MinerLostBlock(h)) => {
warn!(
target: LOG_TARGET,
"Height {} already mined by other node. Restarting ...", h
);
},
Err(err) => {
error!(target: LOG_TARGET, "Error: {:?}", err);
sleep(config.wait_timeout()).await;
},
Ok(submitted) => {
info!(target: LOG_TARGET, "💰 Found block");
if submitted {
blocks_found += 1;
}
println!("blocks_found: {} {}", blocks_found, cli.miner_max_blocks.unwrap_or(0));
if let Some(max_blocks) = cli.miner_max_blocks {
if blocks_found >= max_blocks {
println!("blocks_found >= max_blocks exit");
return Ok(());
}
}
},
}
}
}
}
pub struct NodeClientResult {
base_node_client: BaseNodeGrpcClient,
p2pool_node_client: Option<ShaP2PoolGrpcClient>,
}
async fn connect(config: &MinerConfig) -> Result<NodeClientResult, MinerError> {
// always connect to base node first
let base_node_client = match connect_base_node(config).await {
Ok(client) => client,
Err(e) => {
error!(target: LOG_TARGET, "Could not connect to base node: {}", e);
let msg = "Could not connect to base node. \nIs the base node's gRPC running? Try running it with \
`--enable-grpc` or enable it in the config.";
println!("{}", msg);
return Err(e);
},
};
// init client to sha p2pool grpc if enabled
let mut p2pool_node_client = None;
if config.sha_p2pool_enabled {
p2pool_node_client = match connect_sha_p2pool(config).await {
Ok(client) => Some(client),
Err(e) => {
error!(target: LOG_TARGET, "Could not connect to base node: {}", e);
let msg = "Could not connect to base node. \nIs the base node's gRPC running? Try running it with \
`--enable-grpc` or enable it in the config.";
println!("{}", msg);
return Err(e);
},
};
}
Ok(NodeClientResult {
base_node_client,
p2pool_node_client,
})
}
async fn connect_sha_p2pool(config: &MinerConfig) -> Result<ShaP2PoolGrpcClient, MinerError> {
let p2pool_node_addr;
if let Some(ref a) = config.base_node_grpc_address {
p2pool_node_addr = a.clone();
} else {
p2pool_node_addr = prompt_for_p2pool_address()?;
}
info!(target: LOG_TARGET, "👛 Connecting to p2pool node at {}", p2pool_node_addr);
let mut endpoint = Endpoint::new(p2pool_node_addr)?;
if let Some(domain_name) = config.base_node_grpc_tls_domain_name.as_ref() {
let pem = tokio::fs::read(config.config_dir.join(&config.base_node_grpc_ca_cert_filename))
.await
.map_err(|e| MinerError::TlsConnectionError(e.to_string()))?;
let ca = Certificate::from_pem(pem);
let tls = ClientTlsConfig::new().ca_certificate(ca).domain_name(domain_name);
endpoint = endpoint
.tls_config(tls)
.map_err(|e| MinerError::TlsConnectionError(e.to_string()))?;
}
let channel = endpoint
.connect()
.await
.map_err(|e| MinerError::TlsConnectionError(e.to_string()))?;
let node_conn = ShaP2PoolClient::with_interceptor(
channel,
ClientAuthenticationInterceptor::create(&config.base_node_grpc_authentication)?,
)
.max_encoding_message_size(MAX_GRPC_MESSAGE_SIZE)
.max_decoding_message_size(MAX_GRPC_MESSAGE_SIZE);
Ok(node_conn)
}
async fn connect_base_node(config: &MinerConfig) -> Result<BaseNodeGrpcClient, MinerError> {
let base_node_addr;
if let Some(ref a) = config.base_node_grpc_address {
base_node_addr = a.clone();
} else {
base_node_addr = prompt_for_base_node_address(config.network)?;
}
info!(target: LOG_TARGET, "👛 Connecting to base node at {}", base_node_addr);
let mut endpoint = Endpoint::new(base_node_addr)?;
if let Some(domain_name) = config.base_node_grpc_tls_domain_name.as_ref() {
let pem = tokio::fs::read(config.config_dir.join(&config.base_node_grpc_ca_cert_filename))
.await
.map_err(|e| MinerError::TlsConnectionError(e.to_string()))?;
let ca = Certificate::from_pem(pem);
let tls = ClientTlsConfig::new().ca_certificate(ca).domain_name(domain_name);
endpoint = endpoint
.tls_config(tls)
.map_err(|e| MinerError::TlsConnectionError(e.to_string()))?;
}
let channel = endpoint
.connect()
.await
.map_err(|e| MinerError::TlsConnectionError(e.to_string()))?;
let node_conn = BaseNodeClient::with_interceptor(
channel,
ClientAuthenticationInterceptor::create(&config.base_node_grpc_authentication)?,
)
.max_encoding_message_size(MAX_GRPC_MESSAGE_SIZE)
.max_decoding_message_size(MAX_GRPC_MESSAGE_SIZE);
Ok(node_conn)
}
async fn verify_base_node_responses(
node_conn: &mut BaseNodeGrpcClient,
config: &MinerConfig,
) -> Result<(), MinerError> {
if let Err(e) = verify_base_node_grpc_mining_responses(node_conn, config.pow_algo_request()).await {
return Err(MinerError::BaseNodeNotResponding(e));
}
Ok(())
}
struct GetNewBlockResponse {
block: Block,
target_difficulty: u64,
vm_key: FixedHash,
}
/// Gets a new block from base node or p2pool node if its enabled in config
async fn get_new_block(
base_node_client: &mut BaseNodeGrpcClient,
sha_p2pool_client: Arc<Mutex<Option<ShaP2PoolGrpcClient>>>,
config: &MinerConfig,
cli: &Cli,
key_manager: &MemoryDbKeyManager,
wallet_payment_address: &TariAddress,
consensus_manager: &ConsensusManager,
) -> Result<GetNewBlockResponse, MinerError> {
if config.sha_p2pool_enabled {
if let Some(client) = sha_p2pool_client.lock().await.as_mut() {
return get_new_block_p2pool_node(config, client, wallet_payment_address).await;
}
}
get_new_block_base_node(
base_node_client,
config,
cli,
key_manager,
wallet_payment_address,
consensus_manager,
)
.await
}
async fn get_new_block_base_node(
base_node_client: &mut BaseNodeGrpcClient,
config: &MinerConfig,
cli: &Cli,
key_manager: &MemoryDbKeyManager,
wallet_payment_address: &TariAddress,
consensus_manager: &ConsensusManager,
) -> Result<GetNewBlockResponse, MinerError> {
debug!(target: LOG_TARGET, "Getting new block template");
let template_response = base_node_client
.get_new_block_template(config.pow_algo_request())
.await?
.into_inner();
let mut block_template = template_response
.new_block_template
.clone()
.ok_or_else(|| err_empty("new_block_template"))?;
let height = block_template
.header
.as_ref()
.ok_or_else(|| err_empty("header"))?
.height;
if config.mine_on_tip_only {
debug!(
target: LOG_TARGET,
"Checking if base node is synced, because mine_on_tip_only is true"
);
validate_tip(base_node_client, height, cli.mine_until_height).await?;
}
debug!(target: LOG_TARGET, "Getting coinbase");
let miner_data = template_response.miner_data.ok_or_else(|| err_empty("miner_data"))?;
let fee = MicroMinotari::from(miner_data.total_fees);
let reward = MicroMinotari::from(miner_data.reward);
let (coinbase_output, coinbase_kernel) = generate_coinbase(
fee,
reward,
height,
&CoinBaseExtra::try_from(config.coinbase_extra.as_bytes().to_vec())?,
key_manager,
wallet_payment_address,
true,
consensus_manager.consensus_constants(height),
config.range_proof_type,
PaymentId::Open {
user_data: vec![],
tx_type: TxType::Coinbase,
},
)
.await
.map_err(|e| MinerError::CoinbaseError(e.to_string()))?;
debug!(target: LOG_TARGET, "Coinbase kernel: {}", coinbase_kernel);
debug!(target: LOG_TARGET, "Coinbase output: {}", coinbase_output);
let body = block_template
.body
.as_mut()
.ok_or_else(|| err_empty("new_block_template.body"))?;
let grpc_output =
grpc_output_with_payref(coinbase_output.clone(), None).map_err(|e| MinerError::Conversion(e.to_string()))?;
body.outputs.push(grpc_output);
body.kernels.push(coinbase_kernel.into());
let target_difficulty = miner_data.target_difficulty;
debug!(target: LOG_TARGET, "Asking base node to assemble the MMR roots");
let block_result = base_node_client.get_new_block(block_template).await?.into_inner();
Ok(GetNewBlockResponse {
block: block_result.block.ok_or_else(|| err_empty("block"))?,
target_difficulty,
vm_key: FixedHash::try_from(block_result.vm_key).map_err(|_| MinerError::Conversion("vm_key".to_string()))?,
})
}
async fn get_new_block_p2pool_node(
config: &MinerConfig,
sha_p2pool_client: &mut ShaP2PoolGrpcClient,
wallet_payment_address: &TariAddress,
) -> Result<GetNewBlockResponse, MinerError> {
let pow_algo = PowAlgo {
pow_algo: PowAlgos::Sha3x.into(),
};
let coinbase_extra = if config.coinbase_extra.trim().is_empty() {
String::new()
} else {
config.coinbase_extra.clone()
};
let block_result = sha_p2pool_client
.get_new_block(GetNewBlockRequest {
pow: Some(pow_algo),
coinbase_extra,
wallet_payment_address: wallet_payment_address.to_base58(),
})
.await?
.into_inner();
let new_block_result = block_result.block.ok_or_else(|| err_empty("block result"))?;
let block = new_block_result.block.ok_or_else(|| err_empty("block response"))?;
Ok(GetNewBlockResponse {
block,
target_difficulty: block_result.target_difficulty,
vm_key: FixedHash::zero(),
})
}
async fn submit_block(
config: &MinerConfig,
base_node_client: &mut BaseNodeGrpcClient,
sha_p2pool_client: Option<&mut ShaP2PoolGrpcClient>,
block: Block,
wallet_payment_address: &TariAddress,
) -> Result<SubmitBlockResponse, MinerError> {
if config.sha_p2pool_enabled {
if let Some(client) = sha_p2pool_client {
return Ok(client
.submit_block(SubmitBlockRequest {
block: Some(block),
wallet_payment_address: wallet_payment_address.to_hex(),
})
.await
.map_err(MinerError::GrpcStatus)?
.into_inner());
}
}
Ok(base_node_client
.submit_block(block)
.await
.map_err(MinerError::GrpcStatus)?
.into_inner())
}
#[allow(clippy::too_many_lines)]
async fn mining_cycle(
base_node_client: &mut BaseNodeGrpcClient,
sha_p2pool_client: Option<ShaP2PoolGrpcClient>,
config: &MinerConfig,
cli: &Cli,
key_manager: &MemoryDbKeyManager,
wallet_payment_address: &TariAddress,
consensus_manager: &ConsensusManager,
publisher_socket: Arc<Mutex<Socket>>,
subscriber_socket: Arc<Mutex<Socket>>,
) -> Result<bool, MinerError> {
let sha_p2pool_client = Arc::new(Mutex::new(sha_p2pool_client));
let block_result = get_new_block(
base_node_client,
sha_p2pool_client.clone(),
config,
cli,
key_manager,
wallet_payment_address,
consensus_manager,
)
.await?;
let block = block_result.block;
let header = block.clone().header.ok_or_else(|| err_empty("block.header"))?;
debug!(target: LOG_TARGET, "Initializing miner");
let rx_factory = if config.proof_of_work_algo == PowAlgorithm::RandomXT {
Some(RandomXFactory::new(config.num_mining_threads))
} else {
None
};
let mut reports = Miner::init_mining(
base_node_client.clone(),
header.clone(),
block_result.target_difficulty,
config.num_mining_threads,
false,
block_result.vm_key,
rx_factory,
publisher_socket,
subscriber_socket,
);
let mut reporting_timeout = Instant::now();
let mut block_submitted = false;
while let Some(report) = reports.next().await {
if let Some(header) = report.header.clone() {
let mut submit = true;
if let Some(min_diff) = cli.miner_min_diff {
if report.difficulty < min_diff {
submit = false;
debug!(
target: LOG_TARGET_FILE,
"Mined difficulty {} below minimum difficulty {}. Not submitting.", report.difficulty, min_diff
);
}
}
if let Some(max_diff) = cli.miner_max_diff {
if report.difficulty > max_diff {
submit = false;
debug!(
target: LOG_TARGET_FILE,
"Mined difficulty {} greater than maximum difficulty {}. Not submitting.",
report.difficulty,
max_diff
);
}
}
if submit {
// Mined a block fitting the difficulty
let block_header = BlockHeader::try_from(header.clone()).map_err(MinerError::Conversion)?;
debug!(
target: LOG_TARGET,
"Miner found block header {} with difficulty {:?}", block_header, report.difficulty,
);
println!(
"Miner found block header {} with difficulty {:?}", block_header, report.difficulty,
);
let mut mined_block = block.clone();
mined_block.header = Some(header);
// 5. Sending block to the node
submit_block(
config,
base_node_client,
sha_p2pool_client.lock().await.as_mut(),
mined_block,
wallet_payment_address,
)
.await?;
block_submitted = true;
break;
} else {
display_report(&report, config.num_mining_threads).await;
}
} else {
display_report(&report, config.num_mining_threads).await;
}
//if config.mine_on_tip_only && reporting_timeout.elapsed() > config.validate_tip_interval() {
validate_tip(base_node_client, report.height, cli.mine_until_height).await?;
reporting_timeout = Instant::now();
//}
}
// Not waiting for threads to stop, they should stop in a short while after `reports` dropped
Ok(block_submitted)
}
pub async fn display_report(report: &MiningReport, num_mining_threads: usize) {
let mut hashrate = report.hashes as f64 / report.elapsed.as_secs() as f64;
let display_string = if hashrate > 1_000_000.0 {
hashrate /= 1_000_000.0;
"MH/s"
} else {
hashrate /= 1_000.0;
"KH/s"
};
info!(
target: LOG_TARGET,
"⛏ Miner {:0>2} reported {:.2}{} with total {:.2}{} over {} threads. Height: {}. Target: {})",
report.miner,
hashrate,
display_string,
hashrate * num_mining_threads as f64,
display_string,
num_mining_threads,
report.height,
report.target_difficulty,
);
}
/// If config
async fn validate_tip(
node_conn: &mut BaseNodeGrpcClient,
height: u64,
mine_until_height: Option<u64>,
) -> Result<(), MinerError> {
let tip = node_conn
.get_tip_info(minotari_app_grpc::tari_rpc::Empty {})
.await?
.into_inner();
let longest_height = tip.clone().metadata.unwrap().best_block_height;
if let Some(height) = mine_until_height {
if longest_height >= height {
return Err(MinerError::MineUntilHeightReached(height));
}
}
if height <= longest_height {
return Err(MinerError::MinerLostBlock(height));
}
if !tip.initial_sync_achieved || tip.metadata.is_none() {
return Err(MinerError::NodeNotReady);
}
if height <= longest_height {
return Err(MinerError::MinerLostBlock(height));
}
Ok(())
}

View File

@@ -0,0 +1,419 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::{
io::{BufRead, ErrorKind, Write},
sync::mpsc,
thread,
time::{Duration, Instant},
};
use log::*;
use crate::stratum::{error::Error, stratum_types as types, stream::Stream};
pub const LOG_TARGET: &str = "minotari::miner::stratum::controller";
pub const LOG_TARGET_FILE: &str = "minotari::logging::miner::stratum::controller";
pub struct Controller {
server_url: String,
server_login: Option<String>,
server_password: Option<String>,
server_tls_enabled: Option<bool>,
stream: Option<Stream>,
rx: mpsc::Receiver<types::client_message::ClientMessage>,
pub tx: mpsc::Sender<types::client_message::ClientMessage>,
miner_tx: mpsc::Sender<types::miner_message::MinerMessage>,
last_request_id: String,
}
// fn invalid_error_response() -> types::RpcError {
// types::RpcError {
// code: 0,
// message: "Invalid error response received".to_owned(),
// }
// }
impl Controller {
pub fn new(
server_url: &str,
server_login: Option<String>,
server_password: Option<String>,
server_tls_enabled: Option<bool>,
miner_tx: mpsc::Sender<types::miner_message::MinerMessage>,
) -> Result<Controller, Error> {
let (tx, rx) = mpsc::channel::<types::client_message::ClientMessage>();
Ok(Controller {
server_url: server_url.to_string(),
server_login,
server_password,
server_tls_enabled,
stream: None,
tx,
rx,
miner_tx,
last_request_id: "".to_string(),
})
}
pub fn try_connect(&mut self) -> Result<(), Error> {
self.stream = None;
let stream = Stream::try_connect(&self.server_url, self.server_tls_enabled)?;
self.stream = Some(stream);
Ok(())
}
fn stream(&mut self) -> Result<&mut Stream, Error> {
self.stream.as_mut().ok_or(Error::NotConnected)
}
fn read_message(&mut self) -> Result<Option<String>, Error> {
let mut line = String::new();
match self.stream()?.read_line(&mut line) {
Ok(_) => {
// stream is not returning a proper error on disconnect
if line.is_empty() {
return Err(Error::Connection("broken pipe".to_string()));
}
Ok(Some(line))
},
Err(ref e) if e.kind() == ErrorKind::BrokenPipe => Err(Error::Connection("broken pipe".to_string())),
Err(ref e) if e.kind() == ErrorKind::WouldBlock => Ok(None),
Err(e) => {
error!(target: LOG_TARGET, "Communication error with stratum server: {}", e);
Err(Error::Connection("broken pipe".to_string()))
},
}
}
fn send_message(&mut self, message: &str) -> Result<(), Error> {
let stream = self.stream()?;
debug!(target: LOG_TARGET_FILE, "sending request: {}", message);
stream.write_all(message.as_bytes())?;
stream.write_all(b"\n")?;
stream.flush()?;
Ok(())
}
fn send_message_get_job_template(&mut self) -> Result<(), Error> {
let params = types::worker_identifier::WorkerIdentifier {
id: self.last_request_id.clone(),
};
let req = types::rpc_request::RpcRequest {
id: Some(self.last_request_id.clone()),
jsonrpc: "2.0".to_string(),
method: "getjob".to_string(),
params: Some(serde_json::to_value(params)?),
};
let req_str = serde_json::to_string(&req)?;
self.send_message(&req_str)
}
fn send_login(&mut self) -> Result<(), Error> {
// only send the login request if a login string is configured
let login_str = match self.server_login.clone() {
None => "".to_string(),
Some(server_login) => server_login,
};
if login_str.is_empty() {
return Ok(());
}
let password_str = match self.server_password.clone() {
None => "".to_string(),
Some(server_password) => server_password,
};
let params = types::login_params::LoginParams {
login: login_str,
pass: password_str,
agent: "minotari-miner".to_string(),
};
let req_id = self.last_request_id.to_string();
let req = types::rpc_request::RpcRequest {
id: if req_id.is_empty() {
Some("0".to_string())
} else {
Some(req_id)
},
jsonrpc: "2.0".to_string(),
method: "login".to_string(),
params: Some(serde_json::to_value(params)?),
};
let req_str = serde_json::to_string(&req)?;
self.send_message(&req_str)
}
fn send_keepalive(&mut self) -> Result<(), Error> {
let req = types::rpc_request::RpcRequest {
id: Some(self.last_request_id.to_string()),
jsonrpc: "2.0".to_string(),
method: "keepalive".to_string(),
params: None,
};
let req_str = serde_json::to_string(&req)?;
self.send_message(&req_str)
}
fn send_message_submit(&mut self, job_id: u64, hash: String, nonce: u64) -> Result<(), Error> {
debug!(
target: LOG_TARGET,
"Submitting share with hash {} and nonce {}", hash, nonce
);
let params_in = types::submit_params::SubmitParams {
id: self.last_request_id.to_string(),
job_id,
hash,
nonce,
};
let params = serde_json::to_string(&params_in)?;
let req = types::rpc_request::RpcRequest {
id: Some(self.last_request_id.to_string()),
jsonrpc: "2.0".to_string(),
method: "submit".to_string(),
params: Some(serde_json::from_str(&params)?),
};
let req_str = serde_json::to_string(&req)?;
self.send_message(&req_str)
}
fn send_miner_job(&mut self, job: types::job_params::JobParams) -> Result<(), Error> {
let blob_bytes =
base64::decode(&job.blob).map_err(|_| Error::General("Invalid base64 byte string received".to_string()))?;
let miner_message = types::miner_message::MinerMessage::ReceivedJob(
job.height,
job.job_id.parse::<u64>()?,
job.target.parse::<u64>()?,
blob_bytes,
);
self.miner_tx.send(miner_message).map_err(Error::from)
}
fn send_miner_stop(&mut self) -> Result<(), Error> {
let miner_message = types::miner_message::MinerMessage::StopJob;
self.miner_tx.send(miner_message).map_err(Error::from)
}
fn send_miner_resume(&mut self) -> Result<(), Error> {
let miner_message = types::miner_message::MinerMessage::ResumeJob;
self.miner_tx.send(miner_message).map_err(Error::from)
}
pub fn handle_request(&mut self, req: types::rpc_request::RpcRequest) -> Result<(), Error> {
debug!(target: LOG_TARGET_FILE, "Received request type: {}", req.method);
match req.method.as_str() {
"job" => match req.params {
None => Err(Error::Request("No params in job request".to_owned())),
Some(params) => {
let job = serde_json::from_value::<types::job_params::JobParams>(params)?;
info!(
target: LOG_TARGET,
"Got a new job for height {} with target difficulty {}", job.height, job.target
);
self.send_miner_job(job)
},
},
_ => Err(Error::Request("Unknown method".to_owned())),
}
}
fn handle_error(&mut self, error: types::rpc_error::RpcError) {
if [-1, 24].contains(&error.code) {
// unauthorized
let _result = self.send_login();
} else if [21, 20, 22, 23, 25].contains(&error.code) {
// problem with template
let _result = self.send_message_get_job_template();
} else {
// unhandled
}
}
#[allow(clippy::cognitive_complexity)]
pub fn handle_response(&mut self, res: types::rpc_response::RpcResponse) -> Result<(), Error> {
debug!(target: LOG_TARGET_FILE, "Received response with id: {}", res.id);
match res.result {
Some(result) => {
let login_response = serde_json::from_value::<types::login_response::LoginResponse>(result.clone());
if let Ok(st) = login_response {
info!(
target: LOG_TARGET,
"Successful login to server, worker identifier is {}", st.id
);
info!(
target: LOG_TARGET,
"Got a new job for height {} with target difficulty {}", st.job.height, st.job.target
);
self.last_request_id = st.id;
let _result = self.send_miner_job(st.job);
return Ok(());
};
let job_response = serde_json::from_value::<types::job_params::JobParams>(result.clone());
if let Ok(st) = job_response {
info!(
target: LOG_TARGET,
"Got a new job for height {} with target difficulty {}", st.height, st.target
);
let _result = self.send_miner_job(st);
return Ok(());
};
let submit_response = serde_json::from_value::<types::submit_response::SubmitResponse>(result.clone());
if let Ok(st) = submit_response {
let error = st.error;
if let Some(error) = error {
// rejected share
self.handle_error(error);
warn!(target: LOG_TARGET, "Rejected");
} else {
// accepted share
debug!(target: LOG_TARGET, "Share accepted: {:?}", st.status);
}
return Ok(());
}
let rpc_response = serde_json::from_value::<types::rpc_response::RpcResponse>(result);
if let Ok(st) = rpc_response {
let error = st.error;
if let Some(error) = error {
self.handle_error(error);
}
return Ok(());
} else {
debug!(target: LOG_TARGET_FILE, "RPC Response: {:?}", rpc_response);
};
},
None => {
error!(target: LOG_TARGET, "RPC error: {:?}", res);
},
}
Ok(())
}
#[allow(clippy::cognitive_complexity)]
pub fn run(mut self) {
let server_read_interval = Duration::from_secs(1);
let server_retry_interval = Duration::from_secs(5);
let mut next_server_read = Instant::now() + server_read_interval;
let mut next_server_retry = Instant::now();
// Request the first job template
thread::sleep(Duration::from_secs(1));
let mut was_disconnected = true;
loop {
// Check our connection status, and try to correct if possible
if self.stream.is_none() {
if !was_disconnected {
let _result = self.send_miner_stop();
}
was_disconnected = true;
if Instant::now() > next_server_retry {
if self.try_connect().is_err() {
let status = format!(
"Connection Status: Can't establish server connection to {}. Will retry every {} seconds",
self.server_url,
server_retry_interval.as_secs()
);
warn!("{}", status);
self.stream = None;
} else {
let status = format!("Connection Status: Connected to server at {}.", self.server_url);
info!(target: LOG_TARGET, "{}", status);
}
next_server_retry = Instant::now() + server_retry_interval;
if self.stream.is_none() {
thread::sleep(std::time::Duration::from_secs(1));
continue;
}
}
} else {
// get new job template
if was_disconnected {
was_disconnected = false;
let _result = self.send_login();
let _result = self.send_miner_resume();
}
// read messages from server
if Instant::now() > next_server_read {
match self.read_message() {
Ok(Some(m)) => {
// figure out what kind of message,
// and dispatch appropriately
debug!(target: LOG_TARGET_FILE, "Received message: {}", m);
// Deserialize to see what type of object it is
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&m) {
// Is this a response or request?
if v["method"] == "job" {
// this is a request
match serde_json::from_str::<types::rpc_request::RpcRequest>(&m) {
Err(e) => error!(target: LOG_TARGET, "Error parsing request {} : {:?}", m, e),
Ok(request) => {
if let Err(err) = self.handle_request(request) {
error!(target: LOG_TARGET, "Error handling request {} : :{:?}", m, err)
}
},
}
} else {
// this is a response
match serde_json::from_str::<types::rpc_response::RpcResponse>(&m) {
Err(e) => error!(target: LOG_TARGET, "Error parsing response {} : {:?}", m, e),
Ok(response) => {
if let Err(err) = self.handle_response(response) {
error!(target: LOG_TARGET, "Error handling response {} : :{:?}", m, err)
}
},
}
}
continue;
} else {
error!(target: LOG_TARGET, "Error parsing message: {}", m)
}
},
Ok(None) => {
// noop, nothing to read for this interval
},
Err(e) => {
error!(target: LOG_TARGET, "Error reading message: {:?}", e);
self.stream = None;
continue;
},
}
next_server_read = Instant::now() + server_read_interval;
}
}
// Talk to the miner algorithm
while let Some(message) = self.rx.try_iter().next() {
debug!(target: LOG_TARGET_FILE, "Client received message: {:?}", message);
let result = match message {
types::client_message::ClientMessage::FoundSolution(job_id, hash, nonce) => {
self.send_message_submit(job_id, hash, nonce)
},
types::client_message::ClientMessage::KeepAlive => self.send_keepalive(),
types::client_message::ClientMessage::Shutdown => {
debug!(target: LOG_TARGET_FILE, "Shutting down client controller");
return;
},
};
if let Err(e) = result {
error!(target: LOG_TARGET, "Mining Controller Error {:?}", e);
self.stream = None;
}
}
thread::sleep(Duration::from_millis(10));
} // loop
}
}

View File

@@ -0,0 +1,76 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use tari_max_size::MaxSizeBytesError;
use thiserror::Error;
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Error)]
pub enum Error {
#[error("Connection error: {0}")]
Connection(String),
#[error("Request error: {0}")]
Request(String),
// ResponseError(String),
#[error("Failed to parse JSON: {0}")]
Json(#[from] serde_json::error::Error),
#[error("Blob is not a valid hex value: {0}")]
Hex(#[from] hex::FromHexError),
#[error("System time error: {0}")]
Time(#[from] std::time::SystemTimeError),
#[error("Client Tx is not set")]
ClientTxNotSet,
#[error("Io error: {0}")]
Io(#[from] std::io::Error),
#[error("Can't create TLS connector: {0}")]
Tls(#[from] native_tls::Error),
#[error("Can't establish TLS connection: {0}")]
Tcp(#[from] Box<native_tls::HandshakeError<std::net::TcpStream>>),
#[error("No connected stream")]
NotConnected,
#[error("Can't parse int: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("General error: {0}")]
General(String),
#[error("Missing Data error: {0}")]
MissingData(String),
#[error("Limit exceeded error: {0}")]
MaxSizeBytesError(#[from] MaxSizeBytesError),
}
impl<T> From<std::sync::PoisonError<T>> for Error {
fn from(error: std::sync::PoisonError<T>) -> Self {
Error::General(format!("Failed to get lock: {:?}", error))
}
}
impl<T> From<std::sync::mpsc::SendError<T>> for Error {
fn from(error: std::sync::mpsc::SendError<T>) -> Self {
Error::General(format!("Failed to send to a channel: {:?}", error))
}
}
impl From<native_tls::HandshakeError<std::net::TcpStream>> for Error {
fn from(error: native_tls::HandshakeError<std::net::TcpStream>) -> Self {
Error::General(format!("TLS handshake error: {:?}", error))
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
pub mod controller;
pub mod error;
pub mod stratum_controller;
pub mod stratum_types;
pub mod stream;

View File

@@ -0,0 +1,221 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::{convert::TryFrom, sync::mpsc, thread, time::SystemTime};
use borsh::BorshDeserialize;
use futures::stream::StreamExt;
use log::*;
use minotari_app_grpc::tari_rpc::BlockHeader;
//use tari_common_types::types::FixedHash;
use tari_max_size::MaxSizeBytes;
use tari_utilities::{hex::Hex, ByteArray};
use crate::{
miner::Miner,
run_miner::display_report,
stratum::{error::Error, stratum_types as types},
};
pub const LOG_TARGET: &str = "minotari::miner::stratum::controller";
pub const LOG_TARGET_FILE: &str = "minotari::logging::miner::stratum::controller";
type CurrentBlob = MaxSizeBytes<{ 4 * 1024 * 1024 }>; // 4 MiB
pub struct Controller {
rx: mpsc::Receiver<types::miner_message::MinerMessage>,
pub tx: mpsc::Sender<types::miner_message::MinerMessage>,
client_tx: Option<mpsc::Sender<types::client_message::ClientMessage>>,
current_height: u64,
current_job_id: u64,
current_difficulty_target: u64,
current_blob: CurrentBlob,
current_header: Option<BlockHeader>,
keep_alive_time: SystemTime,
num_mining_threads: usize,
}
impl Controller {
pub fn new(num_mining_threads: usize) -> Result<Controller, String> {
let (tx, rx) = mpsc::channel::<types::miner_message::MinerMessage>();
Ok(Controller {
rx,
tx,
client_tx: None,
current_height: 0,
current_job_id: 0,
current_difficulty_target: 0,
current_blob: CurrentBlob::default(),
current_header: None,
keep_alive_time: SystemTime::now(),
num_mining_threads,
})
}
pub fn set_client_tx(&mut self, client_tx: mpsc::Sender<types::client_message::ClientMessage>) {
self.client_tx = Some(client_tx);
}
#[allow(clippy::too_many_lines)]
pub async fn run(&mut self) -> Result<(), Error> {
let mut miner: Option<Miner> = None;
loop {
// lets see if we need to change the state of the miner.
while let Some(message) = self.rx.try_iter().next() {
debug!(target: LOG_TARGET_FILE, "Miner received message: {:?}", message);
match message {
types::miner_message::MinerMessage::ReceivedJob(height, job_id, diff, blob) => {
match self.should_we_update_job(height, job_id, diff, CurrentBlob::try_from(blob)?) {
Ok(should_we_update) => {
if should_we_update {
/*let header = self
.current_header
.clone()
.ok_or_else(|| Error::MissingData("Header".to_string()))?;
if let Some(acive_miner) = miner.as_mut() {
acive_miner.kill_threads();
}
miner = Some(Miner::init_mining(header, self.current_difficulty_target, self.num_mining_threads, true, FixedHash::zero(), None, ));*/
} else {
continue;
}
},
Err(e) => {
debug!(
target: LOG_TARGET_FILE,
"Miner could not decipher miner message: {:?}", e
);
// lets wait a second before we try again
thread::sleep(std::time::Duration::from_millis(1000));
continue;
},
}
},
types::miner_message::MinerMessage::StopJob => {
debug!(target: LOG_TARGET_FILE, "Stopping jobs");
miner = None;
continue;
},
types::miner_message::MinerMessage::ResumeJob => {
debug!(target: LOG_TARGET_FILE, "Resuming jobs");
miner = None;
continue;
},
types::miner_message::MinerMessage::Shutdown => {
debug!(
target: LOG_TARGET_FILE,
"Stopping jobs and Shutting down mining controller"
);
miner = None;
},
};
}
let mut submit = true;
if let Some(reporter) = miner.as_mut() {
if let Some(report) = (*reporter).next().await {
if let Some(header) = report.header.clone() {
if report.difficulty < self.current_difficulty_target {
submit = false;
debug!(
target: LOG_TARGET_FILE,
"Mined difficulty {} below target difficulty {}. Not submitting.",
report.difficulty,
self.current_difficulty_target
);
}
if submit {
// Mined a block fitting the difficulty
let block_header: tari_core::blocks::BlockHeader =
tari_core::blocks::BlockHeader::try_from(header).map_err(Error::MissingData)?;
let hash = block_header.hash().to_hex();
info!(
target: LOG_TARGET,
"Miner found share with hash {}, nonce {} and difficulty {:?}",
hash,
block_header.nonce,
report.difficulty
);
debug!(
target: LOG_TARGET_FILE,
"Miner found share with hash {}, difficulty {:?} and data {:?}",
hash,
report.difficulty,
block_header
);
self.client_tx
.as_mut()
.ok_or_else(|| Error::Connection("No connection to pool".to_string()))?
.send(types::client_message::ClientMessage::FoundSolution(
self.current_job_id,
hash,
block_header.nonce,
))?;
self.keep_alive_time = SystemTime::now();
continue;
} else {
display_report(&report, self.num_mining_threads).await;
}
} else {
display_report(&report, self.num_mining_threads).await;
}
}
}
if self.keep_alive_time.elapsed()?.as_secs() >= 30 {
self.keep_alive_time = SystemTime::now();
self.client_tx
.as_mut()
.ok_or(Error::ClientTxNotSet)?
.send(types::client_message::ClientMessage::KeepAlive)?;
}
}
}
pub fn should_we_update_job(
&mut self,
height: u64,
job_id: u64,
diff: u64,
blob: CurrentBlob,
) -> Result<bool, Error> {
if height != self.current_height ||
job_id != self.current_job_id ||
diff != self.current_difficulty_target ||
blob != self.current_blob
{
self.current_height = height;
self.current_job_id = job_id;
self.current_blob = blob.clone();
self.current_difficulty_target = diff;
let mut buffer = blob.as_bytes();
let tari_header: tari_core::blocks::BlockHeader = BorshDeserialize::deserialize(&mut buffer)
.map_err(|_| Error::General("Byte Blob is not a valid header".to_string()))?;
self.current_header = Some(minotari_app_grpc::tari_rpc::BlockHeader::from(tari_header));
Ok(true)
} else {
Ok(false)
}
}
}

View File

@@ -0,0 +1,22 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
pub(crate) mod controller;

View File

@@ -0,0 +1,30 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub enum ClientMessage {
// job_id, hash, nonce
FoundSolution(u64, String, u64),
KeepAlive,
Shutdown,
}

View File

@@ -0,0 +1,31 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
use tari_core::blocks::Block;
#[derive(Serialize, Deserialize, Debug)]
pub struct Job {
pub job_id: u64,
pub block: Option<Block>,
pub target: u64,
pub height: u64,
}

View File

@@ -0,0 +1,30 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct JobParams {
pub job_id: String,
pub blob: String,
pub target: String,
pub height: u64,
}

View File

@@ -0,0 +1,34 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use derivative::Derivative;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Derivative)]
#[derivative(Debug)]
pub struct LoginParams {
pub login: String,
#[derivative(Debug = "ignore")]
#[allow(dead_code)]
#[serde(skip_serializing)]
pub pass: String,
pub agent: String,
}

View File

@@ -0,0 +1,30 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
use crate::stratum::stratum_types::job_params::JobParams;
#[derive(Serialize, Deserialize, Debug)]
pub struct LoginResponse {
pub id: String,
pub job: JobParams,
}

View File

@@ -0,0 +1,31 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub enum MinerMessage {
// Height, Id, difficulty, HeaderBlob
ReceivedJob(u64, u64, u64, Vec<u8>),
ResumeJob,
StopJob,
Shutdown,
}

View File

@@ -0,0 +1,33 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
pub(crate) mod client_message;
pub(crate) mod job;
pub(crate) mod job_params;
pub(crate) mod login_params;
pub(crate) mod login_response;
pub(crate) mod miner_message;
pub(crate) mod rpc_error;
pub(crate) mod rpc_request;
pub(crate) mod rpc_response;
pub(crate) mod submit_params;
pub(crate) mod submit_response;
pub(crate) mod worker_identifier;

View File

@@ -0,0 +1,28 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RpcError {
pub code: i32,
pub message: String,
}

View File

@@ -0,0 +1,31 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Serialize, Deserialize, Debug)]
pub struct RpcRequest {
pub id: Option<String>,
pub jsonrpc: String,
pub method: String,
pub params: Option<Value>,
}

View File

@@ -0,0 +1,32 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::stratum::stratum_types::rpc_error::RpcError;
#[derive(Serialize, Deserialize, Debug)]
pub struct RpcResponse {
pub id: String,
pub result: Option<Value>,
pub error: Option<RpcError>,
}

View File

@@ -0,0 +1,30 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct SubmitParams {
pub id: String,
pub job_id: u64,
pub nonce: u64,
pub hash: String,
}

View File

@@ -0,0 +1,30 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
use crate::stratum::stratum_types::rpc_error::RpcError;
#[derive(Serialize, Deserialize, Debug)]
pub struct SubmitResponse {
pub status: Option<String>,
pub error: Option<RpcError>,
}

View File

@@ -0,0 +1,27 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct WorkerIdentifier {
pub id: String,
}

View File

@@ -0,0 +1,107 @@
// Copyright 2024. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::{
io::{self, BufRead, Read, Write},
net::TcpStream,
};
use bufstream::BufStream;
use native_tls::{TlsConnector, TlsStream};
use crate::stratum::error::Error;
pub(crate) enum Stream {
Stream(BufStream<TcpStream>),
TlsStream(Box<BufStream<TlsStream<TcpStream>>>),
}
impl Stream {
pub fn try_connect(server_url: &str, tls: Option<bool>) -> Result<Self, Error> {
let conn = TcpStream::connect(server_url)?;
if let Some(true) = tls {
let connector = TlsConnector::new()?;
let url_port: Vec<&str> = server_url.split(':').collect();
let split_url: Vec<&str> = url_port[0].split('.').collect();
let base_host = format!("{}.{}", split_url[split_url.len() - 2], split_url[split_url.len() - 1]);
let mut stream = connector.connect(&base_host, conn).map_err(Box::new)?;
stream.get_mut().set_nonblocking(true)?;
Ok(Self::TlsStream(Box::from(BufStream::new(stream))))
} else {
conn.set_nonblocking(true)?;
Ok(Self::Stream(BufStream::new(conn)))
}
}
fn reader(&mut self) -> &mut dyn Read {
match self {
Self::TlsStream(tls_stream) => tls_stream,
Self::Stream(stream) => stream,
}
}
fn writer(&mut self) -> &mut dyn Write {
match self {
Self::TlsStream(tls_stream) => tls_stream,
Self::Stream(stream) => stream,
}
}
fn buf_reader(&mut self) -> &mut dyn BufRead {
match self {
Self::TlsStream(tls_stream) => tls_stream,
Self::Stream(stream) => stream,
}
}
}
impl Write for Stream {
fn write(&mut self, b: &[u8]) -> Result<usize, io::Error> {
self.writer().write(b)
}
fn flush(&mut self) -> Result<(), io::Error> {
self.writer().flush()
}
}
impl Read for Stream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader().read(buf)
}
}
impl BufRead for Stream {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
self.buf_reader().fill_buf()
}
fn consume(&mut self, amt: usize) {
self.buf_reader().consume(amt)
}
fn read_until(&mut self, byte: u8, buf: &mut Vec<u8>) -> io::Result<usize> {
self.buf_reader().read_until(byte, buf)
}
fn read_line(&mut self, string: &mut String) -> io::Result<usize> {
self.buf_reader().read_line(string)
}
}

View File

@@ -0,0 +1,56 @@
@echo off
title Minotari Miner
rem Verify arguments
if ["%base_path%"]==[""] (
echo Problem with "base_path" environment variable: '%base_path%'
pause
exit /b 10101
)
if not exist "%base_path%" (
echo Path as per "base_path" environment variable not found: '%base_path%'
pause
exit /b 10101
)
if ["%my_exe%"]==[""] (
echo Problem with "my_exe" environment variable: '%my_exe%'
pause
exit /b 10101
)
rem Find the miner executable
if exist "%my_exe_path%\%my_exe%" (
set miner=%my_exe_path%\%my_exe%
echo.
echo Using "%my_exe%" found in %my_exe_path%
echo.
) else (
if exist "%base_path%\%my_exe%" (
set miner=%base_path%\%my_exe%
echo.
echo Using "%my_exe%" found in base_path
echo.
) else (
set FOUND=
for %%X in (%my_exe%) do (set FOUND=%%~$PATH:X)
if defined FOUND (
set miner=%my_exe%
echo.
echo Using "%my_exe%" found in system path:
where "%my_exe%"
echo.
) else (
echo.
echo Runtime "%my_exe%" not found in %my_exe_path%, base_path or the system path
echo.
pause
exit /b 10101
)
)
)
echo.
echo.
cd "%base_path%"
"%miner%" --base-path "%base_path%"

View File

@@ -0,0 +1,28 @@
@echo off
echo.
echo Set up environment variables
echo ----------------------------
rem These are the miner executable and SQLite dynamic link library names
set my_exe=minotari_miner.exe
rem The default location for the miner executable
set my_exe_path=%~dp0
if %my_exe_path:~-1%==\ set my_exe_path=%my_exe_path:~0,-1%
echo my_exe_path = %my_exe_path%
rem The base folder where the database and log files will be located
set base_path=%~dp0..
echo base_path = %base_path%
echo.
echo Run the miner
echo ----------------------
call "%my_exe_path%\source_miner_env.bat"
goto END:
:END
echo.
pause