diff --git a/zebra-chain/src/block/difficulty.rs b/zebra-chain/src/block/difficulty.rs index 819b99f3f..9a55d2bab 100644 --- a/zebra-chain/src/block/difficulty.rs +++ b/zebra-chain/src/block/difficulty.rs @@ -93,6 +93,39 @@ impl fmt::Debug for ExpandedDifficulty { } } +/// A 128-bit unsigned "Work" value. +/// +/// Used to calculate the total work for each chain of blocks. +/// +/// Details: +/// +/// The relative value of `Work` is consensus-critical, because it is used to +/// choose the best chain. But its precise value and bit pattern are not +/// consensus-critical. +/// +/// We calculate work values according to the Zcash specification, but store +/// them as u128, rather than the implied u256. We don't expect the total chain +/// work to ever exceed 2^128. The current total chain work for Zcash is 2^58, +/// and Bitcoin adds around 2^91 work per year. (Each extra bit represents twice +/// as much work.) +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] +pub struct Work(u128); + +impl fmt::Debug for Work { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // There isn't a standard way to represent alternate formats for the + // same value. + f.debug_tuple("Work") + // Use hex, because expanded difficulty is in hex. + .field(&format_args!("{:#x}", self.0)) + // Use decimal, to compare with zcashd + .field(&format_args!("{}", self.0)) + // Use log2, to compare with zcashd + .field(&format_args!("{:.5}", (self.0 as f64).log2())) + .finish() + } +} + impl CompactDifficulty { /// CompactDifficulty exponent base. const BASE: u32 = 256; @@ -181,6 +214,28 @@ impl CompactDifficulty { Some(ExpandedDifficulty(result)) } } + + /// Calculate the Work for a compact representation. + /// + /// See `Definition of Work` in the Zcash Specification, and + /// `GetBlockProof()` in zcashd. + /// + /// Returns None if the corresponding ExpandedDifficulty is None. + /// Also returns None on Work overflow, which should be impossible on a + /// valid chain. + pub fn to_work(&self) -> Option { + let expanded = self.to_expanded()?; + // We need to compute `2^256 / (expanded + 1)`, but we can't represent + // 2^256, as it's too large for a u256. However, as 2^256 is at least as + // large as `expanded + 1`, it is equal to + // `((2^256 - expanded - 1) / (expanded + 1)) + 1`, or + let result = (!expanded.0 / (expanded.0 + 1)) + 1; + if result <= u128::MAX.into() { + return Some(Work(result.as_u128())); + } + + None + } } impl ExpandedDifficulty { diff --git a/zebra-chain/src/block/difficulty/tests.rs b/zebra-chain/src/block/difficulty/tests.rs index 45d447848..7b552fb78 100644 --- a/zebra-chain/src/block/difficulty/tests.rs +++ b/zebra-chain/src/block/difficulty/tests.rs @@ -27,6 +27,16 @@ impl Arbitrary for ExpandedDifficulty { type Strategy = BoxedStrategy; } +impl Arbitrary for Work { + type Parameters = (); + + fn arbitrary_with(_args: ()) -> Self::Strategy { + (any::()).prop_map(Work).boxed() + } + + type Strategy = BoxedStrategy; +} + /// Test debug formatting. #[test] fn debug_format() { @@ -57,6 +67,20 @@ fn debug_format() { format!("{:?}", ExpandedDifficulty(U256::MAX)), "ExpandedDifficulty(\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\")" ); + + assert_eq!(format!("{:?}", Work(0)), "Work(0x0, 0, -inf)"); + assert_eq!( + format!("{:?}", Work(u8::MAX as u128)), + "Work(0xff, 255, 7.99435)" + ); + assert_eq!( + format!("{:?}", Work(u64::MAX as u128)), + "Work(0xffffffffffffffff, 18446744073709551615, 64.00000)" + ); + assert_eq!( + format!("{:?}", Work(u128::MAX)), + "Work(0xffffffffffffffffffffffffffffffff, 340282366920938463463374607431768211455, 128.00000)" + ); } /// Test zero values for CompactDifficulty. @@ -66,22 +90,29 @@ fn compact_zero() { let natural_zero = CompactDifficulty(0); assert_eq!(natural_zero.to_expanded(), None); + assert_eq!(natural_zero.to_work(), None); // Small value zeroes let small_zero_1 = CompactDifficulty(1); assert_eq!(small_zero_1.to_expanded(), None); + assert_eq!(small_zero_1.to_work(), None); let small_zero_max = CompactDifficulty(UNSIGNED_MANTISSA_MASK); assert_eq!(small_zero_max.to_expanded(), None); + assert_eq!(small_zero_max.to_work(), None); // Special-cased zeroes, negative in the floating-point representation let sc_zero = CompactDifficulty(SIGN_BIT); assert_eq!(sc_zero.to_expanded(), None); + assert_eq!(sc_zero.to_work(), None); let sc_zero_next = CompactDifficulty(SIGN_BIT + 1); assert_eq!(sc_zero_next.to_expanded(), None); + assert_eq!(sc_zero_next.to_work(), None); let sc_zero_high = CompactDifficulty((1 << PRECISION) - 1); assert_eq!(sc_zero_high.to_expanded(), None); + assert_eq!(sc_zero_high.to_work(), None); let sc_zero_max = CompactDifficulty(u32::MAX); assert_eq!(sc_zero_max.to_expanded(), None); + assert_eq!(sc_zero_max.to_work(), None); } /// Test extreme values for CompactDifficulty. @@ -91,32 +122,44 @@ fn compact_extremes() { // Values equal to one let expanded_one = Some(ExpandedDifficulty(U256::one())); + let work_one = None; let one = CompactDifficulty(OFFSET as u32 * (1 << PRECISION) + 1); assert_eq!(one.to_expanded(), expanded_one); + assert_eq!(one.to_work(), work_one); let another_one = CompactDifficulty((1 << PRECISION) + (1 << 16)); assert_eq!(another_one.to_expanded(), expanded_one); + assert_eq!(another_one.to_work(), work_one); // Maximum mantissa let expanded_mant = Some(ExpandedDifficulty(UNSIGNED_MANTISSA_MASK.into())); + let work_mant = None; let mant = CompactDifficulty(OFFSET as u32 * (1 << PRECISION) + UNSIGNED_MANTISSA_MASK); assert_eq!(mant.to_expanded(), expanded_mant); + assert_eq!(mant.to_work(), work_mant); // Maximum valid exponent let exponent: U256 = (31 * 8).into(); - let expanded_exp = Some(ExpandedDifficulty(U256::from(2).pow(exponent))); + let u256_exp = U256::from(2).pow(exponent); + let expanded_exp = Some(ExpandedDifficulty(u256_exp)); + let work_exp = Some(Work( + ((U256::MAX - u256_exp) / (u256_exp + 1) + 1).as_u128(), + )); let exp = CompactDifficulty((31 + OFFSET as u32) * (1 << PRECISION) + 1); assert_eq!(exp.to_expanded(), expanded_exp); + assert_eq!(exp.to_work(), work_exp); // Maximum valid mantissa and exponent let exponent: U256 = (29 * 8).into(); - let expanded_me = U256::from(UNSIGNED_MANTISSA_MASK) * U256::from(2).pow(exponent); - let expanded_me = Some(ExpandedDifficulty(expanded_me)); + let u256_me = U256::from(UNSIGNED_MANTISSA_MASK) * U256::from(2).pow(exponent); + let expanded_me = Some(ExpandedDifficulty(u256_me)); + let work_me = Some(Work((!u256_me / (u256_me + 1) + 1).as_u128())); let me = CompactDifficulty((31 + 1) * (1 << PRECISION) + UNSIGNED_MANTISSA_MASK); assert_eq!(me.to_expanded(), expanded_me); + assert_eq!(me.to_work(), work_me); // Maximum value, at least according to the spec // @@ -127,6 +170,68 @@ fn compact_extremes() { // zcashd rejects these blocks without comparing the hash. let difficulty_max = CompactDifficulty(u32::MAX & !SIGN_BIT); assert_eq!(difficulty_max.to_expanded(), None); + assert_eq!(difficulty_max.to_work(), None); + + // Bitcoin test vectors for CompactDifficulty + // See https://developer.bitcoin.org/reference/block_chain.html#target-nbits + // These values are not in the table below, because they do not fit in u128 + // + // The minimum difficulty on the bitcoin mainnet and testnet + let difficulty_btc_main = CompactDifficulty(0x1d00ffff); + let u256_btc_main = U256::from(0xffff) << 208; + let expanded_btc_main = Some(ExpandedDifficulty(u256_btc_main)); + let work_btc_main = Some(Work(0x100010001)); + assert_eq!(difficulty_btc_main.to_expanded(), expanded_btc_main); + assert_eq!(difficulty_btc_main.to_work(), work_btc_main); + + // The minimum difficulty in bitcoin regtest + // This is also the easiest respesentable difficulty + let difficulty_btc_reg = CompactDifficulty(0x207fffff); + let u256_btc_reg = U256::from(0x7fffff) << 232; + let expanded_btc_reg = Some(ExpandedDifficulty(u256_btc_reg)); + let work_btc_reg = Some(Work(0x2)); + assert_eq!(difficulty_btc_reg.to_expanded(), expanded_btc_reg); + assert_eq!(difficulty_btc_reg.to_work(), work_btc_reg); +} + +/// Bitcoin test vectors for CompactDifficulty, and their corresponding +/// ExpandedDifficulty and Work values. +/// See https://developer.bitcoin.org/reference/block_chain.html#target-nbits +static COMPACT_DIFFICULTY_CASES: &[(u32, Option, Option)] = &[ + // These Work values will never happen in practice, because the corresponding + // difficulties are extremely high. So it is ok for us to reject them. + (0x01003456, None /* 0x00 */, None), + (0x01123456, Some(0x12), None), + (0x02008000, Some(0x80), None), + (0x05009234, Some(0x92340000), None), + (0x04923456, None /* -0x12345600 */, None), + (0x04123456, Some(0x12345600), None), +]; + +/// Test Bitcoin test vectors for CompactDifficulty. +#[test] +#[spandoc::spandoc] +fn compact_bitcoin_test_vectors() { + zebra_test::init(); + + // We use two spans, so we can diagnose conversion panics, and mismatching results + for (compact, expected_expanded, expected_work) in COMPACT_DIFFICULTY_CASES.iter().cloned() { + /// SPANDOC: Convert compact to expanded and work {?compact, ?expected_expanded, ?expected_work} + { + let expected_expanded = expected_expanded.map(U256::from).map(ExpandedDifficulty); + let expected_work = expected_work.map(Work); + + let compact = CompactDifficulty(compact); + let actual_expanded = compact.to_expanded(); + let actual_work = compact.to_work(); + + /// SPANDOC: Test that compact produces the expected expanded and work {?compact, ?expected_expanded, ?actual_expanded, ?expected_work, ?actual_work} + { + assert_eq!(actual_expanded, expected_expanded); + assert_eq!(actual_work, expected_work); + } + } + } } /// Test blocks using CompactDifficulty. @@ -173,6 +278,15 @@ fn block_difficulty() -> Result<(), Report> { assert!(hash > one); assert!(hash < max_value); } + + /// SPANDOC: Calculate the work for mainnet block {?height} + let _work = block + .header + .difficulty_threshold + .to_work() + .expect("Chain blocks have valid work."); + + // TODO: check work comparison operators and cumulative work addition } Ok(()) @@ -233,8 +347,8 @@ fn expanded_hash_order() -> Result<(), Report> { } proptest! { - /// Check that CompactDifficulty expands without panicking, and compares - /// correctly. + /// Check that CompactDifficulty expands without panicking, and compares + /// correctly. Also check that the work conversion does not panic. #[test] fn prop_compact_expand(compact in any::()) { // TODO: round-trip test, once we have ExpandedDifficulty::to_compact() @@ -247,6 +361,9 @@ proptest! { prop_assert!(expanded >= hash_zero); prop_assert!(expanded <= hash_max); } + + let _work = compact.to_work(); + // TODO: work comparison and addition } /// Check that a random ExpandedDifficulty compares correctly with fixed BlockHeaderHashes.