extern crate alloc;
use super::{HashMap, MAX_SEEN_SHARES};
use alloc::{collections::VecDeque, string::String};
use bitcoin::hashes::sha256d::Hash;
use mining_sv2::{
ERROR_CODE_SUBMIT_SHARES_BAD_EXTRANONCE_SIZE, ERROR_CODE_SUBMIT_SHARES_DIFFICULTY_TOO_LOW,
ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE, ERROR_CODE_SUBMIT_SHARES_INVALID_CHANNEL_ID,
ERROR_CODE_SUBMIT_SHARES_INVALID_JOB_ID,
ERROR_CODE_SUBMIT_SHARES_INVALID_NON_ROLLABLE_VERSION_BIT,
ERROR_CODE_SUBMIT_SHARES_INVALID_SHARE, ERROR_CODE_SUBMIT_SHARES_STALE_SHARE,
ERROR_CODE_VERSION_ROLLING_NOT_ALLOWED,
};
pub const UNKNOWN_ERROR_CODE: &str = "unknown";
const KNOWN_ERROR_CODES: [&str; 9] = [
ERROR_CODE_SUBMIT_SHARES_INVALID_CHANNEL_ID,
ERROR_CODE_SUBMIT_SHARES_INVALID_SHARE,
ERROR_CODE_SUBMIT_SHARES_STALE_SHARE,
ERROR_CODE_SUBMIT_SHARES_INVALID_JOB_ID,
ERROR_CODE_SUBMIT_SHARES_DIFFICULTY_TOO_LOW,
ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE,
ERROR_CODE_SUBMIT_SHARES_BAD_EXTRANONCE_SIZE,
ERROR_CODE_VERSION_ROLLING_NOT_ALLOWED,
ERROR_CODE_SUBMIT_SHARES_INVALID_NON_ROLLABLE_VERSION_BIT,
];
#[derive(Debug)]
pub enum ShareValidationResult {
Valid(Hash),
BlockFound(Hash),
}
#[derive(Debug)]
pub enum ShareValidationError {
Invalid(&'static str),
Stale(&'static str),
InvalidJobId(&'static str),
DoesNotMeetTarget(&'static str),
VersionRollingNotAllowed(&'static str),
DuplicateShare(&'static str),
BadExtranonceSize(&'static str),
NoChainTip,
}
#[derive(Clone, Debug)]
pub struct ShareAccounting {
last_share_sequence_number: u32,
acknowledged_shares: u32,
acknowledged_work_sum: u64,
validated_shares: u32,
validated_work_sum: f64,
rejected_shares: HashMap<String, u32>, seen_shares: VecDeque<Hash>,
best_diff: f64,
blocks_found: u32,
}
impl Default for ShareAccounting {
fn default() -> Self {
Self::new()
}
}
impl ShareAccounting {
pub fn new() -> Self {
Self {
last_share_sequence_number: 0,
acknowledged_shares: 0,
acknowledged_work_sum: 0,
validated_shares: 0,
validated_work_sum: 0.0,
rejected_shares: HashMap::new(),
seen_shares: VecDeque::new(),
best_diff: 0.0,
blocks_found: 0,
}
}
pub fn on_share_acknowledgement(
&mut self,
new_submits_accepted_count: u32,
new_shares_sum: u64,
) {
self.acknowledged_shares = self
.acknowledged_shares
.saturating_add(new_submits_accepted_count);
self.acknowledged_work_sum = self.acknowledged_work_sum.saturating_add(new_shares_sum);
}
pub fn on_share_rejection(&mut self, error_code: &str) {
let key = if KNOWN_ERROR_CODES.contains(&error_code) {
error_code
} else {
UNKNOWN_ERROR_CODE
};
if let Some(count) = self.rejected_shares.get_mut(key) {
*count = count.saturating_add(1);
} else {
self.rejected_shares.insert(String::from(key), 1);
}
}
pub fn track_validated_share(
&mut self,
share_sequence_number: u32,
share_hash: Hash,
share_work: f64,
) {
self.last_share_sequence_number = share_sequence_number;
self.validated_shares = self.validated_shares.saturating_add(1);
self.validated_work_sum += share_work;
if !self.seen_shares.contains(&share_hash) {
if self.seen_shares.len() == MAX_SEEN_SHARES {
self.seen_shares.pop_front();
}
self.seen_shares.push_back(share_hash);
}
}
pub fn flush_seen_shares(&mut self) {
self.seen_shares.clear();
}
pub fn get_last_share_sequence_number(&self) -> u32 {
self.last_share_sequence_number
}
pub fn get_acknowledged_shares(&self) -> u32 {
self.acknowledged_shares
}
pub fn get_validated_shares(&self) -> u32 {
self.validated_shares
}
pub fn get_rejected_shares_error_count(&self, error_code: &str) -> u32 {
self.rejected_shares.get(error_code).copied().unwrap_or(0)
}
pub fn get_rejected_shares_count(&self) -> u32 {
self.rejected_shares
.values()
.copied()
.fold(0, u32::saturating_add)
}
pub fn get_rejected_shares(&self) -> impl Iterator<Item = (&str, u32)> + '_ {
self.rejected_shares
.iter()
.map(|(error_code, count)| (error_code.as_str(), *count))
}
pub fn get_acknowledged_work_sum(&self) -> u64 {
self.acknowledged_work_sum
}
pub fn get_validated_work_sum(&self) -> f64 {
self.validated_work_sum
}
pub fn is_share_seen(&self, share_hash: Hash) -> bool {
self.seen_shares.contains(&share_hash)
}
pub fn get_best_diff(&self) -> f64 {
self.best_diff
}
pub fn update_best_diff(&mut self, diff: f64) {
if diff > self.best_diff {
self.best_diff = diff;
}
}
pub fn increment_blocks_found(&mut self) {
self.blocks_found = self.blocks_found.saturating_add(1);
}
pub fn get_blocks_found(&self) -> u32 {
self.blocks_found
}
}
#[cfg(test)]
mod tests {
use super::{alloc::format, ShareAccounting, MAX_SEEN_SHARES, UNKNOWN_ERROR_CODE};
use bitcoin::hashes::Hash as _;
#[test]
fn counters_saturate_at_u32_max() {
let mut accounting = ShareAccounting::new();
accounting.validated_shares = u32::MAX;
accounting.acknowledged_shares = u32::MAX - 1;
accounting.blocks_found = u32::MAX;
accounting.track_validated_share(0, bitcoin::hashes::sha256d::Hash::all_zeros(), 1.0);
assert_eq!(accounting.validated_shares, u32::MAX);
accounting.track_validated_share(1, bitcoin::hashes::sha256d::Hash::all_zeros(), 1.0);
assert_eq!(accounting.validated_shares, u32::MAX);
accounting.on_share_acknowledgement(1, 0);
assert_eq!(accounting.acknowledged_shares, u32::MAX);
accounting.on_share_acknowledgement(1, 0);
assert_eq!(accounting.acknowledged_shares, u32::MAX);
accounting.increment_blocks_found();
assert_eq!(accounting.blocks_found, u32::MAX);
accounting.increment_blocks_found();
assert_eq!(accounting.blocks_found, u32::MAX);
}
#[test]
fn rejected_shares_count_saturates() {
let mut accounting = ShareAccounting::new();
accounting.rejected_shares.insert("a".to_string(), u32::MAX);
accounting.rejected_shares.insert("b".to_string(), 1);
assert_eq!(accounting.get_rejected_shares_count(), u32::MAX);
}
#[test]
fn on_share_rejection_saturates() {
let mut accounting = ShareAccounting::new();
accounting
.rejected_shares
.insert("difficulty-too-low".to_string(), u32::MAX);
accounting.on_share_rejection("difficulty-too-low");
assert_eq!(
accounting.rejected_shares.get("difficulty-too-low"),
Some(&u32::MAX)
);
}
#[test]
fn unknown_error_codes_are_bounded() {
let mut accounting = ShareAccounting::new();
for i in 0..10_000 {
accounting.on_share_rejection(&format!("attacker-controlled-garbage-{i}"));
}
accounting.on_share_rejection("difficulty-too-low");
accounting.on_share_rejection("difficulty-too-low");
assert_eq!(accounting.get_rejected_shares().count(), 2);
assert_eq!(
accounting.get_rejected_shares_error_count(UNKNOWN_ERROR_CODE),
10_000
);
assert_eq!(
accounting.get_rejected_shares_error_count("difficulty-too-low"),
2
);
assert_eq!(accounting.get_rejected_shares_count(), 10_002);
}
#[test]
fn seen_shares_are_bounded_by_fifo_eviction() {
fn hash(i: u32) -> bitcoin::hashes::sha256d::Hash {
let mut bytes = [0u8; 32];
bytes[..4].copy_from_slice(&i.to_le_bytes());
<bitcoin::hashes::sha256d::Hash as bitcoin::hashes::Hash>::from_slice(&bytes).unwrap()
}
let cap = MAX_SEEN_SHARES as u32;
let overflow = 100;
let mut accounting = ShareAccounting::new();
for i in 0..cap + overflow {
accounting.track_validated_share(i, hash(i), 1.0);
}
assert_eq!(accounting.seen_shares.len(), cap as usize);
for i in 0..overflow {
assert!(!accounting.is_share_seen(hash(i)));
}
for i in overflow..cap + overflow {
assert!(accounting.is_share_seen(hash(i)));
}
accounting.flush_seen_shares();
assert_eq!(accounting.seen_shares.len(), 0);
}
}