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