feature: Implement CompactDifficulty to Work (#838)
* Implement CompactDifficulty to Work * Add Bitcoin test vectors for difficulty
This commit is contained in:
parent
07917421cb
commit
0e21a70b88
|
@ -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<Work> {
|
||||
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 {
|
||||
|
|
|
@ -27,6 +27,16 @@ impl Arbitrary for ExpandedDifficulty {
|
|||
type Strategy = BoxedStrategy<Self>;
|
||||
}
|
||||
|
||||
impl Arbitrary for Work {
|
||||
type Parameters = ();
|
||||
|
||||
fn arbitrary_with(_args: ()) -> Self::Strategy {
|
||||
(any::<u128>()).prop_map(Work).boxed()
|
||||
}
|
||||
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
}
|
||||
|
||||
/// 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<u128>, Option<u128>)] = &[
|
||||
// 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::<CompactDifficulty>()) {
|
||||
// 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.
|
||||
|
|
Loading…
Reference in New Issue