use bitcoin::hashes::sha256d::Hash;
use std::collections::{HashMap, HashSet};
#[derive(Debug)]
pub enum ShareValidationResult {
Valid(Hash),
BlockFound(Hash, Option<u64>, Vec<u8>),
}
#[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),
InvalidCoinbase,
NoChainTip,
SeenSharesBudgetExhausted,
}
#[derive(Clone, Debug)]
pub struct ShareAccounting {
last_share_sequence_number: u32,
shares_accepted: u32,
rejected_shares: HashMap<String, u32>,
share_work_sum: f64,
last_batch_accepted: u32,
last_batch_work_sum: f64,
batch_acknowledged: bool,
share_batch_size: usize,
seen_shares: HashSet<Hash>,
seen_shares_budget: usize,
best_diff: f64,
blocks_found: u32,
}
impl ShareAccounting {
pub fn new(share_batch_size: usize, seen_shares_budget: usize) -> Self {
Self {
last_share_sequence_number: 0,
shares_accepted: 0,
rejected_shares: HashMap::new(),
share_work_sum: 0.0,
last_batch_accepted: 0,
last_batch_work_sum: 0.0,
batch_acknowledged: false,
share_batch_size,
seen_shares: HashSet::new(),
seen_shares_budget,
best_diff: 0.0,
blocks_found: 0,
}
}
pub fn increment_rejected_shares(&mut self, error_code: &str) {
if let Some(count) = self.rejected_shares.get_mut(error_code) {
*count = count.saturating_add(1);
} else {
self.rejected_shares.insert(error_code.to_string(), 1);
}
}
pub fn update_share_accounting(
&mut self,
share_work: f64,
share_sequence_number: u32,
share_hash: Hash,
) {
self.last_share_sequence_number = share_sequence_number;
self.shares_accepted = self.shares_accepted.saturating_add(1);
self.share_work_sum += share_work;
self.seen_shares.insert(share_hash);
if self.batch_acknowledged || self.should_acknowledge() {
self.last_batch_accepted = 1;
self.last_batch_work_sum = share_work;
self.batch_acknowledged = false;
} else {
self.last_batch_accepted = self.last_batch_accepted.saturating_add(1);
self.last_batch_work_sum += share_work;
}
}
pub fn flush_seen_shares(&mut self) {
self.seen_shares.clear();
}
pub fn is_seen_shares_budget_exhausted(&self) -> bool {
self.seen_shares.len() >= self.seen_shares_budget
}
pub fn get_last_share_sequence_number(&self) -> u32 {
self.last_share_sequence_number
}
pub fn get_last_batch_accepted(&self) -> u32 {
self.last_batch_accepted
}
pub fn get_last_batch_work_sum(&self) -> u64 {
self.last_batch_work_sum as u64
}
pub fn get_shares_accepted(&self) -> u32 {
self.shares_accepted
}
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(&self) -> impl Iterator<Item = (&str, u32)> + '_ {
self.rejected_shares
.iter()
.map(|(error_code, count)| (error_code.as_str(), *count))
}
pub fn get_rejected_shares_count(&self) -> u32 {
self.rejected_shares
.values()
.copied()
.fold(0, u32::saturating_add)
}
pub fn get_share_work_sum(&self) -> f64 {
self.share_work_sum
}
pub fn get_share_batch_size(&self) -> usize {
self.share_batch_size
}
pub fn should_acknowledge(&self) -> bool {
self.last_batch_accepted == self.share_batch_size as u32
}
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 mark_batch_acknowledged(&mut self) {
self.batch_acknowledged = true;
}
pub fn get_blocks_found(&self) -> u32 {
self.blocks_found
}
}
#[cfg(test)]
mod tests {
use super::ShareAccounting;
use bitcoin::hashes::Hash as _;
#[test]
fn rejected_shares_are_tracked_by_error_code() {
let mut accounting = ShareAccounting::new(10, 1_200);
accounting.increment_rejected_shares("difficulty-too-low");
accounting.increment_rejected_shares("duplicate-share");
accounting.increment_rejected_shares("difficulty-too-low");
assert_eq!(accounting.get_rejected_shares_count(), 3);
assert_eq!(
accounting.get_rejected_shares_error_count("difficulty-too-low"),
2
);
assert_eq!(
accounting.get_rejected_shares_error_count("duplicate-share"),
1
);
}
#[test]
fn counters_saturate_at_u32_max() {
let mut accounting = ShareAccounting::new(10, 1_200);
accounting.shares_accepted = u32::MAX - 1;
accounting.blocks_found = u32::MAX;
accounting.last_batch_accepted = u32::MAX - 1;
accounting.update_share_accounting(1.0, 0, bitcoin::hashes::sha256d::Hash::all_zeros());
assert_eq!(accounting.shares_accepted, u32::MAX);
assert_eq!(accounting.last_batch_accepted, u32::MAX);
accounting.update_share_accounting(1.0, 1, bitcoin::hashes::sha256d::Hash::all_zeros());
assert_eq!(accounting.shares_accepted, u32::MAX);
assert_eq!(accounting.last_batch_accepted, 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(10, 1_200);
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 increment_rejected_shares_saturates() {
let mut accounting = ShareAccounting::new(10, 1_200);
accounting
.rejected_shares
.insert("err".to_string(), u32::MAX);
accounting.increment_rejected_shares("err");
assert_eq!(accounting.rejected_shares.get("err"), Some(&u32::MAX));
}
#[test]
fn seen_shares_budget_is_a_hard_limit() {
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 budget = 100;
let mut accounting = ShareAccounting::new(10, budget);
for i in 0..budget as u32 {
assert!(!accounting.is_seen_shares_budget_exhausted());
accounting.update_share_accounting(1.0, i, hash(i));
}
assert!(accounting.is_seen_shares_budget_exhausted());
for i in 0..budget as u32 {
assert!(accounting.is_share_seen(hash(i)));
}
accounting.flush_seen_shares();
assert!(!accounting.is_seen_shares_budget_exhausted());
assert!(!accounting.is_share_seen(hash(0)));
}
}