channels_sv2 3.0.0

Sv2 Channel Primitives
Documentation
//! Share Validation - Mining Server Abstraction.
//!
//! This module provides types and logic for validating mining shares and tracking share accounting
//! state on a mining server. It is used to determine the outcome of submitted shares, track
//! duplicate submissions, maintain batch acknowledgment state, and compute share statistics for
//! downstream Stratum V2 (SV2) messaging.
//!
//! ## Responsibilities
//!
//! - **Share Validation Result**: Encapsulates the result of validating a mining share, including
//!   success, batch acknowledgment, and block discovery.
//! - **Share Validation Error**: Enumerates possible failure reasons when validating a share.
//! - **Share Accounting**: Tracks per-channel share statistics, acknowledges batches, detects
//!   duplicate shares, and maintains best difficulty found.
//!
//! ## Usage
//!
//! Intended for use within mining server implementations that process SV2 share submissions and
//! issue `SubmitShares.Success` messages. Not intended for use by mining clients.

use bitcoin::hashes::sha256d::Hash;
use std::collections::HashSet;

/// The outcome of share validation, from the perspective of a Mining Server.
///
/// The [`ShareValidationResult::Valid`] variant carries the hash of the accepted share.
///
/// The [`ShareValidationResult::BlockFound`] variant carries:
/// - `share_hash`: The hash of the share that solved the block.
/// - `template_id`: The template ID associated with the job (as `Option<u64>`), or `None` for custom jobs.
/// - `coinbase`: The serialized coinbase transaction for the block (as `Vec<u8>`).
#[derive(Debug)]
pub enum ShareValidationResult {
    /// The share is valid and accepted.
    Valid(Hash),
    /// The share solves a block.
    /// Contains:
    /// - `share_hash`: The hash of the share that solved the block.
    /// - `template_id`: The template ID associated with the job, or `None` for custom jobs.
    /// - `coinbase`: The serialized coinbase transaction for the block.
    BlockFound(Hash, Option<u64>, Vec<u8>),
}

/// The error variants that can occur during share validation.
#[derive(Debug)]
pub enum ShareValidationError {
    /// The share is invalid for unspecified reasons.
    Invalid,
    /// The share is stale due to chain tip changes.
    Stale,
    /// The submitted job ID does not refer to any known job for this channel.
    InvalidJobId,
    /// The share does not meet the required target difficulty.
    DoesNotMeetTarget,
    /// The submitted share attempts version rolling when not allowed.
    VersionRollingNotAllowed,
    /// The share is a duplicate of a previously accepted share.
    DuplicateShare,
    /// The coinbase transaction was invalid or malformed.
    InvalidCoinbase,
    /// No chain tip is set for the channel (required for share validation).
    NoChainTip,
    /// The share extranonce size is different from the channel's rollable extranonce size.
    BadExtranonceSize,
}

/// The state of share validation in the context of some specific channel (either Extended or
/// Standard).
///
/// This struct manages per-channel share statistics, batch acknowledgment, duplicate detection,
/// and difficulty tracking. Only meant for usage on Mining Servers.
#[derive(Clone, Debug)]
pub struct ShareAccounting {
    last_share_sequence_number: u32,
    shares_accepted: u32,
    share_work_sum: f64,
    last_batch_accepted: u32,
    last_batch_work_sum: f64,
    share_batch_size: usize,
    seen_shares: HashSet<Hash>,
    best_diff: f64,
}

impl ShareAccounting {
    /// Constructs a new `ShareAccounting` instance for a channel.
    ///
    /// `share_batch_size` controls how many accepted shares trigger a batch acknowledgment.
    pub fn new(share_batch_size: usize) -> Self {
        Self {
            last_share_sequence_number: 0,
            shares_accepted: 0,
            share_work_sum: 0.0,
            last_batch_accepted: 0,
            last_batch_work_sum: 0.0,
            share_batch_size,
            seen_shares: HashSet::new(),
            best_diff: 0.0,
        }
    }

    /// Updates internal accounting for a newly accepted share.
    ///
    /// - Increments total shares accepted and work sum.
    /// - Increments last batch accepted and work sum if the share batch size is reached.
    /// - Updates last accepted sequence number.
    /// - Records the share hash to detect duplicates.
    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 += 1;
        self.share_work_sum += share_work;
        self.seen_shares.insert(share_hash);

        if self.should_acknowledge() {
            let current_batch_accepted = self.shares_accepted - self.last_batch_accepted;
            let current_batch_work_sum = self.share_work_sum - self.last_batch_work_sum;
            self.last_batch_accepted = current_batch_accepted;
            self.last_batch_work_sum = current_batch_work_sum;
        }
    }

    /// Clears the set of seen share hashes.
    ///
    /// Should be called on every chain tip update to avoid unbounded growth of memory
    /// and allow new shares for the new tip.
    pub fn flush_seen_shares(&mut self) {
        self.seen_shares.clear();
    }

    /// Returns the sequence number of the last accepted share.
    pub fn get_last_share_sequence_number(&self) -> u32 {
        self.last_share_sequence_number
    }

    /// Returns the number of shares accepted in the last batch.
    pub fn get_last_batch_accepted(&self) -> u32 {
        self.last_batch_accepted
    }

    /// Returns the sum of work contributed by shares in the last batch.
    pub fn get_last_batch_work_sum(&self) -> f64 {
        self.last_batch_work_sum
    }

    /// Returns the total number of shares accepted on this channel.
    ///
    /// Note: this is not what we use for `SubmitShares.Success` messages.
    /// Instead, there we should use `get_last_batch_accepted()`.
    pub fn get_shares_accepted(&self) -> u32 {
        self.shares_accepted
    }

    /// Returns the sum of work contributed by all accepted shares.
    ///
    /// Note: this is not what we use for `SubmitShares.Success` messages.
    /// Instead, there we should use `get_last_batch_work_sum()`.
    pub fn get_share_work_sum(&self) -> f64 {
        self.share_work_sum
    }

    /// Returns the configured batch size for share acknowledgments.
    pub fn get_share_batch_size(&self) -> usize {
        self.share_batch_size
    }

    /// Returns true if the current count of accepted shares triggers an acknowledgment.
    pub fn should_acknowledge(&self) -> bool {
        self.shares_accepted % self.share_batch_size as u32 == 0
    }

    /// Checks if the share hash has already been accepted (duplicate detection).
    pub fn is_share_seen(&self, share_hash: Hash) -> bool {
        self.seen_shares.contains(&share_hash)
    }

    /// Returns the highest difficulty found among accepted shares.
    pub fn get_best_diff(&self) -> f64 {
        self.best_diff
    }

    /// Updates the best difficulty if the new value is higher.
    pub fn update_best_diff(&mut self, diff: f64) {
        if diff > self.best_diff {
            self.best_diff = diff;
        }
    }
}