use std::sync::{
atomic::{AtomicU8, Ordering},
Arc,
};
use stratum_apps::{
key_utils::Secp256k1PublicKey,
stratum_core::{
binary_sv2::U256,
bitcoin::{
block::{Header, Version},
hashes::Hash,
CompactTarget, Target, TxMerkleNode,
},
channels_sv2::{
merkle_root::merkle_root_from_path,
target::{bytes_to_hex, u256_to_block_hash},
},
extensions_sv2::{UserIdentity, MAX_USER_IDENTITY_LENGTH},
sv1_api::{
client_to_server::{self, Submit},
json_rpc,
server_to_client::Notify,
utils::HexU32Be,
Message,
},
},
utils::types::{ChannelId, DownstreamId},
};
use tracing::{debug, warn};
use crate::error::TproxyErrorKind;
pub const AGGREGATED_CHANNEL_ID: ChannelId = u32::MAX;
pub fn validate_sv1_share(
share: &client_to_server::Submit<'static>,
target: Target,
extranonce1: Vec<u8>,
version_rolling_mask: Option<HexU32Be>,
job: Notify<'static>,
) -> Result<bool, TproxyErrorKind> {
let mut full_extranonce = vec![];
full_extranonce.extend_from_slice(extranonce1.as_slice());
full_extranonce.extend_from_slice(share.extra_nonce2.0.as_ref());
let share_version = share
.version_bits
.clone()
.map(|vb| vb.0)
.unwrap_or(job.version.0);
let mask = version_rolling_mask.unwrap_or(HexU32Be(0x1FFFE000_u32)).0;
let version = (job.version.0 & !mask) | (share_version & mask);
let prev_hash: U256<'static> = Vec::<u8>::from(job.prev_hash.clone())
.try_into()
.map_err(TproxyErrorKind::BinarySv2)?;
let merkle_root: [u8; 32] = merkle_root_from_path(
job.coin_base1.as_ref(),
job.coin_base2.as_ref(),
full_extranonce.as_ref(),
job.merkle_branch.as_ref(),
)
.ok_or(TproxyErrorKind::InvalidMerkleRoot)?
.try_into()
.map_err(|_| TproxyErrorKind::InvalidMerkleRoot)?;
let header = Header {
version: Version::from_consensus(version as i32),
prev_blockhash: u256_to_block_hash(prev_hash),
merkle_root: TxMerkleNode::from_byte_array(merkle_root),
time: share.time.0,
bits: CompactTarget::from_consensus(job.bits.0),
nonce: share.nonce.0,
};
let hash = header.block_hash();
let raw_hash: [u8; 32] = *hash.to_raw_hash().as_ref();
let hash_as_target = Target::from_le_bytes(raw_hash);
let hash_bytes = hash_as_target.to_be_bytes();
let target_bytes = target.to_be_bytes();
debug!(
"share validation \nshare:\t\t{}\ndownstream target:\t{}\n",
bytes_to_hex(&hash_bytes),
bytes_to_hex(&target_bytes),
);
if hash_as_target < target {
return Ok(true);
}
Ok(false)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AggregatedState {
NoChannel = 0,
Pending = 1,
Connected = 2,
}
#[derive(Clone, Debug)]
pub struct AtomicAggregatedState {
inner: Arc<AtomicU8>,
}
impl AtomicAggregatedState {
pub fn new(state: AggregatedState) -> Self {
Self {
inner: Arc::new(AtomicU8::new(state as u8)),
}
}
pub fn get(&self) -> AggregatedState {
match self.inner.load(Ordering::SeqCst) {
0 => AggregatedState::NoChannel,
1 => AggregatedState::Pending,
2 => AggregatedState::Connected,
v => panic!("Invalid UpstreamState value: {v}"),
}
}
pub fn set(&self, state: AggregatedState) {
self.inner.store(state as u8, Ordering::SeqCst);
}
}
#[derive(Debug)]
pub struct UpstreamEntry {
pub host: String,
pub port: u16,
pub authority_pubkey: Secp256k1PublicKey,
pub tried_or_flagged: bool,
pub user_identity: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TproxyMode {
Aggregated,
NonAggregated,
}
impl From<bool> for TproxyMode {
fn from(value: bool) -> Self {
if value {
return TproxyMode::Aggregated;
}
TproxyMode::NonAggregated
}
}
impl TproxyMode {
pub(crate) fn is_aggregated(self) -> bool {
TproxyMode::Aggregated == self
}
pub(crate) fn is_non_aggregated(self) -> bool {
TproxyMode::NonAggregated == self
}
}
#[derive(Debug)]
pub enum DownstreamMessages {
SubmitShares(SubmitShareWithChannelId),
OpenChannel(DownstreamId), }
#[derive(Debug, Clone)]
pub struct SubmitShareWithChannelId {
pub channel_id: ChannelId,
pub downstream_id: DownstreamId,
pub share: Submit<'static>,
pub extranonce: Vec<u8>,
pub extranonce2_len: usize,
pub version_rolling_mask: Option<HexU32Be>,
pub job_version: Option<u32>,
}
pub(crate) const KEEPALIVE_JOB_ID_DELIMITER: char = '#';
pub(crate) fn is_mining_authorize(msg: &Message) -> bool {
if let json_rpc::Message::StandardRequest(r) = &msg {
r.method == "mining.authorize"
} else {
false
}
}
pub(crate) fn sv1_worker_name_from_sv1_username(sv1_username: &str) -> &str {
sv1_username
.split_once('.')
.map(|(_, worker_name)| worker_name)
.filter(|worker_name| !worker_name.is_empty())
.unwrap_or(sv1_username)
}
pub(crate) fn aggregated_upstream_user_identity(user_identity: &str) -> String {
if user_identity.starts_with("sri/") {
user_identity.to_string()
} else if let Some((account, _)) = user_identity.split_once('.') {
format!("{account}.translator-proxy")
} else {
format!("{user_identity}.translator-proxy")
}
}
pub(crate) fn tlv_user_identity_from_sv1_worker_name(
sv1_worker_name: &str,
) -> Result<UserIdentity, TproxyErrorKind> {
let len = sv1_worker_name.len();
let tlv_user_identity = if len <= MAX_USER_IDENTITY_LENGTH {
sv1_worker_name
} else {
let mut end = MAX_USER_IDENTITY_LENGTH;
while end > 0 && !sv1_worker_name.is_char_boundary(end) {
end -= 1;
}
let tlv_user_identity = &sv1_worker_name[..end];
warn!(
"extension 0x0002 negotiated; sv1_worker_name '{}' exceeds {} bytes ({} bytes), \
truncating TLV user_identity to '{}'",
sv1_worker_name, MAX_USER_IDENTITY_LENGTH, len, tlv_user_identity
);
tlv_user_identity
};
UserIdentity::new(tlv_user_identity)
.map_err(|_| TproxyErrorKind::InvalidUserIdentity(tlv_user_identity.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derives_sv1_worker_name_from_first_dot() {
assert_eq!(
sv1_worker_name_from_sv1_username("account.worker1"),
"worker1"
);
assert_eq!(sv1_worker_name_from_sv1_username("worker1"), "worker1");
assert_eq!(sv1_worker_name_from_sv1_username("addr.rig.01"), "rig.01");
assert_eq!(sv1_worker_name_from_sv1_username("account."), "account.");
}
#[test]
fn formats_aggregated_upstream_user_identity() {
assert_eq!(
aggregated_upstream_user_identity("account.miner1"),
"account.translator-proxy"
);
assert_eq!(
aggregated_upstream_user_identity("account"),
"account.translator-proxy"
);
assert_eq!(
aggregated_upstream_user_identity("sri/solo/addr/worker"),
"sri/solo/addr/worker"
);
}
#[test]
fn truncates_tlv_user_identity_without_splitting_utf8() {
let worker_name = format!("{}é", "a".repeat(MAX_USER_IDENTITY_LENGTH - 1));
let tlv_user_identity = tlv_user_identity_from_sv1_worker_name(&worker_name)
.expect("truncated worker name should build a valid TLV UserIdentity");
let expected = "a".repeat(MAX_USER_IDENTITY_LENGTH - 1);
assert_eq!(tlv_user_identity.len(), MAX_USER_IDENTITY_LENGTH - 1);
assert_eq!(tlv_user_identity.as_str(), Some(expected.as_str()));
}
}