diff --git a/protocols/v2/subprotocols/mining/src/cashu.rs b/protocols/v2/subprotocols/mining/src/cashu.rs index 7cc5fa2b1..4e6a7e7b8 100644 --- a/protocols/v2/subprotocols/mining/src/cashu.rs +++ b/protocols/v2/subprotocols/mining/src/cashu.rs @@ -267,20 +267,56 @@ impl<'a> TryFrom> for KeySet { } // TODO find a better place for this +// TODO make configurable pub fn calculate_work(hash: [u8; 32]) -> u64 { - let mut work = 0u64; + calculate_work_in_range(hash, 36, 64) +} + +/// Calculate work using exponential valuation (2^n) where n is number of leading zero bits. +/// - Each additional zero bit is exponentially more difficult to find +/// - Shares should be valued proportionally to their computational difficulty +/// +/// Parameters: +/// - hash: The 32-byte hash to analyze +/// - min_leading_zeros: Minimum number of leading zero bits required (e.g., 36 for mainnet) +/// - max_representable_bits: Maximum representable difficulty bits (e.g., 64 to compress from 256) +/// +/// Returns the exponential work value: 2^(min(leading_zeros, max_representable_bits)) +pub fn calculate_work_in_range(hash: [u8; 32], min_leading_zeros: u32, max_representable_bits: u32) -> u64 { + let leading_zero_bits = count_leading_zero_bits(hash); + + // Only count work above the minimum threshold + if leading_zero_bits < min_leading_zeros { + return 0; // Below minimum difficulty, no reward + } + + // Cap at maximum representable bits to prevent overflow + let effective_bits = std::cmp::min(leading_zero_bits, max_representable_bits); + + // Use exponential valuation: 2^n where n is effective difficulty bits + // For values above 63, we'd overflow u64, so cap at 2^63 + if effective_bits >= 63 { + 1u64 << 63 // Maximum representable value in u64 + } else { + 1u64 << effective_bits + } +} + +/// Count the number of leading zero bits in a hash +pub fn count_leading_zero_bits(hash: [u8; 32]) -> u32 { + let mut count = 0u32; for byte in hash { if byte == 0 { - work += 8; // Each zero byte adds 8 bits of work + count += 8; // Each zero byte adds 8 bits } else { // Count the leading zeros in the current byte - work += byte.leading_zeros() as u64; + count += byte.leading_zeros(); break; // Stop counting after the first non-zero byte } } - work + count } fn sv2_signing_keys_to_keys(keys: &[Sv2SigningKey]) -> Result { @@ -446,14 +482,71 @@ mod tests { #[test] fn test_calculate_work_expected_values() { - assert_eq!(calculate_work([0u8; 32]), 256); + // All zeros: 256 leading zero bits, capped at 64, gets 2^63 (max u64 value we can represent) + assert_eq!(calculate_work([0u8; 32]), 1u64 << 63); + + // 255 leading zeros (one bit set at the end): gets 2^63 (still capped) let mut one = [0u8; 32]; one[31] = 1; - assert_eq!(calculate_work(one), 255); + assert_eq!(calculate_work(one), 1u64 << 63); + // 251 leading zeros (0x10 = bit 4 set): gets 2^63 (still capped) let mut sixteen = [0u8; 32]; sixteen[31] = 0x10; - assert_eq!(calculate_work(sixteen), 251); + assert_eq!(calculate_work(sixteen), 1u64 << 63); + + // Test configurable function with lower thresholds + // 40 leading zeros: bytes 0-4 are zero (40 bits), byte 5 has bit set + let mut test_hash = [0u8; 32]; + test_hash[5] = 1; // First non-zero bit is at bit 47 (5*8 + 7), so 47 leading zeros + assert_eq!(calculate_work_in_range(test_hash, 36, 50), 1u64 << 47); + + // Below minimum: should get 0 + let mut low_hash = [0u8; 32]; + low_hash[3] = 1; // 3*8 + 7 = 31 leading zeros, below minimum of 36 + assert_eq!(calculate_work_in_range(low_hash, 36, 50), 0); + } + + #[test] + fn test_count_leading_zero_bits() { + // All zeros should have 256 leading zero bits + assert_eq!(count_leading_zero_bits([0u8; 32]), 256); + + let mut one = [0u8; 32]; + one[31] = 1; + assert_eq!(count_leading_zero_bits(one), 255); + + // 0x10 in last byte: 4 leading zeros in that byte + 31*8 from previous bytes = 251 + let mut sixteen = [0u8; 32]; + sixteen[31] = 0x10; + assert_eq!(count_leading_zero_bits(sixteen), 251); + + // First bit set should have 0 leading zeros + let mut first_bit = [0u8; 32]; + first_bit[0] = 0x80; + assert_eq!(count_leading_zero_bits(first_bit), 0); + + // Second bit set should have 1 leading zero + let mut second_bit = [0u8; 32]; + second_bit[0] = 0x40; + assert_eq!(count_leading_zero_bits(second_bit), 1); + } + + #[test] + fn test_exponential_valuation() { + // Test that each additional zero bit doubles the reward + // Use lower thresholds to avoid overflow in tests + + // 10 leading zeros: 2^10 = 1024 + let mut hash1 = [0u8; 32]; + hash1[1] = 0x40; // Sets bit 6 of byte 1: 8 + 1 = 9 leading zeros + hash1[1] = 0x20; // Sets bit 5 of byte 1: 8 + 2 = 10 leading zeros + assert_eq!(calculate_work_in_range(hash1, 5, 20), 1u64 << 10); + + // 11 leading zeros: 2^11 = 2048 (double the previous) + let mut hash2 = [0u8; 32]; + hash2[1] = 0x10; // Sets bit 4 of byte 1: 8 + 3 = 11 leading zeros + assert_eq!(calculate_work_in_range(hash2, 5, 20), 1u64 << 11); }