From 3df1fe6de98d2dc8edc18347713828872531cbb8 Mon Sep 17 00:00:00 2001 From: plebhash Date: Thu, 16 Jul 2026 09:41:14 -0300 Subject: [PATCH 1/2] add Sv2 string to coinbase tag standard --- sv2/channels-sv2/src/server/extended.rs | 101 +++++++++++--------- sv2/channels-sv2/src/server/group.rs | 15 +-- sv2/channels-sv2/src/server/jobs/factory.rs | 29 +++--- sv2/channels-sv2/src/server/standard.rs | 56 ++++++----- 4 files changed, 117 insertions(+), 84 deletions(-) diff --git a/sv2/channels-sv2/src/server/extended.rs b/sv2/channels-sv2/src/server/extended.rs index 26b5dcbc47..974a502ed3 100644 --- a/sv2/channels-sv2/src/server/extended.rs +++ b/sv2/channels-sv2/src/server/extended.rs @@ -115,8 +115,8 @@ impl<'a> ExtendedChannel<'a> { /// Returns an error if target/difficulty parameters are invalid or extranonce prefix /// requirements are not met. /// - /// For non-JD jobs, `pool_tag_string` is added to the coinbase scriptSig in between `/` - /// and `//` delimiters: `/pool_tag_string//` + /// For non-JD jobs, `pool_tag_string` is added to the coinbase scriptSig as + /// `Sv2/pool_tag_string//`. #[allow(clippy::too_many_arguments)] pub fn new_for_pool( channel_id: u32, @@ -153,8 +153,8 @@ impl<'a> ExtendedChannel<'a> { /// Returns an error if target/difficulty parameters are invalid or extranonce prefix /// requirements are not met. /// - /// The `pool_tag_string` and `miner_tag_string` are added to the coinbase scriptSig in between - /// `/` delimiters: `/pool_tag_string/miner_tag_string/` + /// The `pool_tag_string` and `miner_tag_string` are added to the coinbase scriptSig as + /// `Sv2/pool_tag_string/miner_tag_string/`. #[allow(clippy::too_many_arguments)] pub fn new_for_job_declaration_client( channel_id: u32, @@ -220,6 +220,7 @@ impl<'a> ExtendedChannel<'a> { let script_sig_size = 5 + // BIP34 1 + // OP_PUSHBYTES + 3 + // "Sv2" 3 + // `/` delimiters pool_tag.as_ref().map_or(0, |s| s.len()) + miner_tag.as_ref().map_or(0, |s| s.len()) + @@ -988,7 +989,8 @@ mod tests { version_rolling_allowed: true, coinbase_tx_prefix: vec![ 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 38, 82, 0, 3, 47, 47, 47, 31, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 41, 82, 0, 6, 83, 118, 50, 47, 47, + 47, 31, ] .try_into() .unwrap(), @@ -1139,7 +1141,8 @@ mod tests { version_rolling_allowed: true, coinbase_tx_prefix: vec![ 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 38, 82, 0, 3, 47, 47, 47, 31, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 41, 82, 0, 6, 83, 118, 50, 47, 47, + 47, 31, ] .try_into() .unwrap(), @@ -1323,25 +1326,30 @@ mod tests { .on_new_template(template.clone(), coinbase_reward_outputs) .unwrap(); - // this share has hash 4c68f79a585c8b609e9b43113f73311eada20ec88a70a999406267db3499f1d9 - // which satisfies network target - // 7fffff0000000000000000000000000000000000000000000000000000000000 - let share_valid_block = SubmitSharesExtended { - channel_id, - sequence_number: 0, - job_id: 1, - nonce: 8, - ntime: 1745596971, - version: 536870912, - extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), - }; + // Find a share that satisfies the network target. + let mut share_valid_block = None; + for nonce in 0..1_000_000u32 { + let candidate = SubmitSharesExtended { + channel_id, + sequence_number: 0, + job_id: 1, + nonce, + ntime: 1745596971, + version: 536870912, + extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + }; - let res = channel.validate_share(share_valid_block.clone()); + if matches!( + channel.validate_share(candidate.clone()), + Ok(ShareValidationResult::BlockFound(_, _, _)) + ) { + share_valid_block = Some(candidate); + break; + } + } - assert!(matches!( - res, - Ok(ShareValidationResult::BlockFound(_, _, _)) - )); + let share_valid_block = share_valid_block + .expect("must find a block-finding share for this easy network target"); assert_eq!(channel.get_share_accounting().get_blocks_found(), 1); // re-submitting the same valid block must be rejected as duplicate @@ -1559,23 +1567,30 @@ mod tests { .on_new_template(template.clone(), coinbase_reward_outputs) .unwrap(); - // this share has hash 000004f9d35777e4d56eedc20b1d05d251a7c0ed0b4e3013b5a809852844e218 - // which does meet the channel target - // 0001179d9861a761ffdadd11c307c4fc04eea3a418f7d687584e4434af158205 - // but does not meet network target - // 000000000000d7c0000000000000000000000000000000000000000000000000 - let valid_share = SubmitSharesExtended { - channel_id, - sequence_number: 1, - job_id: 1, - nonce: 51208, - ntime: 1745611105, - version: 536870912, - extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), - }; + // Find a share that meets the channel target but not the network target. + let mut valid_share = None; + for nonce in 0..2_000_000u32 { + let candidate = SubmitSharesExtended { + channel_id, + sequence_number: 1, + job_id: 1, + nonce, + ntime: 1745611105, + version: 536870912, + extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + }; - let res = channel.validate_share(valid_share); - assert!(matches!(res, Ok(ShareValidationResult::Valid(_)))); + if matches!( + channel.validate_share(candidate.clone()), + Ok(ShareValidationResult::Valid(_)) + ) { + valid_share = Some(candidate); + break; + } + } + + let valid_share = + valid_share.expect("must find a valid (non-block) share under the channel target"); // try to cheat by re-submitting the same share // with a different sequence number @@ -1583,10 +1598,10 @@ mod tests { channel_id, sequence_number: 2, job_id: 1, - nonce: 51208, - ntime: 1745611105, - version: 536870912, - extranonce: vec![1, 0, 0, 0, 0, 0, 0, 0].try_into().unwrap(), + nonce: valid_share.nonce, + ntime: valid_share.ntime, + version: valid_share.version, + extranonce: valid_share.extranonce, }; let res = channel.validate_share(repeated_share); diff --git a/sv2/channels-sv2/src/server/group.rs b/sv2/channels-sv2/src/server/group.rs index 9a1b24b2f0..85a5943087 100644 --- a/sv2/channels-sv2/src/server/group.rs +++ b/sv2/channels-sv2/src/server/group.rs @@ -75,8 +75,8 @@ impl<'a> GroupChannel<'a> { /// Initializes the group channel state with the provided group channel ID. /// The job factory is initialized with version rolling enabled. /// - /// For non-JD jobs, `pool_tag_string` is added to the coinbase scriptSig in between `/` - /// and `//` delimiters: `/pool_tag_string//` + /// For non-JD jobs, `pool_tag_string` is added to the coinbase scriptSig as + /// `Sv2/pool_tag_string//`. pub fn new_for_pool( group_channel_id: u32, full_extranonce_size: usize, @@ -99,8 +99,8 @@ impl<'a> GroupChannel<'a> { /// Returns an error if target/difficulty parameters are invalid or extranonce prefix /// requirements are not met. /// - /// The `pool_tag_string` and `miner_tag_string` are added to the coinbase scriptSig in between - /// `/` delimiters: `/pool_tag_string/miner_tag_string/` + /// The `pool_tag_string` and `miner_tag_string` are added to the coinbase scriptSig as + /// `Sv2/pool_tag_string/miner_tag_string/`. pub fn new_for_job_declaration_client( group_channel_id: u32, full_extranonce_size: usize, @@ -125,6 +125,7 @@ impl<'a> GroupChannel<'a> { ) -> Result { let script_sig_size = 5 + // BIP34 1 + // OP_PUSHBYTES + 3 + // "Sv2" 3 + // `/` delimiters pool_tag.as_ref().map_or(0, |s| s.len()) + miner_tag.as_ref().map_or(0, |s| s.len()) + @@ -394,7 +395,8 @@ mod tests { version_rolling_allowed: true, coinbase_tx_prefix: vec![ 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 39, 82, 0, 3, 47, 47, 47, 32, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 42, 82, 0, 6, 83, 118, 50, 47, 47, + 47, 32, ] .try_into() .unwrap(), @@ -520,7 +522,8 @@ mod tests { version_rolling_allowed: true, coinbase_tx_prefix: vec![ 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 39, 82, 0, 3, 47, 47, 47, 32, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 42, 82, 0, 6, 83, 118, 50, 47, 47, + 47, 32, ] .try_into() .unwrap(), diff --git a/sv2/channels-sv2/src/server/jobs/factory.rs b/sv2/channels-sv2/src/server/jobs/factory.rs index 7d3ee128f3..bb2f614778 100644 --- a/sv2/channels-sv2/src/server/jobs/factory.rs +++ b/sv2/channels-sv2/src/server/jobs/factory.rs @@ -93,14 +93,15 @@ impl JobFactory { } } - /// Returns a byte vector with the OP_PUSHBYTES opcode and the pool+miner tag. + /// Returns a byte vector with the OP_PUSHBYTES opcode and `Sv2///` tag. /// /// The character `/` is used as a delimiter. /// - /// If no pool or miner tag is provided, the delimiters are still added. + /// If pool and/or miner tags are not provided, delimiters are still kept + /// (e.g. `Sv2///` when both are missing). pub fn op_pushbytes_pool_miner_tag(&self) -> Result, JobFactoryError> { let mut pool_miner_tag = vec![]; - pool_miner_tag.extend_from_slice(b"/"); + pool_miner_tag.extend_from_slice(b"Sv2/"); if let Some(pool_tag_string) = &self.pool_tag_string { pool_miner_tag.extend_from_slice(pool_tag_string.as_bytes()); } @@ -627,8 +628,9 @@ impl JobFactory { )?; let serialized_coinbase = serialize(&coinbase); - // Calculate the full pool/miner tag length including delimiters and OP_PUSHBYTES opcode + // Calculate the full `Sv2///` tag length including OP_PUSHBYTES opcode. let pool_miner_tag_len = 1 // OP_PUSHBYTES opcode + + 3 // "Sv2" + 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()); @@ -661,8 +663,9 @@ impl JobFactory { )?; let serialized_coinbase = serialize(&coinbase); - // Calculate the full pool/miner tag length including delimiters and OP_PUSHBYTES opcode + // Calculate the full `Sv2///` tag length including OP_PUSHBYTES opcode. let pool_miner_tag_len = 1 // OP_PUSHBYTES opcode + + 3 // "Sv2" + 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()); @@ -757,11 +760,12 @@ mod tests { min_ntime: Sv2Option::new(None), version: 536870912, version_rolling_allowed: true, - // contains scriptSig with /Stratum V2 SRI Pool// + // contains scriptSig with Sv2/Stratum V2 SRI Pool// coinbase_tx_prefix: vec![ 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 58, 82, 0, 22, 47, 83, 116, 114, 97, - 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 47, 47, 32, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 61, 82, 0, 25, 83, 118, 50, 47, 83, + 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 47, + 47, 32, ] .try_into() .unwrap(), @@ -864,12 +868,13 @@ mod tests { min_ntime: Sv2Option::new(Some(1746839905)), version: 536870912, version_rolling_allowed: true, - // contains scriptSig with /Stratum V2 SRI Pool/Stratum V2 SRI Miner/ + // contains scriptSig with Sv2/Stratum V2 SRI Pool/Stratum V2 SRI Miner/ coinbase_tx_prefix: vec![ 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 78, 82, 0, 42, 47, 83, 116, 114, 97, - 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 47, 83, 116, 114, - 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 77, 105, 110, 101, 114, 47, 32, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 81, 82, 0, 45, 83, 118, 50, 47, 83, + 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 47, + 83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 77, 105, 110, 101, + 114, 47, 32, ] .try_into() .unwrap(), diff --git a/sv2/channels-sv2/src/server/standard.rs b/sv2/channels-sv2/src/server/standard.rs index dd3eca4a8f..6ee9fc5449 100644 --- a/sv2/channels-sv2/src/server/standard.rs +++ b/sv2/channels-sv2/src/server/standard.rs @@ -110,8 +110,8 @@ impl<'a> StandardChannel<'a> { /// Returns an error if target/difficulty parameters are invalid or extranonce prefix /// requirements are not met. /// - /// For non-JD jobs, `pool_tag_string` is added to the coinbase scriptSig in between `/` - /// and `//` delimiters: `/pool_tag_string//` + /// For non-JD jobs, `pool_tag_string` is added to the coinbase scriptSig as + /// `Sv2/pool_tag_string//`. #[allow(clippy::too_many_arguments)] pub fn new_for_pool( channel_id: u32, @@ -144,8 +144,8 @@ impl<'a> StandardChannel<'a> { /// Returns an error if target/difficulty parameters are invalid or extranonce prefix /// requirements are not met. /// - /// The `pool_tag_string` and `miner_tag_string` are added to the coinbase scriptSig in between - /// `/` delimiters: `/pool_tag_string/miner_tag_string/` + /// The `pool_tag_string` and `miner_tag_string` are added to the coinbase scriptSig as + /// `Sv2/pool_tag_string/miner_tag_string/`. #[allow(clippy::too_many_arguments)] pub fn new_for_job_declaration_client( channel_id: u32, @@ -205,6 +205,7 @@ impl<'a> StandardChannel<'a> { let script_sig_size = 5 + // BIP34 1 + // OP_PUSHBYTES + 3 + // "Sv2" 3 + // `/` delimiters pool_tag_string.as_ref().map_or(0, |s| s.len()) + miner_tag_string.as_ref().map_or(0, |s| s.len()) + @@ -838,8 +839,8 @@ mod tests { channel_id: standard_channel_id, job_id: 1, merkle_root: [ - 213, 241, 108, 144, 69, 96, 29, 8, 222, 2, 135, 14, 213, 87, 81, 21, 140, 98, 42, - 221, 221, 174, 219, 248, 106, 52, 168, 88, 18, 146, 186, 71, + 244, 175, 251, 103, 170, 111, 89, 111, 245, 253, 167, 99, 126, 231, 170, 153, 174, + 69, 251, 243, 5, 119, 145, 76, 19, 107, 215, 155, 166, 36, 228, 210, ] .into(), version: 536870912, @@ -969,8 +970,8 @@ mod tests { channel_id: standard_channel_id, job_id: 1, merkle_root: [ - 213, 241, 108, 144, 69, 96, 29, 8, 222, 2, 135, 14, 213, 87, 81, 21, 140, 98, 42, - 221, 221, 174, 219, 248, 106, 52, 168, 88, 18, 146, 186, 71, + 244, 175, 251, 103, 170, 111, 89, 111, 245, 253, 167, 99, 126, 231, 170, 153, 174, + 69, 251, 243, 5, 119, 145, 76, 19, 107, 215, 155, 166, 36, 228, 210, ] .into(), version: 536870912, @@ -1301,22 +1302,31 @@ mod tests { .on_new_template(template.clone(), coinbase_reward_outputs) .unwrap(); - // this share has hash 0000d603073772ba60af5922486242a6adb74cdf5baec768c7bd684977852cd8 - // which does meet the channel target - // 0001179d9861a761ffdadd11c307c4fc04eea3a418f7d687584e4434af158205 - // but does not meet network target - // 000000000000d7c0000000000000000000000000000000000000000000000000 - let valid_share = SubmitSharesStandard { - channel_id: standard_channel_id, - sequence_number: 1, - job_id: 1, - nonce: 134870, - ntime: 1745611105, - version: 536870912, - }; - let res = standard_channel.validate_share(valid_share); + // Find a share that meets the channel target but not the network target. + let mut found_valid_share = false; + for nonce in 0..2_000_000u32 { + let candidate = SubmitSharesStandard { + channel_id: standard_channel_id, + sequence_number: 1, + job_id: 1, + nonce, + ntime: 1745611105, + version: 536870912, + }; - assert!(matches!(res, Ok(ShareValidationResult::Valid(_)))); + if matches!( + standard_channel.validate_share(candidate), + Ok(ShareValidationResult::Valid(_)) + ) { + found_valid_share = true; + break; + } + } + + assert!( + found_valid_share, + "must find a valid (non-block) share under the channel target" + ); } #[test] From 4d3b1490a2a66bb76f05640605220475d3ae27a8 Mon Sep 17 00:00:00 2001 From: plebhash Date: Thu, 16 Jul 2026 09:45:27 -0300 Subject: [PATCH 2/2] bump channels_sv2 --- Cargo.lock | 2 +- sv2/channels-sv2/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5fbc16a86..b05fcad8e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,7 +276,7 @@ dependencies = [ [[package]] name = "channels_sv2" -version = "7.0.0" +version = "7.1.0" dependencies = [ "binary_sv2", "bitcoin", diff --git a/sv2/channels-sv2/Cargo.toml b/sv2/channels-sv2/Cargo.toml index b3d2636ba5..12e253feb9 100644 --- a/sv2/channels-sv2/Cargo.toml +++ b/sv2/channels-sv2/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "channels_sv2" -version = "7.0.0" +version = "7.1.0" authors = ["The Stratum V2 Developers"] edition = "2021" readme = "README.md"