stratum.rs (7532B)
1 use anyhow::{Context, Result}; 2 use sha2::{Digest, Sha256}; 3 4 use super::{ 5 AddressNetwork, AddressVersion, HASH_BYTES, TransactionSigningDomain, decode_hex_array, 6 decode_versioned_address, hex_encode, mine_signing_bytes, 7 validation::{decode_canonical_hex_array, validate_address, validate_hash}, 8 }; 9 10 pub const STRATUM_EXTRANONCE1_HEX: &str = "00000000"; 11 pub const STRATUM_EXTRANONCE2_SIZE: usize = 4; 12 pub(super) const STRATUM_MINE_HEADER_BYTES: usize = 80; 13 14 const STRATUM_MINE_VERSION: [u8; 4] = [1, 0, 0, 0]; 15 const STRATUM_MINE_NTIME: [u8; 4] = [0, 0, 0, 0]; 16 17 #[derive(Clone, Debug, Eq, PartialEq)] 18 pub struct StratumMineTemplate { 19 pub recipient: String, 20 pub anchor: String, 21 pub salt: u64, 22 pub difficulty_bits: u32, 23 pub coinbase_prefix: Vec<u8>, 24 pub version_hex: String, 25 pub prev_hash_hex: String, 26 pub nbits_hex: String, 27 pub ntime_hex: String, 28 } 29 30 impl StratumMineTemplate { 31 pub fn coinb1_hex(&self) -> String { 32 hex_encode(&self.coinbase_prefix) 33 } 34 } 35 36 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 37 pub struct StratumMineShare { 38 pub extranonce2: [u8; 4], 39 pub header_nonce: [u8; 4], 40 } 41 42 pub fn pack_stratum_nonce(extranonce2: [u8; 4], header_nonce: [u8; 4]) -> u64 { 43 let extra = u32::from_be_bytes(extranonce2) as u64; 44 let nonce = u32::from_le_bytes(header_nonce) as u64; 45 (extra << 32) | nonce 46 } 47 48 fn unpack_stratum_nonce(nonce: u64) -> ([u8; 4], [u8; 4]) { 49 ( 50 ((nonce >> 32) as u32).to_be_bytes(), 51 (nonce as u32).to_le_bytes(), 52 ) 53 } 54 55 fn stratum_coinbase_prefix( 56 domain: &TransactionSigningDomain, 57 recipient: &str, 58 anchor: &str, 59 salt: u64, 60 difficulty_bits: u32, 61 ) -> Result<Vec<u8>> { 62 if domain.is_chain_bound() { 63 mine_signing_bytes(domain, recipient, anchor, salt, 0, difficulty_bits) 64 } else { 65 Ok( 66 format!("iuna-stratum-mine:{recipient}:{anchor}:{salt}:{difficulty_bits}:") 67 .into_bytes(), 68 ) 69 } 70 } 71 72 fn stratum_coinbase_bytes( 73 domain: &TransactionSigningDomain, 74 recipient: &str, 75 anchor: &str, 76 salt: u64, 77 nonce: u64, 78 difficulty_bits: u32, 79 ) -> Result<Vec<u8>> { 80 let (extranonce2, _) = unpack_stratum_nonce(nonce); 81 let mut coinbase = stratum_coinbase_prefix(domain, recipient, anchor, salt, difficulty_bits)?; 82 coinbase.extend_from_slice(&[0, 0, 0, 0]); 83 coinbase.extend_from_slice(&extranonce2); 84 Ok(coinbase) 85 } 86 87 fn double_sha256(bytes: &[u8]) -> [u8; 32] { 88 let first = Sha256::digest(bytes); 89 let second = Sha256::digest(first); 90 second.into() 91 } 92 93 pub(super) fn stratum_mine_header_bytes( 94 domain: &TransactionSigningDomain, 95 recipient: &str, 96 anchor: &str, 97 salt: u64, 98 nonce: u64, 99 difficulty_bits: u32, 100 ) -> Result<[u8; 80]> { 101 let mut header = [0_u8; STRATUM_MINE_HEADER_BYTES]; 102 header[0..4].copy_from_slice(&STRATUM_MINE_VERSION); 103 let anchor_bytes = if domain.is_chain_bound() { 104 decode_canonical_hex_array::<HASH_BYTES>(anchor) 105 } else { 106 decode_hex_array::<HASH_BYTES>(anchor) 107 } 108 .context("mine transaction anchor is not hex")?; 109 header[4..36].copy_from_slice(&anchor_bytes); 110 let merkle_root = double_sha256(&stratum_coinbase_bytes( 111 domain, 112 recipient, 113 anchor, 114 salt, 115 nonce, 116 difficulty_bits, 117 )?); 118 header[36..68].copy_from_slice(&merkle_root); 119 header[68..72].copy_from_slice(&STRATUM_MINE_NTIME); 120 header[72..76].copy_from_slice(&difficulty_bits.to_le_bytes()); 121 let (_, header_nonce) = unpack_stratum_nonce(nonce); 122 header[76..80].copy_from_slice(&header_nonce); 123 Ok(header) 124 } 125 126 pub(super) fn stratum_mine_signature(header: &[u8; 80]) -> String { 127 let mut digest = double_sha256(header); 128 digest.reverse(); 129 hex_encode(digest) 130 } 131 132 pub(super) fn stratum_mine_template( 133 domain: &TransactionSigningDomain, 134 recipient: impl Into<String>, 135 anchor: &str, 136 salt: u64, 137 difficulty_bits: u32, 138 ) -> Result<StratumMineTemplate> { 139 let recipient = recipient.into(); 140 if validate_address(&recipient, "mine recipient").is_err() { 141 let network = if recipient.starts_with("tiuna1") { 142 AddressNetwork::Testnet 143 } else { 144 AddressNetwork::Mainnet 145 }; 146 let decoded = 147 decode_versioned_address(&recipient, network).context("mine recipient is invalid")?; 148 if decoded.version != AddressVersion::HybridKeyCommitment { 149 anyhow::bail!("mine recipient must use a legacy or hybrid address"); 150 } 151 } 152 validate_hash(anchor, "mine transaction anchor")?; 153 let anchor_bytes = if domain.is_chain_bound() { 154 decode_canonical_hex_array::<HASH_BYTES>(anchor) 155 } else { 156 decode_hex_array::<HASH_BYTES>(anchor) 157 } 158 .context("mine transaction anchor is not hex")?; 159 Ok(StratumMineTemplate { 160 recipient: recipient.clone(), 161 anchor: anchor.to_string(), 162 salt, 163 difficulty_bits, 164 coinbase_prefix: stratum_coinbase_prefix( 165 domain, 166 &recipient, 167 anchor, 168 salt, 169 difficulty_bits, 170 )?, 171 version_hex: hex_encode(STRATUM_MINE_VERSION), 172 prev_hash_hex: hex_encode(anchor_bytes), 173 nbits_hex: hex_encode(difficulty_bits.to_le_bytes()), 174 ntime_hex: hex_encode(STRATUM_MINE_NTIME), 175 }) 176 } 177 178 pub(super) fn hash_meets_difficulty(hash: &str, difficulty_bits: u32) -> bool { 179 let full_zero_nibbles = (difficulty_bits / 4) as usize; 180 let remaining_bits = difficulty_bits % 4; 181 if hash.len() < full_zero_nibbles + usize::from(remaining_bits > 0) { 182 return false; 183 } 184 if !hash.as_bytes()[..full_zero_nibbles] 185 .iter() 186 .all(|byte| *byte == b'0') 187 { 188 return false; 189 } 190 if remaining_bits == 0 { 191 return true; 192 } 193 let Some(next) = hash.as_bytes().get(full_zero_nibbles).copied() else { 194 return false; 195 }; 196 let Some(value) = (next as char).to_digit(16) else { 197 return false; 198 }; 199 value < (1 << (4 - remaining_bits)) 200 } 201 202 #[cfg(test)] 203 mod tests { 204 use super::{ 205 STRATUM_EXTRANONCE1_HEX, STRATUM_EXTRANONCE2_SIZE, hash_meets_difficulty, 206 pack_stratum_nonce, stratum_mine_header_bytes, 207 }; 208 use crate::domain::{TransactionSigningDomain, Wallet}; 209 210 #[test] 211 fn stratum_nonce_packs_extranonce_big_endian_and_header_nonce_little_endian() { 212 let nonce = pack_stratum_nonce([0x01, 0x02, 0x03, 0x04], [0x08, 0x07, 0x06, 0x05]); 213 214 assert_eq!(nonce, 0x0102_0304_0506_0708); 215 } 216 217 #[test] 218 fn stratum_public_extranonce_contract_is_stable() { 219 assert_eq!(STRATUM_EXTRANONCE1_HEX, "00000000"); 220 assert_eq!(STRATUM_EXTRANONCE2_SIZE, 4); 221 } 222 223 #[test] 224 fn stratum_header_rejects_non_hex_anchor() { 225 let wallet = Wallet::from_seed("stratum-invalid-anchor-recipient"); 226 let domain = TransactionSigningDomain::new("test", "0".repeat(64)); 227 let error = 228 stratum_mine_header_bytes(&domain, wallet.address(), "not-hex", 0, 0, 12).unwrap_err(); 229 230 assert!( 231 error 232 .to_string() 233 .contains("mine transaction anchor is not hex") 234 ); 235 } 236 237 #[test] 238 fn difficulty_check_handles_nibble_and_partial_nibble_targets() { 239 assert!(hash_meets_difficulty("00f", 8)); 240 assert!(!hash_meets_difficulty("010", 8)); 241 assert!(hash_meets_difficulty("1ff", 3)); 242 assert!(!hash_meets_difficulty("2ff", 3)); 243 } 244 }