iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }