From 7f19d4868ce330016a43410431c43313d1352a66 Mon Sep 17 00:00:00 2001 From: haoshengzhen Date: Fri, 19 Jun 2026 00:02:41 +0800 Subject: [PATCH] Fix Digest hex encoding to preserve numeric ordering Signed-off-by: haoshengzhen --- twenty-first/src/tip5/digest.rs | 56 +++++++++++++------ twenty-first/src/tip5/mod.rs | 4 +- .../src/util_types/mmr/mmr_accumulator.rs | 8 +-- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/twenty-first/src/tip5/digest.rs b/twenty-first/src/tip5/digest.rs index 093f19b74..f03fd5071 100644 --- a/twenty-first/src/tip5/digest.rs +++ b/twenty-first/src/tip5/digest.rs @@ -84,15 +84,15 @@ impl fmt::Display for Digest { impl fmt::LowerHex for Digest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let bytes = <[u8; Self::BYTES]>::from(*self); - write!(f, "{}", hex::encode(bytes)) + let value = BigUint::from(*self); + write!(f, "{value:0width$x}", width = Self::BYTES * 2) } } impl fmt::UpperHex for Digest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let bytes = <[u8; Self::BYTES]>::from(*self); - write!(f, "{}", hex::encode_upper(bytes)) + let value = BigUint::from(*self); + write!(f, "{value:0width$X}", width = Self::BYTES * 2) } } @@ -240,8 +240,13 @@ impl Digest { /// Decode hex string to [`Digest`]. Must not include leading “0x”. pub fn try_from_hex(data: impl AsRef<[u8]>) -> Result { - let slice = hex::decode(data)?; - Ok(Self::try_from(&slice as &[u8])?) + let bytes = hex::decode(data)?; + if bytes.len() != Self::BYTES { + return Err(TryFromDigestError::InvalidLength(bytes.len()).into()); + } + + let value = BigUint::from_bytes_be(&bytes); + Ok(Self::try_from(value)?) } } @@ -340,7 +345,7 @@ pub(crate) mod tests { assert_eq!("1,2,3,4,5", format!("{digest}")); let hex_digest = - "01000000000000000200000000000000030000000000000004000000000000000500000000000000"; + "0000000000000004ffffffec00000035ffffffa400000079ffffff8e00000054ffffffd80000000f"; assert_eq!(hex_digest, format!("{digest:x}")); } @@ -575,8 +580,8 @@ pub(crate) mod tests { ( Digest::new(bfe_array![0, 1, 10, 15, 255]), concat!( - "000000000000000001000000000000000a000000", - "000000000f00000000000000ff00000000000000" + "00000000000000fefffffc0400000a04ffffefe3", + "00001350ffffef9300000a6efffffbc200000119" ), ), // note: this would result in NotCanonical error. See issue 195 @@ -595,6 +600,28 @@ pub(crate) mod tests { } } + #[macro_rules_attr::apply(test)] + fn small_digest_hex_has_leading_zeroes() { + let digest = Digest::new(bfe_array![14, 0, 0, 0, 0]); + let expected_hex = format!("{:0>width$x}", 14, width = Digest::BYTES * 2); + + assert_eq!(expected_hex, digest.to_hex()); + assert_eq!(digest, Digest::try_from_hex(&expected_hex).unwrap()); + } + + #[macro_rules_attr::apply(proptest)] + fn hex_matches_biguint_representation(digest: Digest) { + let value = BigUint::from(digest); + let expected_hex = format!("{value:0width$x}", width = Digest::BYTES * 2); + + prop_assert_eq!(expected_hex, digest.to_hex()); + } + + #[macro_rules_attr::apply(proptest)] + fn hex_ordering_matches_digest_ordering(lhs: Digest, rhs: Digest) { + prop_assert_eq!(lhs.cmp(&rhs), lhs.to_hex().cmp(&rhs.to_hex())); + } + #[macro_rules_attr::apply(proptest)] fn to_hex_and_from_hex_are_reciprocal_proptest(bytes: [u8; Digest::BYTES]) { let digest = Digest::try_from(bytes).unwrap(); @@ -657,16 +684,13 @@ pub(crate) mod tests { TryFromHexDigestError::Digest(TryFromDigestError::InvalidLength(_)) ))); - // NotCanonical error. See issue 195 + // All "ff…ff" exceeds the range representable by a Digest. assert!(Digest::try_from_hex( "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ) - .is_err_and(|e| matches!( - e, - TryFromHexDigestError::Digest(TryFromDigestError::InvalidBFieldElement( - ParseBFieldElementError::NotCanonical(_) - )) - ))); + .is_err_and(|e| matches!(e, TryFromHexDigestError::Digest( + TryFromDigestError::Overflow + )))); } } diff --git a/twenty-first/src/tip5/mod.rs b/twenty-first/src/tip5/mod.rs index a505c776b..7765d2689 100644 --- a/twenty-first/src/tip5/mod.rs +++ b/twenty-first/src/tip5/mod.rs @@ -1292,7 +1292,7 @@ pub(crate) mod tests { /// binaries for equivalent behavior is easiest when that behavior is pinned /// through snapshots. const MAGIC_SNAPSHOT_HEX: &str = - "109cc2fe453bd9962f754b96d8f5b919b60af030940a275f5540da195fef65ee651c1b6fa19b2c6a"; + "6a2c9b9fc668ade457b7921002edd4209799ac6d252f4d64eac72809cf5beb9f662609854ef378af"; #[macro_rules_attr::apply(test)] fn hash10_test_vectors_snapshot() { @@ -1319,7 +1319,7 @@ pub(crate) mod tests { let final_digest = Digest::new(digest_sum).to_hex(); assert_eq!( - "efbafa86622a9c69652f8a1c4ffd734f021ad23a0a8085412a877de0f9170b18ea4ff69b6fff9a03", + "039aff6f8d8a522bcc3fd2ad764b6767b56b710eac02947a6fdd22d9fe84847c72bb86965acadb6a", final_digest, ); } diff --git a/twenty-first/src/util_types/mmr/mmr_accumulator.rs b/twenty-first/src/util_types/mmr/mmr_accumulator.rs index 1a8581b7d..668d90c96 100644 --- a/twenty-first/src/util_types/mmr/mmr_accumulator.rs +++ b/twenty-first/src/util_types/mmr/mmr_accumulator.rs @@ -1041,25 +1041,25 @@ mod tests { let empty = MmrAccumulator::new_from_leafs(vec![]); assert_eq!( - "cd65052100640f0d27e5654f97c47e49899add2f265967ccbefee7264e9bc08f588542d9dc3d5ac5", + "c55a3dd9c3d98dece03cf0809e9b8488c7bd42935c86a955be835cb49e0fa266ef2d2f15a0736993", snapshot(empty), ); let one_leaf = MmrAccumulator::new_from_leafs(vec![rng.random()]); assert_eq!( - "9cfb825709fd76de8c24daf437e1bd16a2ea512f2d2338ee417df51dae55905a49628d4f13a2f83e", + "3ef8a21253aad9fe921121463a4031e36757905584a466484d4e3af65354f9ea9161fe78e931ea54", snapshot(one_leaf), ); let two_leafs = MmrAccumulator::new_from_leafs(rng.random::<[_; 2]>().to_vec()); assert_eq!( - "20a22079d30e76c93215f54d986d414d657d4bd056fe8e9a38e87d432ce391cd1dab92f9b6d3442c", + "2c44d3b6487f5c43a1f77bd8d635b26e51719d7352b9339196d6faa5775d4b2f0bccbc8dd471c80c", snapshot(two_leafs), ); let ten_peaks = MmrAccumulator::init(rng.random::<[_; 10]>().to_vec(), 0b11_1111_1111); assert_eq!( - "42dbf8fcf51d4d8134f719ad9a3a9c621a141b3bc8d7941e8baf5919bceea120e6ecf6314e939b87", + "879b934c13889fb2a4d9fc29315fef7a877081f3ed385adc87215805ca108e297382d0c2307e8301", snapshot(ten_peaks), ); }