iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }