diff --git a/pool-apps/pool/Cargo.toml b/pool-apps/pool/Cargo.toml index b0cf7ab18..9258fadbe 100644 --- a/pool-apps/pool/Cargo.toml +++ b/pool-apps/pool/Cargo.toml @@ -19,6 +19,8 @@ path = "src/lib/mod.rs" [dependencies] stratum-apps = { version = "0.8.0", path = "../../stratum-apps", features = ["pool"] } +braidpool-common = { path = "../braidpool-common" } +braidpool-template-provider = { path = "../braidpool-template-provider" } async-channel = "1.5.1" serde = { version = "1.0.89", features = ["derive", "alloc"], default-features = false } tokio = { version = "1.44.1", features = ["full"] } diff --git a/pool-apps/pool/src/lib/braidpool/mod.rs b/pool-apps/pool/src/lib/braidpool/mod.rs new file mode 100644 index 000000000..280546f13 --- /dev/null +++ b/pool-apps/pool/src/lib/braidpool/mod.rs @@ -0,0 +1,117 @@ +//! Braidpool-specific types bridging the SV2 pool to the braidpool node. +//! +//! This module is the boundary layer between the generic SV2 pool and +//! Braidpool's bead-based consensus. It defines the types that flow from +//! the pool (where shares are validated) to the node (where beads are built). +//! +//! # Data flow +//! ```text +//! SubmitSharesExtended +//! → validate_share (ExtendedChannel) +//! → ValidatedShare ──[ShareBridgeSender]──► node::propagate_valid_bead (PR 4) +//! ``` + +use tokio::sync::mpsc; + +/// A share that passed pool-level PoW validation, ready for bead construction. +/// +/// `extranonce1` is `Vec` rather than `u64` because in audit mode the +/// pool-assigned prefix is `N + 7` bytes (upstream extranonce1 + 2-byte miner +/// prefix + 5-byte commitment), which can exceed 8 bytes for common 8-byte +/// upstream configurations. +#[derive(Debug, Clone)] +pub struct ValidatedShare { + /// Template the share was mined against. `None` for custom-job shares. + pub template_id: Option, + /// SV2 extended channel ID the share arrived on. + pub channel_id: u32, + /// Pool-assigned extranonce prefix (extranonce1 in Stratum v1 terms). + pub extranonce1: Vec, + /// Miner-rolled extranonce suffix (extranonce2). + pub extranonce2: Vec, + /// Block version field (may include BIP-320 rolled bits). + pub version: u32, + /// Block header nTime. + pub ntime: u32, + /// Block header nonce. + pub nonce: u32, +} + +/// Template context attached to a validated share for bead metadata construction. +/// +/// Carries the information needed to build `CommittedMetadata` in the node. +/// Populated when a `ValidatedShare` is matched to a template. +#[derive(Debug)] +pub struct BeadContext { + /// Template that was being mined when the share arrived. + pub template_id: u64, + /// Wall-clock time the template was received from the template provider. + /// Used to measure share propagation latency for bead timing. + pub received_at: std::time::Instant, + /// Committed transactions from the template (for CommittedMetadata). + pub transactions: Vec>, +} + +/// Extranonce allocation strategy for the pool↔tproxy Extended Channel. +/// +/// Determines the split between pool-assigned prefix (extranonce1) and +/// miner-rolled space (extranonce2) based on the upstream pool configuration. +/// +/// # Normal mode (4+8) +/// ```text +/// upstream negotiates: ext1=4, ext2=8 +/// pool sends to tproxy: prefix=4 bytes (random), extranonce_size=12 +/// ``` +/// +/// # Audit mode +/// Braidpool embeds commitment bytes inside the extranonce space it sends to +/// downstream miners, then reconstructs the upstream-agreed format before +/// submitting shares back upstream. +/// +/// ```text +/// upstream negotiates: ext1=N, ext2=8 (common: N=4 or N=8) +/// pool prefix sent to tproxy: N + 7 bytes (N upstream + 2 miner prefix + 5 commitment) +/// miner rolling space: total - (N+7) bytes +/// on share submission upstream: reconstruct ext1=N, ext2=8 +/// ``` +#[derive(Debug, Clone)] +pub enum ExtraNonceConfig { + /// Standard mode: 4-byte pool prefix, up to 12-byte miner rolling space. + Normal, + /// Audit mode: prefix embeds upstream extranonce1 + commitment bytes. + Audit { + /// Size (bytes) of extranonce1 as negotiated with the upstream pool. + upstream_ext1_size: usize, + }, +} + +impl ExtraNonceConfig { + /// Pool-assigned prefix size (extranonce1) for this configuration. + pub fn extranonce1_size(&self) -> usize { + match self { + Self::Normal => 4, + // N bytes upstream + 2 miner prefix + 5 commitment + Self::Audit { upstream_ext1_size } => upstream_ext1_size + 7, + } + } + + /// Miner-controlled rolling space size (extranonce2) for this configuration, + /// given `total_extranonce_bytes` agreed with the upstream pool. + pub fn extranonce2_size(&self, total_extranonce_bytes: usize) -> usize { + total_extranonce_bytes.saturating_sub(self.extranonce1_size()) + } +} + +/// Sender half of the share bridge — held by the pool's `ChannelManager`. +pub type ShareBridgeSender = mpsc::UnboundedSender; + +/// Receiver half of the share bridge — consumed by the node IPC task (PR 4). +pub type ShareBridgeReceiver = mpsc::UnboundedReceiver; + +/// Create the share bridge channel connecting pool validation to node bead construction. +/// +/// The sender is stored on `ChannelManager`; the receiver is kept alive in +/// `PoolRuntime` until PR 4 wires it to `propagate_valid_bead` over IPC. +pub fn create_share_bridge() -> (ShareBridgeSender, ShareBridgeReceiver) { + mpsc::unbounded_channel() +} diff --git a/pool-apps/pool/src/lib/channel_manager/mining_message_handler.rs b/pool-apps/pool/src/lib/channel_manager/mining_message_handler.rs index 40eccda2d..d875c6614 100644 --- a/pool-apps/pool/src/lib/channel_manager/mining_message_handler.rs +++ b/pool-apps/pool/src/lib/channel_manager/mining_message_handler.rs @@ -19,6 +19,7 @@ use stratum_apps::stratum_core::{ server::{ error::{ExtendedChannelError, StandardChannelError}, extended::ExtendedChannel, + jobs::JobOrigin, share_accounting::{ShareValidationError, ShareValidationResult}, standard::StandardChannel, }, @@ -36,6 +37,7 @@ use tracing::{error, info, warn}; use jd_server_sv2::job_declarator::SetCustomMiningJobResponse; use crate::{ + braidpool::ValidatedShare, channel_manager::{CLIENT_SEARCH_SPACE_BYTES, ChannelManager, RouteMessageTo}, error::{self, PoolError, PoolErrorKind}, utils::{PayoutMode, PayoutModeError, create_close_channel_msg}, @@ -943,6 +945,9 @@ impl HandleMiningMessagesFromClientOwnedAsync for ChannelManager { let channel_id = msg.channel_id; let vardiff_key = (downstream_id, channel_id).into(); + // Clone before the immutable borrow below so the sender can be moved + // into the closure without holding a reference to self. + let share_bridge = self.share_bridge_sender.clone(); let messages = self.with_registered_downstream(downstream_id, |downstream| { let messages = if !downstream.extended_channels.contains_key(&channel_id) { let error = SubmitSharesErrorOwned { @@ -1001,6 +1006,26 @@ impl HandleMiningMessagesFromClientOwnedAsync for ChannelManager { downstream_id, channel_id, msg.sequence_number, share_hash, share_work ); } + if let Some(sender) = &share_bridge { + let template_id = extended_channel + .get_active_job() + .and_then(|job| match job.get_origin() { + JobOrigin::NewTemplate(t) => Some(t.template_id), + _ => None, + }); + let validated = ValidatedShare { + template_id, + channel_id, + extranonce1: extended_channel.get_extranonce_prefix().to_vec(), + extranonce2: msg.extranonce.as_bytes().to_vec(), + version: msg.version, + ntime: msg.ntime, + nonce: msg.nonce, + }; + if sender.send(validated).is_err() { + warn!("share bridge closed — validated share dropped"); + } + } } Ok(ShareValidationResult::BlockFound( share_hash, @@ -1008,6 +1033,20 @@ impl HandleMiningMessagesFromClientOwnedAsync for ChannelManager { coinbase, )) => { info!("SubmitSharesExtended: 💰 Block Found!!! 💰{share_hash}"); + if let Some(sender) = &share_bridge { + let validated = ValidatedShare { + template_id, + channel_id, + extranonce1: extended_channel.get_extranonce_prefix().to_vec(), + extranonce2: msg.extranonce.as_bytes().to_vec(), + version: msg.version, + ntime: msg.ntime, + nonce: msg.nonce, + }; + if sender.send(validated).is_err() { + warn!("share bridge closed — block-found share dropped"); + } + } if let Some(template_id) = template_id { info!("SubmitSharesExtended: Propagating solution to the Template Provider."); let solution = SubmitSolutionOwned { diff --git a/pool-apps/pool/src/lib/channel_manager/mod.rs b/pool-apps/pool/src/lib/channel_manager/mod.rs index 5e9ad4cff..bb162bbfb 100644 --- a/pool-apps/pool/src/lib/channel_manager/mod.rs +++ b/pool-apps/pool/src/lib/channel_manager/mod.rs @@ -118,6 +118,10 @@ pub struct ChannelManager { required_extensions: Vec, /// Embedded Job Declaration engine (present when `[jds]` config is set). job_declarator: Option, + /// Sender half of the Braidpool share bridge. Present when the pool runs in + /// Braidpool mode; `None` in standalone SRI mode. Every validated share is + /// forwarded here for bead construction in the node (wired by PR 4). + share_bridge_sender: Option, } #[cfg_attr(not(test), hotpath::measure_all)] @@ -166,6 +170,7 @@ impl ChannelManager { downstream_receiver: Receiver<(DownstreamId, MiningOwned, Option>)>, coinbase_outputs: Vec, job_declarator: Option, + share_bridge_sender: Option, ) -> PoolResult { // Simulating a scenario where there are multiple mining servers, // `server_id` is used as `local_prefix_bytes` so each pool instance @@ -199,6 +204,7 @@ impl ChannelManager { supported_extensions: config.supported_extensions().to_vec(), required_extensions: config.required_extensions().to_vec(), job_declarator, + share_bridge_sender, }; Ok(channel_manager) diff --git a/pool-apps/pool/src/lib/config.rs b/pool-apps/pool/src/lib/config.rs index 4322075c9..00afbd493 100644 --- a/pool-apps/pool/src/lib/config.rs +++ b/pool-apps/pool/src/lib/config.rs @@ -14,11 +14,14 @@ use std::{ }; pub use jd_server_sv2::config::{JDSConfig, JDSPartialConfig}; -use stratum_apps::{ - config_helpers::{CoinbaseRewardScript, opt_path_from_toml}, +pub use stratum_apps::{ + config_helpers::CoinbaseRewardScript, key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}, - stratum_core::bitcoin::{Amount, TxOut}, tp_type::TemplateProviderType, +}; +use stratum_apps::{ + config_helpers::opt_path_from_toml, + stratum_core::bitcoin::{Amount, TxOut}, utils::types::{SharesBatchSize, SharesPerMinute}, }; @@ -93,6 +96,34 @@ impl PoolConfig { } } + /// Builds a [`PoolConfig`] for Braidpool's in-process pool mode. + /// + /// Uses `BraidpoolIpc` as the template provider (no external TDP address needed — + /// the template channel is wired at runtime via [`PoolSv2::with_braidpool_template_rx`]). + /// Sensible defaults are applied: 1-hour cert validity, 20 shares/min target, + /// batch size 100, no JDS, no monitoring. + pub fn for_braidpool( + listen_address: SocketAddr, + authority_public_key: Secp256k1PublicKey, + authority_secret_key: Secp256k1SecretKey, + coinbase_reward_script: CoinbaseRewardScript, + ) -> Self { + Self::new( + ConnectionConfig::new(listen_address, 3600, "braidpool".to_string()), + TemplateProviderType::BraidpoolIpc, + AuthorityConfig::new(authority_public_key, authority_secret_key), + coinbase_reward_script, + 20.0, + 100, + 0, + vec![], + vec![], + None, + None, + None, + ) + } + /// Returns the coinbase output. pub fn coinbase_reward_script(&self) -> &CoinbaseRewardScript { &self.coinbase_reward_script diff --git a/pool-apps/pool/src/lib/mod.rs b/pool-apps/pool/src/lib/mod.rs index 0eb06bbd5..4c7d16d76 100644 --- a/pool-apps/pool/src/lib/mod.rs +++ b/pool-apps/pool/src/lib/mod.rs @@ -1,5 +1,7 @@ use error::PoolErrorKind; use pool_runtime::{Init, PoolRuntime}; + +pub mod braidpool; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -8,7 +10,9 @@ use stratum_apps::bitcoin_core_sv2::CancellationToken; use tokio::sync::Notify; use tracing::info; -use crate::config::PoolConfig; +use braidpool_common::template::BraidpoolTemplate; + +use crate::{braidpool::ShareBridgeReceiver, config::PoolConfig}; pub mod channel_manager; pub mod config; @@ -27,6 +31,10 @@ pub struct PoolSv2 { cancellation_token: CancellationToken, shutdown_notify: Arc, is_alive: Arc, + /// In-process template channel for `BraidpoolIpc` mode. + /// Wrapped in `Arc>>` so `PoolSv2` remains `Clone`. + braidpool_template_rx: + Arc>>>, } #[cfg_attr(not(test), hotpath::measure_all)] @@ -37,9 +45,35 @@ impl PoolSv2 { cancellation_token: CancellationToken::new(), shutdown_notify: Arc::new(Notify::new()), is_alive: Arc::new(AtomicBool::new(true)), + braidpool_template_rx: Arc::new(std::sync::Mutex::new(None)), } } + /// Wires the in-process template channel for `BraidpoolIpc` mode. + /// + /// Must be called before [`start`] when `template_provider_type = BraidpoolIpc`. + /// The receiver is consumed once during bootstrap; calling `start` a second + /// time after the channel has been taken will fail with a configuration error. + pub fn with_braidpool_template_rx( + self, + rx: tokio::sync::mpsc::Receiver, + ) -> Self { + self.braidpool_template_rx + .lock() + .expect("braidpool_template_rx mutex poisoned") + .replace(rx); + self + } + + pub(crate) fn take_braidpool_template_rx( + &self, + ) -> Option> { + self.braidpool_template_rx + .lock() + .expect("braidpool_template_rx mutex poisoned") + .take() + } + /// Starts the Pool server and blocks asynchronously on the `PoolRuntime`. /// /// The startup and execution sequence follows: @@ -53,7 +87,15 @@ impl PoolSv2 { /// /// If any error occurs during bootstrapping, `start` receives the partially initialized /// runtime, gracefully shuts it down, and then returns the error. - pub async fn start(&self) -> Result<(), PoolErrorKind> { + /// + /// When `share_rx_notify` is `Some`, the [`ShareBridgeReceiver`] is sent over it once + /// the channel manager is bootstrapped and the pool is ready to process shares. The + /// caller can then forward validated shares to `propagate_valid_bead` without waiting + /// for the pool to shut down. + pub async fn start( + &self, + share_rx_notify: Option>, + ) -> Result<(), PoolErrorKind> { let runtime = match PoolRuntime::::new(self.clone()) { Ok(runtime) => runtime, Err(err) => { @@ -62,7 +104,7 @@ impl PoolSv2 { } }; - let runtime = match runtime.bootstrap().await { + let mut runtime = match runtime.bootstrap().await { Ok(runtime) => runtime, Err(err) => { let (err, runtime) = err.into_parts(); @@ -71,6 +113,14 @@ impl PoolSv2 { } }; + // Deliver the share bridge receiver to the caller before blocking on shutdown, + // so they can start consuming shares without waiting for the pool to exit. + if let Some(tx) = share_rx_notify { + if let Some(rx) = runtime.take_share_bridge_receiver() { + let _ = tx.send(rx); + } + } + runtime.wait_for_shutdown().await; runtime.shutdown().await; diff --git a/pool-apps/pool/src/lib/pool_runtime.rs b/pool-apps/pool/src/lib/pool_runtime.rs index 84a4f9b66..7028864e2 100644 --- a/pool-apps/pool/src/lib/pool_runtime.rs +++ b/pool-apps/pool/src/lib/pool_runtime.rs @@ -26,7 +26,10 @@ use jd_server_sv2::job_declarator::{ }; use super::PoolSv2; +use braidpool_common::template::BraidpoolTemplate; + use crate::{ + braidpool::{ShareBridgeReceiver, create_share_bridge}, channel_manager::ChannelManager, error::PoolErrorKind, template_receiver::{ @@ -105,6 +108,13 @@ pub(super) struct PoolRuntime { bitcoin_core_sv2: Option, encoded_outputs: Vec, coinbase_outputs: Vec, + /// Receiver half of the Braidpool share bridge. Created in + /// `bootstrap_channel_manager` and delivered to the node via + /// `PoolSv2::start`'s `share_rx_notify` oneshot. + share_bridge_receiver: Option, + /// In-process template channel for `BraidpoolIpc` mode. Taken from + /// `PoolSv2` during `new()` and consumed in `bootstrap_template_provider`. + braidpool_template_rx: Option>, } impl PoolRuntime { @@ -117,6 +127,8 @@ impl PoolRuntime { bitcoin_core_sv2: self.bitcoin_core_sv2, encoded_outputs: self.encoded_outputs, coinbase_outputs: self.coinbase_outputs, + share_bridge_receiver: self.share_bridge_receiver, + braidpool_template_rx: self.braidpool_template_rx, } } @@ -183,6 +195,8 @@ impl PoolRuntime { .consensus_encode(&mut encoded_outputs) .map_err(|err| PoolErrorKind::Io(err.into()))?; + let braidpool_template_rx = pool.take_braidpool_template_rx(); + Ok(PoolRuntime { pool, task_manager: Arc::new(TaskManager::new()), @@ -191,6 +205,8 @@ impl PoolRuntime { bitcoin_core_sv2: None, coinbase_outputs, encoded_outputs, + share_bridge_receiver: None, + braidpool_template_rx, }) } @@ -219,6 +235,8 @@ impl PoolRuntime { coinbase_outputs: self.coinbase_outputs, encoded_outputs: self.encoded_outputs, state: IoReady { io }, + share_bridge_receiver: self.share_bridge_receiver, + braidpool_template_rx: self.braidpool_template_rx, } } @@ -276,11 +294,12 @@ impl PoolRuntime { Err(err) => return Err((PoolErrorKind::Jds(err), self)), } } - TemplateProviderType::Sv2Tp { .. } => { + TemplateProviderType::Sv2Tp { .. } + | TemplateProviderType::BraidpoolIpc => { return Err(( PoolErrorKind::Configuration( "[jds] requires template_provider_type = BitcoinCoreIpc \ - (JDS needs direct IPC access to Bitcoin Core)" + (JDS needs direct IPC access to Bitcoin Core)" .to_string(), ), self, @@ -357,18 +376,55 @@ impl PoolRuntime { coinbase_outputs: self.coinbase_outputs, encoded_outputs: self.encoded_outputs, state: new_state, + share_bridge_receiver: self.share_bridge_receiver, + braidpool_template_rx: self.braidpool_template_rx, }) } } impl PoolRuntime { async fn bootstrap_template_provider( - self, + mut self, ) -> Result, (PoolErrorKind, PoolRuntime)> { let cancellation_token = self.pool.cancellation_token.clone(); let mut bitcoin_core_sv2: Option = None; match self.pool.config.template_provider_type().clone() { + TemplateProviderType::BraidpoolIpc => { + let template_rx = match self.braidpool_template_rx.take() { + Some(rx) => rx, + None => { + return Err(( + PoolErrorKind::Configuration( + "BraidpoolIpc requires a template receiver; \ + call with_braidpool_template_rx() before start()" + .to_string(), + ), + self, + )); + } + }; + + let sv2_outgoing_tx = self.state.io.tp_to_channel_manager_sender.clone(); + let sv2_incoming_rx = self.state.io.channel_manager_to_tp_receiver.clone(); + let ct = cancellation_token.clone(); + let tm = self.task_manager.clone(); + + tm.spawn(async move { + if let Err(e) = braidpool_template_provider::sv2_template_consumer( + template_rx, + sv2_outgoing_tx, + sv2_incoming_rx, + ct, + ) + .await + { + error!(error = %e, "braidpool sv2_template_consumer exited with error"); + } + }); + + info!("Braidpool IPC template provider started"); + } TemplateProviderType::Sv2Tp { address, public_key, @@ -467,6 +523,8 @@ impl PoolRuntime { coinbase_outputs: self.coinbase_outputs, encoded_outputs: self.encoded_outputs, state: new_state, + share_bridge_receiver: self.share_bridge_receiver, + braidpool_template_rx: None, // consumed above; not needed after this stage }) } } @@ -476,6 +534,8 @@ impl PoolRuntime { self, ) -> Result, (PoolErrorKind, PoolRuntime)> { + let (share_bridge_sender, share_bridge_receiver) = create_share_bridge(); + let channel_manager = match ChannelManager::new( self.pool.config.clone(), self.state.io.channel_manager_to_tp_sender.clone(), @@ -483,6 +543,7 @@ impl PoolRuntime { self.state.io.downstream_to_channel_manager_receiver.clone(), self.encoded_outputs.clone(), self.jd.clone(), + Some(share_bridge_sender), ) .await { @@ -505,6 +566,8 @@ impl PoolRuntime { coinbase_outputs: self.coinbase_outputs, encoded_outputs: self.encoded_outputs, state: new_state, + share_bridge_receiver: Some(share_bridge_receiver), + braidpool_template_rx: None, }) } } @@ -612,6 +675,8 @@ impl PoolRuntime { coinbase_outputs: self.coinbase_outputs, encoded_outputs: self.encoded_outputs, state: Running, + share_bridge_receiver: self.share_bridge_receiver, + braidpool_template_rx: None, }) } } @@ -620,4 +685,10 @@ impl PoolRuntime { pub(super) async fn wait_for_shutdown(&self) { self.pool.cancellation_token.cancelled().await; } + + /// Takes the share bridge receiver out of the runtime so the caller can + /// forward validated shares to `propagate_valid_bead`. + pub(super) fn take_share_bridge_receiver(&mut self) -> Option { + self.share_bridge_receiver.take() + } } diff --git a/pool-apps/pool/src/main.rs b/pool-apps/pool/src/main.rs index 6813f04d2..41c714347 100644 --- a/pool-apps/pool/src/main.rs +++ b/pool-apps/pool/src/main.rs @@ -36,7 +36,7 @@ async fn inner_main() { } }); - if let Err(e) = pool.start().await { + if let Err(e) = pool.start(None).await { tracing::error!("Pool Error'ed out: {e}"); std::process::exit(1); }; diff --git a/stratum-apps/src/tp_type.rs b/stratum-apps/src/tp_type.rs index b0330fcfe..77b82994b 100644 --- a/stratum-apps/src/tp_type.rs +++ b/stratum-apps/src/tp_type.rs @@ -96,6 +96,12 @@ pub enum TemplateProviderType { fee_threshold: u64, min_interval: u8, }, + /// Braidpool node injects templates in-process via a tokio channel. + /// + /// No config fields — the channel receiver is wired at runtime via + /// [`PoolSv2::with_braidpool_template_rx`]. Configure in TOML as: + /// `template_provider_type = "BraidpoolIpc"`. + BraidpoolIpc, } #[cfg(test)]