From 804a58c6865ceb1bdcaf33b07ca51e1581daab76 Mon Sep 17 00:00:00 2001 From: Ansh Sharma Date: Sun, 3 May 2026 00:51:52 +0530 Subject: [PATCH] feat: adding timestamp,bead_context and altering coinbase construction --- Cargo.lock | 16 ++ Cargo.toml | 3 +- .../stratum-translation/src/sv1_to_sv2.rs | 2 + sv2/channels-sv2/Cargo.toml | 2 +- sv2/channels-sv2/src/client/extended.rs | 120 ++++++++- .../src/client/share_accounting.rs | 4 +- sv2/channels-sv2/src/client/standard.rs | 23 +- sv2/channels-sv2/src/server/extended.rs | 142 +++++++++-- sv2/channels-sv2/src/server/group.rs | 19 +- sv2/channels-sv2/src/server/jobs/factory.rs | 241 ++++++++++++++---- .../src/server/share_accounting.rs | 5 +- sv2/channels-sv2/src/server/standard.rs | 54 +++- sv2/subprotocols/mining/src/submit_shares.rs | 5 +- .../template-distribution/src/new_template.rs | 4 +- 14 files changed, 527 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce59fd8f98..3935673ae6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,7 @@ dependencies = [ "hex-conservative 0.2.2", "hex_lit", "secp256k1 0.29.1", + "serde", ] [[package]] @@ -128,6 +129,9 @@ name = "bitcoin-internals" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] [[package]] name = "bitcoin-io" @@ -142,6 +146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ "bitcoin-internals 0.3.0", + "serde", ] [[package]] @@ -171,6 +176,7 @@ checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative 0.2.2", + "serde", ] [[package]] @@ -200,6 +206,14 @@ dependencies = [ "generic-array", ] +[[package]] +name = "braidpool-common" +version = "0.1.0" +source = "git+https://github.com/Sansh2356/braidpool.git?branch=braidpool-common-utils#fe1d5f0b02f9c9562bcd84f35e2901a2b0b7f843" +dependencies = [ + "bitcoin", +] + [[package]] name = "bs58" version = "0.4.0" @@ -290,6 +304,7 @@ version = "5.0.0" dependencies = [ "binary_sv2", "bitcoin", + "braidpool-common", "hashbrown 0.15.5", "mining_sv2", "primitive-types", @@ -1135,6 +1150,7 @@ checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes 0.14.1", "secp256k1-sys 0.10.1", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6fd9bce303..ec4560c24a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,4 +31,5 @@ trait-variant = "0.1.2" # Pinned because newer versions pull in a transitive dependency that # requires the Rust 2024 edition, which is not supported by the 1.75 toolchain. quickcheck = ">= 1.0.3, < 1.1" -quickcheck_macros = ">= 1.0.0, < 1.1" \ No newline at end of file +quickcheck_macros = ">= 1.0.0, < 1.1" +braidpool-common = {git = "https://github.com/Sansh2356/braidpool.git",branch="braidpool-common-utils"} \ No newline at end of file diff --git a/stratum-core/stratum-translation/src/sv1_to_sv2.rs b/stratum-core/stratum-translation/src/sv1_to_sv2.rs index 767b40cddd..38a839cae8 100644 --- a/stratum-core/stratum-translation/src/sv1_to_sv2.rs +++ b/stratum-core/stratum-translation/src/sv1_to_sv2.rs @@ -1,4 +1,5 @@ use crate::error::{Result, StratumTranslationError}; +use binary_sv2::Sv2Option; use bitcoin::Target; use mining_sv2::{OpenExtendedMiningChannel, SubmitSharesExtended}; use v1::{client_to_server, utils::HexU32Be}; @@ -76,6 +77,7 @@ pub fn build_sv2_submit_shares_extended_from_sv1_submit( extranonce: extranonce .try_into() .map_err(|_| StratumTranslationError::InvalidExtranonceLength)?, + time: Sv2Option::new(None), }; Ok(submit_share_extended) } diff --git a/sv2/channels-sv2/Cargo.toml b/sv2/channels-sv2/Cargo.toml index 1c22cd1e15..261ef45530 100644 --- a/sv2/channels-sv2/Cargo.toml +++ b/sv2/channels-sv2/Cargo.toml @@ -21,7 +21,7 @@ tracing = { workspace = true } bitcoin = { workspace = true } primitive-types = { workspace = true } hashbrown = { workspace = true, optional = true } - +braidpool-common = {workspace = true} [features] default = [] diff --git a/sv2/channels-sv2/src/client/extended.rs b/sv2/channels-sv2/src/client/extended.rs index 382b312b3b..52248d516f 100644 --- a/sv2/channels-sv2/src/client/extended.rs +++ b/sv2/channels-sv2/src/client/extended.rs @@ -4,6 +4,9 @@ //! **Extended Channel** within a mining client. extern crate alloc; +use braidpool_common::compute_block_hash; +use std::time::UNIX_EPOCH; + use super::HashMap; use crate::{ bip141::try_strip_bip141, @@ -40,7 +43,7 @@ use tracing::debug; /// - A [`NewExtendedMiningJob`] message /// - The `extranonce_prefix` in use when the job was created /// - The target of the job -pub type ExtendedJob<'a> = (NewExtendedMiningJob<'a>, Vec, Target); +pub type ExtendedJob<'a> = (NewExtendedMiningJob<'a>, Vec, Target, Option); /// Mining Client abstraction for the state management of an Sv2 Extended Channel. /// @@ -78,6 +81,8 @@ pub struct ExtendedChannel<'a> { stale_jobs: HashMap>, share_accounting: ShareAccounting, chain_tip: Option, + //Network type on the basis of which hash shall be computed + network_type: String, } impl<'a> ExtendedChannel<'a> { @@ -90,6 +95,7 @@ impl<'a> ExtendedChannel<'a> { nominal_hashrate: f32, version_rolling: bool, rollable_extranonce_size: u16, + network_type: String, ) -> Self { Self { channel_id, @@ -105,6 +111,7 @@ impl<'a> ExtendedChannel<'a> { stale_jobs: HashMap::new(), share_accounting: ShareAccounting::new(), chain_tip: None, + network_type, } } @@ -280,10 +287,26 @@ impl<'a> ExtendedChannel<'a> { if let Some(active_job) = self.active_job.clone() { self.past_jobs.insert(active_job.0.job_id, active_job); } + + //While submitting shares either that share will be active or available as a past share + //So storing timestamp only for past jobs or for active jobs to be shared with upstream + let current_system_time = std::time::SystemTime::now(); + let duration_since_epoch = match current_system_time.duration_since(UNIX_EPOCH) { + Ok(duration) => duration, + Err(error) => { + tracing::error!( + "An error occurred while fetching timestamp - {}", + error.to_string() + ); + panic!() + } + }; + let unix_timestamp = duration_since_epoch.as_secs() as u32; self.active_job = Some(( new_extended_mining_job, self.extranonce_prefix.as_bytes().to_vec(), self.target, + Some(unix_timestamp), )); } None => { @@ -293,6 +316,7 @@ impl<'a> ExtendedChannel<'a> { new_extended_mining_job, self.extranonce_prefix.as_bytes().to_vec(), self.target, + None, ), ); } @@ -402,10 +426,25 @@ impl<'a> ExtendedChannel<'a> { if let Some(active_job) = self.active_job.clone() { self.past_jobs.insert(active_job.0.job_id, active_job); } + let current_system_time = std::time::SystemTime::now(); + let duration_since_epoch = match current_system_time.duration_since(UNIX_EPOCH) { + Ok(duration) => duration, + Err(error) => { + tracing::error!( + "An error occurred while fetching timestamp - {}", + error.to_string() + ); + panic!() + } + }; + + let unix_timestamp = duration_since_epoch.as_secs() as u32; + self.active_job = Some(( new_extended_mining_job, self.extranonce_prefix.as_bytes().to_vec(), self.target, + Some(unix_timestamp), )); Ok(()) @@ -453,6 +492,21 @@ impl<'a> ExtendedChannel<'a> { match self.future_jobs.remove(&set_new_prev_hash.job_id) { Some(mut activated_job) => { activated_job.0.min_ntime = Sv2Option::new(Some(set_new_prev_hash.min_ntime)); + let current_system_time = std::time::SystemTime::now(); + let duration_since_epoch = match current_system_time.duration_since(UNIX_EPOCH) { + Ok(duration) => duration, + Err(error) => { + tracing::error!( + "An error occurred while fetching timestamp - {}", + error.to_string() + ); + panic!() + } + }; + + let unix_timestamp = duration_since_epoch.as_secs() as u32; + + activated_job.3 = Some(unix_timestamp); self.active_job = Some(activated_job); } None => { @@ -566,7 +620,7 @@ impl<'a> ExtendedChannel<'a> { }; // convert the header hash to a target type for easy comparison - let share_hash = header.block_hash(); + let share_hash = compute_block_hash(&header, &self.network_type); let raw_share_hash: [u8; 32] = *share_hash.to_raw_hash().as_ref(); let share_hash_target = Target::from_le_bytes(raw_share_hash); let share_hash_as_diff = share_hash_target.difficulty_float(); @@ -584,7 +638,7 @@ impl<'a> ExtendedChannel<'a> { bytes_to_hex(&job_target_bytes), format!("{:x}", network_target) ); - + let block_propagation_time = job.3; // check if a block was found if network_target.is_met_by(share_hash) { if self @@ -596,7 +650,10 @@ impl<'a> ExtendedChannel<'a> { self.share_accounting .track_validated_share(share.sequence_number, share_hash.to_raw_hash()); self.share_accounting.increment_blocks_found(); - return Ok(ShareValidationResult::BlockFound(share_hash.to_raw_hash())); + return Ok(ShareValidationResult::BlockFound( + share_hash.to_raw_hash(), + block_propagation_time, + )); } // check if the share hash meets the job target @@ -614,7 +671,10 @@ impl<'a> ExtendedChannel<'a> { // update the best diff self.share_accounting.update_best_diff(share_hash_as_diff); - return Ok(ShareValidationResult::Valid(share_hash.to_raw_hash())); + return Ok(ShareValidationResult::Valid( + share_hash.to_raw_hash(), + block_propagation_time, + )); } Err(ShareValidationError::DoesNotMeetTarget) @@ -635,7 +695,7 @@ mod tests { use mining_sv2::{ NewExtendedMiningJob, SetNewPrevHash as SetNewPrevHashMp, SubmitSharesExtended, }; - use std::convert::TryInto; + use std::{convert::TryInto, time::UNIX_EPOCH}; #[test] fn test_future_job_activation_flow() { @@ -659,6 +719,7 @@ mod tests { nominal_hashrate, version_rolling, rollable_extranonce_size, + "mainnet".to_string(), ); let future_job = NewExtendedMiningJob { @@ -705,7 +766,18 @@ mod tests { nbits: 503543726, min_ntime: ntime, }; - + let current_system_time = std::time::SystemTime::now(); + let duration_since_epoch = match current_system_time.duration_since(UNIX_EPOCH) { + Ok(duration) => duration, + Err(error) => { + tracing::error!( + "An error occurred while fetching timestamp - {}", + error.to_string() + ); + panic!() + } + }; + let unix_timestamp = duration_since_epoch.as_secs() as u32; channel.on_set_new_prev_hash(set_new_prev_hash).unwrap(); assert!(channel.get_future_jobs().is_empty()); @@ -718,7 +790,8 @@ mod tests { Some(&( previously_future_job, extranonce_prefix, - channel.get_target().clone() + channel.get_target().clone(), + Some(unix_timestamp) )) ); } @@ -745,6 +818,7 @@ mod tests { nominal_hashrate, version_rolling, rollable_extranonce_size, + "mainnet".to_string(), ); let ntime: u32 = 1746839905; @@ -771,7 +845,18 @@ mod tests { .unwrap(), merkle_path: vec![].try_into().unwrap(), }; - + let current_system_time = std::time::SystemTime::now(); + let duration_since_epoch = match current_system_time.duration_since(UNIX_EPOCH) { + Ok(duration) => duration, + Err(error) => { + tracing::error!( + "An error occurred while fetching timestamp - {}", + error.to_string() + ); + panic!() + } + }; + let unix_timestamp = duration_since_epoch.as_secs() as u32; channel .on_new_extended_mining_job(active_job.clone()) .unwrap(); @@ -782,7 +867,8 @@ mod tests { Some(&( active_job.clone(), extranonce_prefix.clone(), - channel.get_target().clone() + channel.get_target().clone(), + Some(unix_timestamp) )) ); assert_eq!(channel.get_past_jobs().len(), 0); @@ -799,7 +885,8 @@ mod tests { Some(&( new_active_job, extranonce_prefix, - channel.get_target().clone() + channel.get_target().clone(), + Some(unix_timestamp) )) ); assert_eq!(channel.get_past_jobs().len(), 1); @@ -826,6 +913,7 @@ mod tests { nominal_hashrate, version_rolling, rollable_extranonce_size, + "mainnet".to_string(), ); let future_job = NewExtendedMiningJob { @@ -885,11 +973,12 @@ mod tests { ntime: 1745596971, version: 536870912, extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + time: Sv2Option::new(None), }; let res = channel.validate_share(share_valid_block.clone()); - assert!(matches!(res, Ok(ShareValidationResult::BlockFound(_)))); + assert!(matches!(res, Ok(ShareValidationResult::BlockFound(_, _)))); assert_eq!(channel.get_share_accounting().get_blocks_found(), 1); // re-submitting the same valid block must be rejected as duplicate @@ -927,6 +1016,7 @@ mod tests { nominal_hashrate, version_rolling, rollable_extranonce_size, + "mainnet".to_string(), ); let future_job = NewExtendedMiningJob { @@ -986,6 +1076,7 @@ mod tests { ntime: 1745596971, version: 536870912, extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + time: Sv2Option::new(None), }; let res = channel.validate_share(share_low_diff); @@ -1022,6 +1113,7 @@ mod tests { nominal_hashrate, version_rolling, rollable_extranonce_size, + "mainnet".to_string(), ); let future_job = NewExtendedMiningJob { @@ -1081,11 +1173,12 @@ mod tests { ntime: 1745596971, version: 536870912, extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + time: Sv2Option::new(None), }; let res = channel.validate_share(valid_share); - assert!(matches!(res, Ok(ShareValidationResult::Valid(_)))); + assert!(matches!(res, Ok(ShareValidationResult::Valid(_, _)))); // try to cheat by re-submitting the same share // with a different sequence number @@ -1097,6 +1190,7 @@ mod tests { ntime: 1745596971, version: 536870912, extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + time: Sv2Option::new(None), }; let res = channel.validate_share(repeated_share); diff --git a/sv2/channels-sv2/src/client/share_accounting.rs b/sv2/channels-sv2/src/client/share_accounting.rs index e0579dd376..7d59243451 100644 --- a/sv2/channels-sv2/src/client/share_accounting.rs +++ b/sv2/channels-sv2/src/client/share_accounting.rs @@ -15,8 +15,8 @@ use bitcoin::hashes::sha256d::Hash; /// - `BlockFound`: The submitted share resulted in a new block being found. #[derive(Debug)] pub enum ShareValidationResult { - Valid(Hash), - BlockFound(Hash), + Valid(Hash, Option), + BlockFound(Hash, Option), } /// Possible errors encountered during share validation. diff --git a/sv2/channels-sv2/src/client/standard.rs b/sv2/channels-sv2/src/client/standard.rs index c46c97ef1a..ddae1a70e6 100644 --- a/sv2/channels-sv2/src/client/standard.rs +++ b/sv2/channels-sv2/src/client/standard.rs @@ -24,11 +24,11 @@ use bitcoin::{ hashes::sha256d::Hash, CompactTarget, Target, }; +use braidpool_common::compute_block_hash; use mining_sv2::{ NewExtendedMiningJob, NewMiningJob, SetNewPrevHash as SetNewPrevHashMp, SubmitSharesStandard, }; use tracing::debug; - /// A type alias representing a standard mining job tied to a specific `target`. pub type StandardJob<'a> = (NewMiningJob<'a>, Target); @@ -59,6 +59,7 @@ pub struct StandardChannel<'a> { stale_jobs: HashMap>, share_accounting: ShareAccounting, chain_tip: Option, + network_type: String, } impl<'a> StandardChannel<'a> { @@ -69,6 +70,7 @@ impl<'a> StandardChannel<'a> { extranonce_prefix: ExtranoncePrefix, target: Target, nominal_hashrate: f32, + network_type: String, ) -> Self { Self { channel_id, @@ -82,6 +84,7 @@ impl<'a> StandardChannel<'a> { stale_jobs: HashMap::new(), share_accounting: ShareAccounting::new(), chain_tip: None, + network_type, } } @@ -347,7 +350,7 @@ impl<'a> StandardChannel<'a> { }; // convert the header hash to a target type for easy comparison - let share_hash = header.block_hash(); + let share_hash = compute_block_hash(&header, &self.network_type); let raw_share_hash: [u8; 32] = *share_hash.to_raw_hash().as_ref(); let share_hash_target = Target::from_le_bytes(raw_share_hash); let share_hash_as_diff = share_hash_target.difficulty_float(); @@ -377,7 +380,10 @@ impl<'a> StandardChannel<'a> { self.share_accounting .track_validated_share(share.sequence_number, share_hash.to_raw_hash()); self.share_accounting.increment_blocks_found(); - return Ok(ShareValidationResult::BlockFound(share_hash.to_raw_hash())); + return Ok(ShareValidationResult::BlockFound( + share_hash.to_raw_hash(), + None, + )); } // check if the share hash meets the job target @@ -395,7 +401,7 @@ impl<'a> StandardChannel<'a> { // update the best diff self.share_accounting.update_best_diff(share_hash_as_diff); - return Ok(ShareValidationResult::Valid(share_hash.to_raw_hash())); + return Ok(ShareValidationResult::Valid(share_hash.to_raw_hash(), None)); } Err(ShareValidationError::DoesNotMeetTarget) @@ -433,6 +439,7 @@ mod tests { ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(), target, nominal_hashrate, + "mainnet".to_string(), ); let future_job = NewMiningJob { @@ -496,6 +503,7 @@ mod tests { ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(), target, nominal_hashrate, + "mainnet".to_string(), ); let ntime: u32 = 1746839905; @@ -550,6 +558,7 @@ mod tests { ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(), target, nominal_hashrate, + "mainnet".to_string(), ); let future_job = NewMiningJob { @@ -597,7 +606,7 @@ mod tests { let res = channel.validate_share(share_valid_block.clone()); - assert!(matches!(res, Ok(ShareValidationResult::BlockFound(_)))); + assert!(matches!(res, Ok(ShareValidationResult::BlockFound(_, _)))); assert_eq!(channel.get_share_accounting().get_blocks_found(), 1); // re-submitting the same valid block must be rejected as duplicate @@ -632,6 +641,7 @@ mod tests { ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(), target, nominal_hashrate, + "mainnet".to_string(), ); let future_job = NewMiningJob { @@ -708,6 +718,7 @@ mod tests { ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(), target, nominal_hashrate, + "mainnet".to_string(), ); let future_job = NewMiningJob { @@ -755,6 +766,6 @@ mod tests { let res = channel.validate_share(valid_share); - assert!(matches!(res, Ok(ShareValidationResult::Valid(_)))); + assert!(matches!(res, Ok(ShareValidationResult::Valid(_, _)))); } } diff --git a/sv2/channels-sv2/src/server/extended.rs b/sv2/channels-sv2/src/server/extended.rs index 899278935b..94205b776a 100644 --- a/sv2/channels-sv2/src/server/extended.rs +++ b/sv2/channels-sv2/src/server/extended.rs @@ -53,15 +53,16 @@ use crate::{ }; use bitcoin::{ blockdata::block::{Header, Version}, + consensus::deserialize, hashes::sha256d::Hash, transaction::TxOut, - CompactTarget, Target, + CompactTarget, Target, Transaction, }; +use braidpool_common::compute_block_hash; use mining_sv2::{SetCustomMiningJob, SubmitSharesExtended}; use std::{collections::HashMap, convert::TryInto, marker::PhantomData}; use template_distribution_sv2::{NewTemplate, SetNewPrevHash as SetNewPrevHashTdp}; use tracing::debug; - /// Mining Server abstraction of a Sv2 Extended Channel. /// /// It keeps track of: @@ -98,8 +99,17 @@ where expected_share_per_minute: f32, chain_tip: Option, phantom: PhantomData<&'a ()>, + network_type: String, +} +#[derive(Debug, Clone)] +//Holding all the resources required for the construction of the Bead object +pub struct BeadContext { + pub candidate_block: bitcoin::Block, + pub extranonce_2_raw_value: Vec, + pub job_sent_timestamp: u32, + // TODO: Will be used as separate entity after altering `uncommitted_metadata` + pub extranonce_1_raw_value: Vec, } - impl<'a, J> ExtendedChannel<'a, J> where J: JobStore>, @@ -127,6 +137,7 @@ where expected_share_per_minute: f32, job_store: J, pool_tag_string: String, + network_type: String, ) -> Result { Self::new( channel_id, @@ -141,6 +152,7 @@ where job_store, Some(pool_tag_string), None, + network_type, ) } @@ -168,6 +180,7 @@ where job_store: J, pool_tag_string: Option, miner_tag_string: String, + network_type: String, ) -> Result { Self::new( channel_id, @@ -182,6 +195,7 @@ where job_store, pool_tag_string, Some(miner_tag_string), + network_type, ) } @@ -200,6 +214,7 @@ where job_store: J, pool_tag: Option, miner_tag: Option, + network_type: String, ) -> Result { let target = match hash_rate_to_target(nominal_hashrate.into(), expected_share_per_minute.into()) { @@ -246,6 +261,7 @@ where expected_share_per_minute, chain_tip: None, phantom: PhantomData, + network_type, }) } @@ -728,7 +744,7 @@ where }; // convert the header hash to a target type for easy comparison - let share_hash = header.block_hash(); + let share_hash = compute_block_hash(&header, &self.network_type); let raw_share_hash: [u8; 32] = *share_hash.to_raw_hash().as_ref(); let share_hash_target = Target::from_le_bytes(raw_share_hash); let share_hash_as_diff = share_hash_target.difficulty_float(); @@ -770,10 +786,31 @@ where match job.get_origin() { JobOrigin::NewTemplate(template) => { let template_id = template.template_id; + let txs = template.txs.clone(); + let mut bead_txs = Vec::new(); + for tx in txs.to_vec() { + let tx: Transaction = deserialize(&tx).expect( + "An error occurred while deserailziing transaction from raw bytes.", + ); + bead_txs.push(tx); + } + // Construct and log the complete block using rust-bitcoin's Block struct + let complete_block = bitcoin::Block { + header, + txdata: bead_txs, + }; + // Bead context required for the formation of bead_propgation_result + let bead_context: BeadContext = BeadContext { + candidate_block: complete_block, + extranonce_1_raw_value: extranonce_prefix.clone(), + extranonce_2_raw_value: share.extranonce.to_vec(), + job_sent_timestamp: share.time.into_inner().unwrap_or_default(), + }; return Ok(ShareValidationResult::BlockFound( share_hash.to_raw_hash(), Some(template_id), coinbase, + Some(bead_context), )); } JobOrigin::SetCustomMiningJob(_set_custom_mining_job) => { @@ -781,6 +818,7 @@ where share_hash.to_raw_hash(), None, coinbase, + None, )); } } @@ -803,8 +841,38 @@ where // update the best diff self.share_accounting.update_best_diff(share_hash_as_diff); - - Ok(ShareValidationResult::Valid(share_hash.to_raw_hash())) + match job.get_origin() { + JobOrigin::NewTemplate(template) => { + let txs = template.txs.clone(); + let mut bead_txs = Vec::new(); + for tx in txs.to_vec() { + let tx: Transaction = deserialize(&tx).expect( + "An error occurred while deserailziing transaction from raw bytes.", + ); + bead_txs.push(tx); + } + // Construct and log the complete block using rust-bitcoin's Block struct + let complete_block = bitcoin::Block { + header, + txdata: bead_txs, + }; + // Bead context required for the formation of bead_propgation_result + let bead_context: BeadContext = BeadContext { + candidate_block: complete_block, + extranonce_1_raw_value: extranonce_prefix.clone(), + extranonce_2_raw_value: share.extranonce.to_vec(), + job_sent_timestamp: share.time.into_inner().unwrap_or_default(), + }; + return Ok(ShareValidationResult::Valid( + share_hash.to_raw_hash(), + Some(bead_context), + )); + } + _ => { + tracing::warn!("Share valid but does not satisfies the NetworkTarget and it CustomTemplate type"); + } + }; + Ok(ShareValidationResult::Valid(share_hash.to_raw_hash(), None)) } else { Err(ShareValidationError::DoesNotMeetTarget) } @@ -826,7 +894,7 @@ mod tests { share_accounting::{ShareValidationError, ShareValidationResult}, }, }; - use binary_sv2::{Sv2Option, U256}; + use binary_sv2::{Seq064K, Sv2Option, B016M, U256}; use bitcoin::{transaction::TxOut, Amount, ScriptBuf, Target}; use mining_sv2::{NewExtendedMiningJob, SetCustomMiningJob, SubmitSharesExtended}; use std::convert::TryInto; @@ -865,11 +933,12 @@ mod tests { share_batch_size, expected_share_per_minute, job_store, + Some("".to_string()), None, - None, + "mainnet".to_string(), ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); let template = NewTemplate { template_id: 1, future_template: true, @@ -888,6 +957,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -1011,8 +1081,10 @@ mod tests { share_batch_size, expected_share_per_minute, job_store, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, - None, + "mainnet".to_string(), ) .unwrap(); @@ -1026,7 +1098,7 @@ mod tests { let chain_tip = ChainTip::new(prev_hash, n_bits, ntime); channel.set_chain_tip(chain_tip); - + let dummy_data: Vec> = Vec::new(); let template = NewTemplate { template_id: 1, future_template: false, @@ -1045,6 +1117,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -1131,11 +1204,13 @@ mod tests { share_batch_size, expected_share_per_minute, job_store, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, - None, + "mainnet".to_string(), ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); let template = NewTemplate { template_id: 1, future_template: true, @@ -1154,6 +1229,8 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + + txs: Seq064K::new(dummy_data).unwrap(), }; let pubkey_hash = [ @@ -1211,11 +1288,12 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); // channel target: 04325c53ef368eb04325c53ef368eb04325c53ef368eb04325c53ef368eb0431 - + let dummy_data: Vec> = Vec::new(); let template_id = 1; let template = NewTemplate { template_id, @@ -1235,6 +1313,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -1279,13 +1358,14 @@ mod tests { ntime: 1745596971, version: 536870912, extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + time: Sv2Option::new(None), }; let res = channel.validate_share(share_valid_block.clone()); assert!(matches!( res, - Ok(ShareValidationResult::BlockFound(_, _, _)) + Ok(ShareValidationResult::BlockFound(_, _, _, _)) )); assert_eq!(channel.get_share_accounting().get_blocks_found(), 1); @@ -1332,11 +1412,12 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); // channel target: 000aebbc990fff5144366f000aebbc990fff5144366f000aebbc990fff514435 - + let dummy_data: Vec> = Vec::new(); let template_id = 1; let template = NewTemplate { template_id, @@ -1356,6 +1437,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -1400,6 +1482,8 @@ mod tests { ntime: 1745596971, version: 536870912, extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + + time: Sv2Option::new(None), }; let res = channel.validate_share(share_low_diff); @@ -1442,14 +1526,16 @@ mod tests { share_batch_size, expected_share_per_minute, job_store, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, - None, + "mainnet".to_string(), ) .unwrap(); // channel target is: // 0001179d9861a761ffdadd11c307c4fc04eea3a418f7d687584e4434af158205 - + let dummy_data: Vec> = Vec::new(); let template_id = 1; let template = NewTemplate { template_id, @@ -1469,6 +1555,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -1515,10 +1602,11 @@ mod tests { ntime: 1745611105, version: 536870912, extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + time: Sv2Option::new(None), }; let res = channel.validate_share(valid_share); - assert!(matches!(res, Ok(ShareValidationResult::Valid(_)))); + assert!(matches!(res, Ok(ShareValidationResult::Valid(_, _)))); // try to cheat by re-submitting the same share // with a different sequence number @@ -1530,6 +1618,7 @@ mod tests { ntime: 1745611105, version: 536870912, extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + time: Sv2Option::new(None), }; let res = channel.validate_share(repeated_share); @@ -1568,8 +1657,10 @@ mod tests { share_batch_size, expected_share_per_minute, job_store, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, - None, + "mainnet".to_string(), ) .unwrap(); @@ -1613,6 +1704,7 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); @@ -1698,6 +1790,7 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); @@ -1755,9 +1848,10 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); let template = NewTemplate { template_id: 1, future_template: true, @@ -1776,6 +1870,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; let pubkey_hash = [ @@ -1861,9 +1956,10 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); let template = NewTemplate { template_id: 1, future_template: false, // non-future/active job @@ -1882,6 +1978,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; let pubkey_hash = [ @@ -1964,6 +2061,7 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); diff --git a/sv2/channels-sv2/src/server/group.rs b/sv2/channels-sv2/src/server/group.rs index ecea25c07e..a7dc2dd83a 100644 --- a/sv2/channels-sv2/src/server/group.rs +++ b/sv2/channels-sv2/src/server/group.rs @@ -323,7 +323,7 @@ mod tests { jobs::job_store::{DefaultJobStore, JobStore}, }, }; - use binary_sv2::Sv2Option; + use binary_sv2::{Seq064K, Sv2Option, B016M}; use bitcoin::{transaction::TxOut, Amount, ScriptBuf}; use mining_sv2::NewExtendedMiningJob; use std::{collections::HashSet, convert::TryInto}; @@ -343,11 +343,11 @@ mod tests { group_channel_id, job_store, full_extranonce_size, - None, + Some("".to_string()), None, ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); let template = NewTemplate { template_id: 1, future_template: true, @@ -366,6 +366,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -473,7 +474,8 @@ mod tests { group_channel_id, job_store, full_extranonce_size, - None, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, ) .unwrap(); @@ -485,7 +487,7 @@ mod tests { ] .into(); let n_bits = 503543726; - + let dummy_data: Vec> = Vec::new(); let chain_tip = ChainTip::new(prev_hash, n_bits, ntime); let template = NewTemplate { template_id: 1, @@ -505,6 +507,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -571,11 +574,12 @@ mod tests { group_channel_id, job_store, full_extranonce_size, - None, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); let template = NewTemplate { template_id: 1, future_template: true, @@ -594,6 +598,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; let pubkey_hash = [ diff --git a/sv2/channels-sv2/src/server/jobs/factory.rs b/sv2/channels-sv2/src/server/jobs/factory.rs index 266555c3b2..82bbe4edc8 100644 --- a/sv2/channels-sv2/src/server/jobs/factory.rs +++ b/sv2/channels-sv2/src/server/jobs/factory.rs @@ -38,6 +38,7 @@ use bitcoin::{ use mining_sv2::{NewExtendedMiningJob, NewMiningJob, SetCustomMiningJob}; use std::convert::TryInto; use template_distribution_sv2::NewTemplate; +use tracing::{debug, info}; #[derive(Debug, PartialEq, Eq, Clone)] struct JobIdFactory { @@ -73,6 +74,21 @@ pub struct JobFactory { } impl JobFactory { + /// Returns true if this factory is in Braidpool mode. + /// + /// Braidpool mode is active when: + /// - `pool_tag_string` is None, OR + /// - `pool_tag_string` equals "Braidpool" + /// + /// Used to distinguish how the coinbase is constructed depending on the data flow either from template_creator or otherwise + /// directly from the config reward coinbase script + fn is_braidpool_mode(&self) -> bool { + match &self.pool_tag_string { + None => true, + Some(tag) => tag == "Braidpool", + } + } + /// Creates a new [`JobFactory`] instance. /// /// The `pool_tag_string` and `miner_tag_string` are optional and will be added to the coinbase @@ -97,8 +113,16 @@ impl JobFactory { /// /// The character `/` is used as a delimiter. /// - /// If no pool or miner tag is provided, the delimiters are still added. + /// If in Braidpool mode (pool_tag is None or "Braidpool"), returns an empty vector + /// the template's coinbase_prefix already contains the pool identifier. pub fn op_pushbytes_pool_miner_tag(&self) -> Result, JobFactoryError> { + // Braidpool mode: skip pool/miner tag entirely + // Template's coinbase_prefix already has [height][pool_id] + if self.is_braidpool_mode() { + return Ok(vec![]); + } + + //Stratum specific miner tag let mut pool_miner_tag = vec![]; pool_miner_tag.extend_from_slice(b"/"); if let Some(pool_tag_string) = &self.pool_tag_string { @@ -149,12 +173,16 @@ impl JobFactory { template: NewTemplate<'a>, additional_coinbase_outputs: Vec, ) -> Result, JobFactoryError> { - let coinbase_outputs_sum = additional_coinbase_outputs - .iter() - .map(|o| o.value.to_sat()) - .sum::(); - if coinbase_outputs_sum != template.coinbase_tx_value_remaining { - return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + // In Braidpool mode, template already has correct outputs, skip validation + // In SV2 mode, validate that additional outputs sum equals template value + if !self.is_braidpool_mode() { + let coinbase_outputs_sum = additional_coinbase_outputs + .iter() + .map(|o| o.value.to_sat()) + .sum::(); + if coinbase_outputs_sum != template.coinbase_tx_value_remaining { + return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + } } let job_id = self.job_id_factory.next(); @@ -237,12 +265,16 @@ impl JobFactory { additional_coinbase_outputs: Vec, full_extranonce_size: usize, ) -> Result, JobFactoryError> { - let coinbase_outputs_sum = additional_coinbase_outputs - .iter() - .map(|o| o.value.to_sat()) - .sum::(); - if coinbase_outputs_sum != template.coinbase_tx_value_remaining { - return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + // In Braidpool mode, template already has correct outputs, skip validation + // In SV2 mode, validate that additional outputs sum equals template value + if !self.is_braidpool_mode() { + let coinbase_outputs_sum = additional_coinbase_outputs + .iter() + .map(|o| o.value.to_sat()) + .sum::(); + if coinbase_outputs_sum != template.coinbase_tx_value_remaining { + return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + } } let job_id = self.job_id_factory.next(); @@ -566,41 +598,134 @@ impl JobFactory { coinbase_reward_outputs: Vec, full_extranonce_size: usize, ) -> Result { - // check that the sum of the additional coinbase outputs is equal to the value remaining in - // the active template - let mut coinbase_reward_outputs_sum = Amount::from_sat(0); - for output in coinbase_reward_outputs.iter() { - coinbase_reward_outputs_sum = coinbase_reward_outputs_sum - .checked_add(output.value) - .ok_or(JobFactoryError::CoinbaseOutputsSumOverflow)?; - } + let mode = if self.is_braidpool_mode() { + "Braidpool" + } else { + "SV2" + }; + info!( + "[JobFactory::coinbase] Building coinbase in {} mode, template_id={}, extranonce_size={}", + mode, template.template_id, full_extranonce_size + ); - if template.coinbase_tx_value_remaining < coinbase_reward_outputs_sum.to_sat() { - return Err(JobFactoryError::InvalidCoinbaseOutputsSum); - } + // Braidpool mode: use only template outputs + // The template already contains correct outputs: [reward][segwit_commit][braidpool_op_return] + let outputs = if self.is_braidpool_mode() { + // Use template outputs directly (Braidpool mode) + let template_outputs = deserialize_template_outputs( + template.coinbase_tx_outputs.to_vec(), + template.coinbase_tx_outputs_count, + ) + .map_err(|_| JobFactoryError::DeserializeCoinbaseOutputsError)?; - let mut outputs = vec![]; + info!( + "[JobFactory::coinbase] Braidpool mode: using {} template outputs directly", + template_outputs.len() + ); + for (i, output) in template_outputs.iter().enumerate() { + info!( + "[JobFactory::coinbase] output[{}]: value={} sats, script_pubkey={}", + i, + output.value.to_sat(), + output + .script_pubkey + .as_bytes() + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ); + } + template_outputs + } else { + // SV2 mode: prepend pool reward outputs before template outputs + let mut coinbase_reward_outputs_sum = Amount::from_sat(0); + for output in coinbase_reward_outputs.iter() { + coinbase_reward_outputs_sum = coinbase_reward_outputs_sum + .checked_add(output.value) + .ok_or(JobFactoryError::CoinbaseOutputsSumOverflow)?; + } - for output in coinbase_reward_outputs.iter() { - outputs.push(output.clone()); - } + if template.coinbase_tx_value_remaining < coinbase_reward_outputs_sum.to_sat() { + return Err(JobFactoryError::InvalidCoinbaseOutputsSum); + } - let mut template_outputs = deserialize_template_outputs( - template.coinbase_tx_outputs.to_vec(), - template.coinbase_tx_outputs_count, - ) - .map_err(|_| JobFactoryError::DeserializeCoinbaseOutputsError)?; + let mut outputs = vec![]; + for output in coinbase_reward_outputs.iter() { + outputs.push(output.clone()); + } - outputs.append(&mut template_outputs); + let mut template_outputs = deserialize_template_outputs( + template.coinbase_tx_outputs.to_vec(), + template.coinbase_tx_outputs_count, + ) + .map_err(|_| JobFactoryError::DeserializeCoinbaseOutputsError)?; + + info!( + "[JobFactory::coinbase] SV2 mode: {} reward outputs + {} template outputs", + coinbase_reward_outputs.len(), + template_outputs.len() + ); + + outputs.append(&mut template_outputs); + + for (i, output) in outputs.iter().enumerate() { + debug!( + "[JobFactory::coinbase] output[{}]: value={} sats, script_pubkey={}", + i, + output.value.to_sat(), + output + .script_pubkey + .as_bytes() + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ); + } + outputs + }; let op_pushbytes_pool_miner_tag = self.op_pushbytes_pool_miner_tag()?; + //Modifying according to template_creator let mut script_sig = vec![]; script_sig.extend_from_slice(&template.coinbase_prefix.to_vec()); script_sig.extend_from_slice(&op_pushbytes_pool_miner_tag); script_sig.push(full_extranonce_size as u8); // OP_PUSHBYTES_X (for the full extranonce) script_sig.extend_from_slice(&vec![0; full_extranonce_size]); + info!( + "[JobFactory::coinbase] scriptSig ({} bytes): {}", + script_sig.len(), + script_sig + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ); + info!( + "[JobFactory::coinbase] coinbase_prefix ({} bytes): {}", + template.coinbase_prefix.len(), + template + .coinbase_prefix + .to_vec() + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ); + if !op_pushbytes_pool_miner_tag.is_empty() { + info!( + "[JobFactory::coinbase] pool_miner_tag ({} bytes): {}", + op_pushbytes_pool_miner_tag.len(), + op_pushbytes_pool_miner_tag + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ); + } + debug!( + "[JobFactory::coinbase] extranonce_space: OP_PUSHBYTES_{} + {} zero bytes", + full_extranonce_size, full_extranonce_size + ); + let tx_in = TxIn { previous_output: OutPoint::null(), script_sig: script_sig.into(), @@ -631,11 +756,16 @@ impl JobFactory { )?; let serialized_coinbase = serialize(&coinbase); - // Calculate the full pool/miner tag length including delimiters and OP_PUSHBYTES opcode - let pool_miner_tag_len = 1 // OP_PUSHBYTES opcode + // Braidpool mode: pool_miner_tag_len is 0 (no pool/miner tag added) + // SV2 mode: calculate full length including delimiters and OP_PUSHBYTES + let pool_miner_tag_len = if self.is_braidpool_mode() { + 0 + } else { + 1 // OP_PUSHBYTES opcode + 3 // three "/" delimiters + self.pool_tag_string.as_ref().map_or(0, |s| s.len()) - + self.miner_tag_string.as_ref().map_or(0, |s| s.len()); + + self.miner_tag_string.as_ref().map_or(0, |s| s.len()) + }; let index = 4 // tx version + 2 // segwit bytes @@ -649,6 +779,15 @@ impl JobFactory { let coinbase_tx_prefix = serialized_coinbase[0..index].to_vec(); + info!( + "[JobFactory::coinbase_tx_prefix] ({} bytes): {}", + coinbase_tx_prefix.len(), + coinbase_tx_prefix + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ); + Ok(coinbase_tx_prefix) } @@ -665,11 +804,16 @@ impl JobFactory { )?; let serialized_coinbase = serialize(&coinbase); - // Calculate the full pool/miner tag length including delimiters and OP_PUSHBYTES opcode - let pool_miner_tag_len = 1 // OP_PUSHBYTES opcode + // Braidpool mode: pool_miner_tag_len is 0 (no pool/miner tag added) + // SV2 mode: calculate full length including delimiters and OP_PUSHBYTES + let pool_miner_tag_len = if self.is_braidpool_mode() { + 0 + } else { + 1 // OP_PUSHBYTES opcode + 3 // three "/" delimiters + self.pool_tag_string.as_ref().map_or(0, |s| s.len()) - + self.miner_tag_string.as_ref().map_or(0, |s| s.len()); + + self.miner_tag_string.as_ref().map_or(0, |s| s.len()) + }; let coinbase_tx_suffix = serialized_coinbase[4 // tx version + 2 // segwit bytes @@ -683,6 +827,15 @@ impl JobFactory { + full_extranonce_size..] .to_vec(); + info!( + "[JobFactory::coinbase_tx_suffix] ({} bytes): {}", + coinbase_tx_suffix.len(), + coinbase_tx_suffix + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + ); + Ok(coinbase_tx_suffix) } } @@ -690,17 +843,17 @@ impl JobFactory { #[cfg(test)] mod tests { use super::*; + use binary_sv2::{Seq064K, B016M}; use bitcoin::ScriptBuf; use template_distribution_sv2::NewTemplate; #[test] fn test_new_pool_job() { let mut job_factory = JobFactory::new(true, Some("Stratum V2 SRI Pool".to_string()), None); - + let dummy_data: Vec> = Vec::new(); // note: // the messages on this test were collected from a sane message flow // we use them as test vectors to assert correct behavior of job creation - let template = NewTemplate { template_id: 1, future_template: true, @@ -719,6 +872,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -797,7 +951,7 @@ mod tests { 0, 0, 0, 0, 0, 0, 1, ] .to_vec(); - + let dummy_data: Vec> = Vec::new(); let template = NewTemplate { template_id: 1, future_template: false, @@ -816,6 +970,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 0, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the diff --git a/sv2/channels-sv2/src/server/share_accounting.rs b/sv2/channels-sv2/src/server/share_accounting.rs index a7dd53e49d..f62350a637 100644 --- a/sv2/channels-sv2/src/server/share_accounting.rs +++ b/sv2/channels-sv2/src/server/share_accounting.rs @@ -18,6 +18,7 @@ //! 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 crate::server::extended::BeadContext; use bitcoin::hashes::sha256d::Hash; use std::collections::HashSet; @@ -32,13 +33,13 @@ use std::collections::HashSet; #[derive(Debug)] pub enum ShareValidationResult { /// The share is valid and accepted. - Valid(Hash), + Valid(Hash, Option), /// 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, Vec), + BlockFound(Hash, Option, Vec, Option), } /// The error variants that can occur during share validation. diff --git a/sv2/channels-sv2/src/server/standard.rs b/sv2/channels-sv2/src/server/standard.rs index 0e07f7e426..198d44a790 100644 --- a/sv2/channels-sv2/src/server/standard.rs +++ b/sv2/channels-sv2/src/server/standard.rs @@ -57,6 +57,7 @@ use bitcoin::{ transaction::{OutPoint, Transaction, TxIn, TxOut, Version as TxVersion}, CompactTarget, Sequence, Target, }; +use braidpool_common::compute_block_hash; use mining_sv2::SubmitSharesStandard; use std::{collections::HashMap, convert::TryInto, marker::PhantomData}; use template_distribution_sv2::{NewTemplate, SetNewPrevHash}; @@ -95,6 +96,7 @@ where job_factory: JobFactory, chain_tip: Option, phantom: PhantomData<&'a ()>, + network_type: String, } impl<'a, J> StandardChannel<'a, J> @@ -122,6 +124,7 @@ where expected_share_per_minute: f32, job_store: J, pool_tag_string: String, + network_type: String, ) -> Result { Self::new( channel_id, @@ -134,6 +137,7 @@ where job_store, Some(pool_tag_string), None, + network_type, ) } @@ -159,6 +163,7 @@ where job_store: J, pool_tag_string: Option, miner_tag_string: String, + network_type: String, ) -> Result { Self::new( channel_id, @@ -171,6 +176,7 @@ where job_store, pool_tag_string, Some(miner_tag_string), + network_type, ) } @@ -187,6 +193,7 @@ where job_store: J, pool_tag_string: Option, miner_tag_string: Option, + network_type: String, ) -> Result { let calculated_target = match hash_rate_to_target(nominal_hashrate.into(), expected_share_per_minute.into()) { @@ -231,6 +238,7 @@ where chain_tip: None, job_store, phantom: PhantomData, + network_type, }) } @@ -621,7 +629,7 @@ where }; // convert the header hash to a target type for easy comparison - let share_hash = header.block_hash(); + let share_hash = compute_block_hash(&header, &self.network_type); let share_raw_hash: [u8; 32] = *share_hash.to_raw_hash().as_ref(); let share_hash_target = Target::from_le_bytes(share_raw_hash); let share_hash_as_diff = share_hash_target.difficulty_float(); @@ -686,6 +694,7 @@ where share_hash.to_raw_hash(), Some(job.get_template().template_id), serialized_coinbase, + None, )); } @@ -707,7 +716,7 @@ where // update the best diff self.share_accounting.update_best_diff(share_hash_as_diff); - Ok(ShareValidationResult::Valid(share_hash.to_raw_hash())) + Ok(ShareValidationResult::Valid(share_hash.to_raw_hash(), None)) } else { Err(ShareValidationError::DoesNotMeetTarget) } @@ -729,7 +738,7 @@ mod tests { standard::StandardChannel, }, }; - use binary_sv2::Sv2Option; + use binary_sv2::{Seq064K, Sv2Option, B016M}; use bitcoin::{transaction::TxOut, Amount, ScriptBuf, Target}; use mining_sv2::{NewMiningJob, SubmitSharesStandard}; use std::convert::TryInto; @@ -766,11 +775,13 @@ mod tests { share_batch_size, expected_share_per_minute, job_store, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, - None, + "mainnet".to_string(), ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); let template = NewTemplate { template_id: 1, future_template: true, @@ -789,6 +800,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 158, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -893,8 +905,10 @@ mod tests { share_batch_size, expected_share_per_minute, job_store, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, - None, + "mainnet".to_string(), ) .unwrap(); @@ -905,7 +919,7 @@ mod tests { ] .into(); let nbits = 503543726; - + let dummy_data: Vec> = Vec::new(); let chain_tip = ChainTip::new(prev_hash, nbits, ntime); let template = NewTemplate { template_id: 1, @@ -925,6 +939,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 158, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -997,11 +1012,13 @@ mod tests { share_batch_size, expected_share_per_minute, job_store, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, - None, + "mainnet".to_string(), ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); // channel target: 04325c53ef368eb04325c53ef368eb04325c53ef368eb04325c53ef368eb0431 let template = NewTemplate { template_id: 1, @@ -1021,6 +1038,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 158, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -1072,7 +1090,7 @@ mod tests { assert!(matches!( res, - Ok(ShareValidationResult::BlockFound(_, _, _)) + Ok(ShareValidationResult::BlockFound(_, _, _, _)) )); assert_eq!( standard_channel.get_share_accounting().get_blocks_found(), @@ -1123,9 +1141,10 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); // channel target: 000aebbc990fff5144366f000aebbc990fff5144366f000aebbc990fff514435 let template = NewTemplate { template_id: 1, @@ -1145,6 +1164,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 158, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -1230,11 +1250,13 @@ mod tests { share_batch_size, expected_share_per_minute, job_store, + //Modifying according to the braidpool or non-braidpool coinbase construction + Some("".to_string()), None, - None, + "mainnet".to_string(), ) .unwrap(); - + let dummy_data: Vec> = Vec::new(); // channel target is: // 0001179d9861a761ffdadd11c307c4fc04eea3a418f7d687584e4434af158205 @@ -1256,6 +1278,7 @@ mod tests { .unwrap(), coinbase_tx_locktime: 158, merkle_path: vec![].try_into().unwrap(), + txs: Seq064K::new(dummy_data).unwrap(), }; // match the original script format used to generate the coinbase_reward_outputs for the @@ -1304,7 +1327,7 @@ mod tests { }; let res = standard_channel.validate_share(valid_share); - assert!(matches!(res, Ok(ShareValidationResult::Valid(_)))); + assert!(matches!(res, Ok(ShareValidationResult::Valid(_, _)))); } #[test] @@ -1335,6 +1358,7 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); @@ -1373,6 +1397,7 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); @@ -1458,6 +1483,7 @@ mod tests { job_store, None, None, + "mainnet".to_string(), ) .unwrap(); diff --git a/sv2/subprotocols/mining/src/submit_shares.rs b/sv2/subprotocols/mining/src/submit_shares.rs index e11eb02a17..46c5be97f0 100644 --- a/sv2/subprotocols/mining/src/submit_shares.rs +++ b/sv2/subprotocols/mining/src/submit_shares.rs @@ -1,5 +1,5 @@ use alloc::{fmt, vec::Vec}; -use binary_sv2::{self, Deserialize, Serialize, Str0255, B032}; +use binary_sv2::{self, Deserialize, Serialize, Str0255, Sv2Option, B032}; use core::convert::TryInto; /// Message used by downstream to send result of its hashing work to an upstream. @@ -70,6 +70,9 @@ pub struct SubmitSharesExtended<'decoder> { /// The size of the provided extranonce must be equal to the negotiated extranonce size from /// channel opening flow. pub extranonce: B032<'decoder>, + /// Template propagation time to downstream for time based statistics at braidpool end + /// committed inside the `UnCommittedMetadata` field + pub time: Sv2Option<'decoder, u32>, } impl fmt::Display for SubmitSharesExtended<'_> { diff --git a/sv2/subprotocols/template-distribution/src/new_template.rs b/sv2/subprotocols/template-distribution/src/new_template.rs index 2206d885a1..3b3b4fffd5 100644 --- a/sv2/subprotocols/template-distribution/src/new_template.rs +++ b/sv2/subprotocols/template-distribution/src/new_template.rs @@ -1,5 +1,5 @@ use alloc::{fmt, vec::Vec}; -use binary_sv2::{self, Deserialize, Seq0255, Serialize, B0255, B064K, U256}; +use binary_sv2::{self, Deserialize, Seq0255, Seq064K, Serialize, B016M, B0255, B064K, U256}; use core::convert::TryInto; /// Message used by an upstream(Template Provider) to provide a new template for downstream to mine @@ -47,6 +47,8 @@ pub struct NewTemplate<'decoder> { pub coinbase_tx_locktime: u32, /// Merkle path hashes ordered from deepest. pub merkle_path: Seq0255<'decoder, U256<'decoder>>, + /// Txids present for the given `NewTemplate` + pub txs: Seq064K<'decoder, B016M<'decoder>>, } impl fmt::Display for NewTemplate<'_> {