What changed, and why it matters
This commit adds a new public function for calculating Bitcoin's next mining difficulty target and updates the library to support a recent test network rule (BIP-94). It is a feature addition that mirrors Bitcoin Core's consensus logic. There is no direct evidence in the commit that it fixes an active security bug, but any mistake in difficulty-target arithmetic could, in principle, cause a node or wallet built on this library to disagree with the rest of the Bitcoin network about which chain is valid. The change is well-documented, heavily tested, and appears to be a straightforward port of existing reference code.
Review the implementation against Bitcoin Core's `GetNextWorkRequired` and the BIP-94 specification, especially the boundary conditions around retarget heights, the testnet walk-back loop, and the `saturating_add`/`saturating_sub` behavior at `u32::MAX`. Run the new unit tests and consider adding differential/fuzz tests comparing this implementation to Bitcoin Core on historical mainnet and testnet4 headers. No urgent security patch appears necessary based solely on this commit.
Security signals we found
Consensus-critical code: difficulty retargeting determines which proof-of-work chain is considered heaviest
New public API `next_target_after` exposes consensus arithmetic to downstream users
BIP-94 (testnet4 block-storm mitigation) rule added to retargeting logic
Testnet/regtest special-casing for minimum-difficulty blocks
No explicit security advisory, CVE, or bug report referenced in commit message
Evidence from the diff
The patch introduces next_target_after() in bitcoin/src/pow.rs, which computes the compact target for the block following a given header, matching Bitcoin Core’s GetNextWorkRequired. It also adds enforce_bip94 to Params and wires it into from_header_difficulty_adjustment() so that testnet4 uses the epoch-start bits rather than the epoch-end bits when retargeting. Supporting saturating_add/saturating_sub methods are added to BlockHeight. The change includes unit tests covering mainnet retargeting, non-retarget heights, testnet minimum-difficulty rules, and the BIP-94 testnet4 behavior.
Changed components
bitcoin/src/pow.rsbitcoin/src/network/params.rsunits/src/block.rsInspect captured patch +368 / −2
diff --git a/api/units/all-features.txt b/api/units/all-features.txt
index 203fd997..c8a28b1a 100644
--- a/api/units/all-features.txt
+++ b/api/units/all-features.txt
@@ -1569,6 +1569,8 @@ pub const fn bitcoin_units::absolute::is_block_height(n: u32) -> bool
pub const fn bitcoin_units::absolute::is_block_time(n: u32) -> bool
pub const fn bitcoin_units::amount::AmountDecoder::new() -> Self
pub const fn bitcoin_units::block::BlockHeight::from_u32(inner: u32) -> Self
+pub const fn bitcoin_units::block::BlockHeight::saturating_add(self, rhs: bitcoin_units::block::BlockHeightInterval) -> Self
+pub const fn bitcoin_units::block::BlockHeight::saturating_sub(self, rhs: bitcoin_units::block::BlockHeightInterval) -> Self
pub const fn bitcoin_units::block::BlockHeight::to_u32(self) -> u32
pub const fn bitcoin_units::block::BlockHeightDecoder::new() -> Self
pub const fn bitcoin_units::block::BlockHeightInterval::from_u32(inner: u32) -> Self
diff --git a/api/units/alloc-only.txt b/api/units/alloc-only.txt
index eeb4cec3..8648fd86 100644
--- a/api/units/alloc-only.txt
+++ b/api/units/alloc-only.txt
@@ -1318,6 +1318,8 @@ pub const fn bitcoin_units::Weight::to_wu(self) -> u64
pub const fn bitcoin_units::absolute::is_block_height(n: u32) -> bool
pub const fn bitcoin_units::absolute::is_block_time(n: u32) -> bool
pub const fn bitcoin_units::block::BlockHeight::from_u32(inner: u32) -> Self
+pub const fn bitcoin_units::block::BlockHeight::saturating_add(self, rhs: bitcoin_units::block::BlockHeightInterval) -> Self
+pub const fn bitcoin_units::block::BlockHeight::saturating_sub(self, rhs: bitcoin_units::block::BlockHeightInterval) -> Self
pub const fn bitcoin_units::block::BlockHeight::to_u32(self) -> u32
pub const fn bitcoin_units::block::BlockHeightInterval::from_u32(inner: u32) -> Self
pub const fn bitcoin_units::block::BlockHeightInterval::to_u32(self) -> u32
diff --git a/api/units/no-features.txt b/api/units/no-features.txt
index 8c0c21e5..e47af20c 100644
--- a/api/units/no-features.txt
+++ b/api/units/no-features.txt
@@ -1294,6 +1294,8 @@ pub const fn bitcoin_units::Weight::to_wu(self) -> u64
pub const fn bitcoin_units::absolute::is_block_height(n: u32) -> bool
pub const fn bitcoin_units::absolute::is_block_time(n: u32) -> bool
pub const fn bitcoin_units::block::BlockHeight::from_u32(inner: u32) -> Self
+pub const fn bitcoin_units::block::BlockHeight::saturating_add(self, rhs: bitcoin_units::block::BlockHeightInterval) -> Self
+pub const fn bitcoin_units::block::BlockHeight::saturating_sub(self, rhs: bitcoin_units::block::BlockHeightInterval) -> Self
pub const fn bitcoin_units::block::BlockHeight::to_u32(self) -> u32
pub const fn bitcoin_units::block::BlockHeightInterval::from_u32(inner: u32) -> Self
pub const fn bitcoin_units::block::BlockHeightInterval::to_u32(self) -> u32
diff --git a/bitcoin/src/network/params.rs b/bitcoin/src/network/params.rs
index 0c6f5fe9..03c911c9 100644
--- a/bitcoin/src/network/params.rs
+++ b/bitcoin/src/network/params.rs
@@ -80,6 +80,8 @@ pub struct Params {
pub bip65_height: BlockHeight,
/// Block height at which BIP-0066 becomes active.
pub bip66_height: BlockHeight,
+ /// Enforce BIP-0094 block storm mitigation.
+ pub enforce_bip94: bool,
/// Minimum blocks including miner confirmation of the total of 2016 blocks in a retargeting period,
/// (nPowTargetTimespan / nPowTargetSpacing) which is also used for BIP-0009 deployments.
/// Examples: 1916 for 95%, 1512 for testchains.
@@ -143,6 +145,7 @@ impl Params {
bip34_height: BlockHeight::from_u32(227931), // 000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8
bip65_height: BlockHeight::from_u32(388381), // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0
bip66_height: BlockHeight::from_u32(363725), // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931
+ enforce_bip94: false,
rule_change_activation_threshold: BlockHeightInterval::from_u32(1916), // 95%
miner_confirmation_window: BlockHeightInterval::from_u32(2016),
pow_limit: Target::MAX_ATTAINABLE_MAINNET,
@@ -161,6 +164,7 @@ impl Params {
bip34_height: BlockHeight::from_u32(21111), // 0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8
bip65_height: BlockHeight::from_u32(581885), // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6
bip66_height: BlockHeight::from_u32(330776), // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182
+ enforce_bip94: false,
rule_change_activation_threshold: BlockHeightInterval::from_u32(1512), // 75%
miner_confirmation_window: BlockHeightInterval::from_u32(2016),
pow_limit: Target::MAX_ATTAINABLE_TESTNET,
@@ -178,6 +182,7 @@ impl Params {
bip34_height: BlockHeight::from_u32(21111), // 0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8
bip65_height: BlockHeight::from_u32(581885), // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6
bip66_height: BlockHeight::from_u32(330776), // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182
+ enforce_bip94: false,
rule_change_activation_threshold: BlockHeightInterval::from_u32(1512), // 75%
miner_confirmation_window: BlockHeightInterval::from_u32(2016),
pow_limit: Target::MAX_ATTAINABLE_TESTNET,
@@ -195,6 +200,7 @@ impl Params {
bip34_height: BlockHeight::from_u32(1),
bip65_height: BlockHeight::from_u32(1),
bip66_height: BlockHeight::from_u32(1),
+ enforce_bip94: true,
rule_change_activation_threshold: BlockHeightInterval::from_u32(1512), // 75%
miner_confirmation_window: BlockHeightInterval::from_u32(2016),
pow_limit: Target::MAX_ATTAINABLE_TESTNET,
@@ -212,6 +218,7 @@ impl Params {
bip34_height: BlockHeight::from_u32(1),
bip65_height: BlockHeight::from_u32(1),
bip66_height: BlockHeight::from_u32(1),
+ enforce_bip94: false,
rule_change_activation_threshold: BlockHeightInterval::from_u32(1916), // 95%
miner_confirmation_window: BlockHeightInterval::from_u32(2016),
pow_limit: Target::MAX_ATTAINABLE_SIGNET,
@@ -229,6 +236,7 @@ impl Params {
bip34_height: BlockHeight::from_u32(100000000), // not activated on regtest
bip65_height: BlockHeight::from_u32(1351),
bip66_height: BlockHeight::from_u32(1251), // used only in rpc tests
+ enforce_bip94: false,
rule_change_activation_threshold: BlockHeightInterval::from_u32(108), // 75%
miner_confirmation_window: BlockHeightInterval::from_u32(144),
pow_limit: Target::MAX_ATTAINABLE_REGTEST,
diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs
index c1fcae91..f8706e95 100644
--- a/bitcoin/src/pow.rs
+++ b/bitcoin/src/pow.rs
@@ -12,7 +12,7 @@ use internals::{impl_to_hex_from_lower_hex, write_err};
use io::{BufRead, Write};
use units::parse_int::{self, ParseIntError, PrefixedHexError, UnprefixedHexError};
-use crate::block::{BlockHash, Header};
+use crate::block::{BlockHash, BlockHeight, BlockHeightInterval, Header};
use crate::consensus::encode::{self, Decodable, Encodable};
use crate::internal_macros;
use crate::network::Params;
@@ -400,6 +400,80 @@ impl Target {
do_impl!(Target, ParseTargetError);
impl_to_hex_from_lower_hex!(Target, |_| 64);
+/// Gets the target for the block after `current_header`.
+///
+/// Implements the [`GetNextWorkRequired`] function from Bitcoin core.
+///
+/// Note, `new_block_timestamp` is only used when `params.allow_min_difficulty_blocks = true` i.e.,
+/// on testnet and regtest.
+///
+/// > Special difficulty rule for testnet: If the new block's timestamp is more
+/// > than 2*10 minutes then allow mining of a min-difficulty block.
+///
+/// # Panics
+///
+/// If we are on testnet/regtest and `new_block_timestamp` is `None`.
+///
+/// [`GetNextWorkRequired`]: <https://github.com/bitcoin/bitcoin/blob/830583eb9d07e054c54a177907a98153ab3e29ae/src/pow.cpp#L13>
+pub fn next_target_after<F, E>(
+ current_header: Header,
+ current_height: BlockHeight,
+ params: &Params,
+ new_block_timestamp: Option<u32>,
+ mut get_block_header_by_height: F,
+) -> Result<CompactTarget, E>
+where
+ F: FnMut(BlockHeight) -> Result<Header, E>
+{
+ // explicitly dropping the high bits because they make no sense since block height is only u32
+ let adjustment_interval = params.difficulty_adjustment_interval() as u32;
+
+ // if ((pindexLast->nHeight+1) % params.DifficultyAdjustmentInterval() != 0)
+ if !is_retarget_height(current_height.saturating_add(1.into()), adjustment_interval) {
+ if params.allow_min_difficulty_blocks { // Only true for testnet and regtest.
+ let new_block_timestamp = new_block_timestamp
+ .expect("new_block_timestamp must contain a value when on testnet/regtest");
+
+ // Special difficulty rule for testnet: If the new block's timestamp is more
+ // than 2*10 minutes then allow mining of a min-difficulty block.
+ let pow_limit = params.max_attainable_target.to_compact_lossy();
+ let pow_target_spacing = u32::try_from(params.pow_target_spacing & u64::from(u32::MAX)).unwrap();
+
+ // if (pblock->GetBlockTime() > pindexLast->GetBlockTime() + params.nPowTargetSpacing*2)
+ if new_block_timestamp > current_header.time.to_u32() + pow_target_spacing * 2 {
+ Ok(pow_limit)
+ } else {
+ let mut header = current_header;
+ let mut height = current_height;
+ // while (pindex->pprev && pindex->nHeight % params.DifficultyAdjustmentInterval() != 0 && pindex->nBits == nProofOfWorkLimit)
+ while header.prev_blockhash != BlockHash::GENESIS_PREVIOUS_BLOCK_HASH
+ && !is_retarget_height(height, adjustment_interval)
+ && header.bits == pow_limit
+ {
+ // pindex = pindex->pprev;
+ height = height.saturating_sub(1.into());
+ header = get_block_header_by_height(height)?;
+ }
+ Ok(header.bits)
+ }
+ } else {
+ Ok(current_header.bits)
+ }
+ } else {
+ // Go back by what we want to be 14 days worth of blocks
+ let back_step = BlockHeightInterval::from_u32(adjustment_interval - 1);
+ let height_first = current_height.saturating_sub(back_step);
+ let block_first = get_block_header_by_height(height_first)?;
+
+ Ok(CompactTarget::from_header_difficulty_adjustment(block_first, current_header, params))
+ }
+}
+
+/// Returns true if `height` ends the difficulty period.
+fn is_retarget_height(height: BlockHeight, adjustment_interval: u32) -> bool {
+ height.to_u32() % adjustment_interval == 0
+}
+
internal_macros::define_extension_trait! {
/// Extension functionality for the [`CompactTarget`] type.
pub trait CompactTargetExt impl for CompactTarget {
@@ -495,7 +569,16 @@ internal_macros::define_extension_trait! {
params: impl AsRef<Params>,
) -> Self {
let timespan = i64::from(current.time.to_u32()) - i64::from(last_epoch_boundary.time.to_u32());
- let bits = current.bits;
+
+ // Special difficulty rule for testnet4.
+ // Take target from start of epoch instead of end of epoch.
+ // See https://github.com/bitcoin/bitcoin/blob/4d7d5f6b79d4c11c47e7a828d81296918fd11d4d/src/pow.cpp#L67
+ let bits = if params.as_ref().enforce_bip94 {
+ last_epoch_boundary.bits
+ } else {
+ current.bits
+ };
+
CompactTarget::from_next_work_required(bits, timespan, params)
}
}
@@ -1230,6 +1313,9 @@ pub mod test_utils {
#[cfg(test)]
mod tests {
use super::*;
+
+ use core::str::FromStr;
+
#[cfg(feature = "std")]
use crate::pow::test_utils::u128_to_work;
use crate::pow::test_utils::{u32_to_target, u64_to_target};
@@ -2049,6 +2135,45 @@ mod tests {
assert_eq!(got, want);
}
+ #[test]
+ fn compact_target_from_adjustment_bip94() {
+ // Two different compact targets to use for epoch_start and current headers.
+ let bits_start = CompactTarget::from_consensus(0x1c00ffff); // Higher difficulty
+ let bits_end = CompactTarget::from_consensus(0x1d00ffff); // Minimum difficulty
+
+ // Same timestamps for both networks to keep timespan consistent.
+ let start_time = BlockTime::from_u32(1_000_000);
+ let end_time = BlockTime::from_u32(1_000_000 + 14 * 24 * 60 * 60); // +14 days. No adjustment.
+
+ let epoch_start = Header {
+ version: crate::block::Version::ONE,
+ prev_blockhash: BlockHash::from_byte_array([0; 32]),
+ merkle_root: crate::TxMerkleNode::from_byte_array([0; 32]),
+ time: start_time,
+ bits: bits_start,
+ nonce: 0,
+ };
+
+ let current = Header {
+ version: crate::block::Version::ONE,
+ prev_blockhash: BlockHash::from_byte_array([0; 32]),
+ merkle_root: crate::TxMerkleNode::from_byte_array([0; 32]),
+ time: end_time,
+ bits: bits_end,
+ nonce: 0,
+ };
+
+ // Test mainnet (enforce_bip94 = false): should use current.bits
+ let mainnet_result =
+ CompactTarget::from_header_difficulty_adjustment(epoch_start, current, &Params::MAINNET);
+ assert_eq!(mainnet_result, bits_end);
+
+ // Test testnet4 (enforce_bip94 = true): should use epoch_start.bits
+ let testnet_result =
+ CompactTarget::from_header_difficulty_adjustment(epoch_start, current, &Params::TESTNET4);
+ assert_eq!(testnet_result, bits_start);
+ }
+
#[test]
fn target_from_compact() {
// (nBits, target)
@@ -2342,6 +2467,149 @@ mod tests {
assert_eq!((U256::MAX >> (256 - 16)).to_f64(), 65535.0_f64);
assert_eq!((U256::MAX >> (256 - 8)).to_f64(), 255.0_f64);
}
+
+ fn current_header() -> Header {
+ Header {
+ version: crate::block::Version::from_consensus(0x2431_a000),
+ prev_blockhash: BlockHash::from_str(
+ "0000000000000000000387dab5f3cf88824c983770f70f8a8eb7a9a240a257a5",
+ )
+ .unwrap(),
+ merkle_root: crate::TxMerkleNode::from_str(
+ "07bf4eafca7979d59b0ec2dc03131c08c1b9ea2ddb8b8945846fcb0ce92cdbe3",
+ )
+ .unwrap(),
+ time: 0x651b_c919.into(), // 2023-10-03 18:56:09 GMT +11 -> 1696359369 -> 651BC919
+ bits: CompactTarget::from_consensus(0x1704_ed7f),
+ nonce: 0xc637_a163,
+ }
+ }
+
+ // Test target calculated going from block 808416 to block 810432 on mainnet.
+ #[test]
+ fn next_target_mainnet() {
+ // Only time and bits are used.
+ let header_810431 = current_header();
+
+ // This closure should return the header for 808416 since 810431 - 2015 is 808416
+ fn fetch_header_808416(height: BlockHeight) -> Result<Header, core::convert::Infallible> {
+ assert_eq!(height, BlockHeight::from_u32(808_416)); // sanity check
+
+ // Header for 808416
+ Ok(Header {
+ version: crate::block::Version::TWO,
+ prev_blockhash: BlockHash::from_str(
+ "000000000000000000027ecc78c2da1cc5c0b0496706baa7e4d7c80812c10bf3",
+ )
+ .unwrap(),
+ merkle_root: crate::TxMerkleNode::from_str(
+ "b920d5b5ebef4e9d106072944e0729cea8bf6defc583a7d87063041a316a757b",
+ )
+ .unwrap(),
+ time: 0x6509_64b5.into(), // 2023-09-19 09:07:01 GMT -> 1695114421 -> 650964B5
+ bits: CompactTarget::from_consensus(0x1704_ed7f),
+ nonce: 0x82d6_8990,
+ })
+ }
+
+ let params = Params::new(crate::Network::Bitcoin);
+ let height = BlockHeight::from_u32(810_431);
+
+ let want = CompactTarget::from_consensus(0x1704_e90f); // Bits from block 810432.
+ let got = next_target_after(header_810431, height, ¶ms, None, fetch_header_808416)
+ .expect("failed to calculate next target");
+
+ assert_eq!(got, want);
+ }
+
+ #[test]
+ fn next_target_mainnet_same_target() {
+ let header_810430 = current_header();
+
+ // This closure should be unused if the target remains the same
+ fn fetch_header(_height: BlockHeight) -> Result<Header, core::num::ParseIntError> {
+ unreachable!("get_block_header_by_height should not be called");
+ }
+
+ let params = Params::new(crate::Network::Bitcoin);
+ let height = BlockHeight::from_u32(810_430);
+
+ // On mainnet, non-retargeting height should return the same block target
+ let want = header_810430.bits; // Bits from block 810430.
+ let got = next_target_after(header_810430, height, ¶ms, None, fetch_header)
+ .expect("failed to calculate next target");
+
+ assert_eq!(got, want);
+ }
+
+ // Test that on testnet, if the new block's timestamp is more than 20 minutes after
+ // the current header's time, we return the pow_limit (minimum difficulty).
+ #[test]
+ fn next_target_testnet_min_difficulty_when_slow() {
+ let header = current_header();
+
+ fn fetch_header(_height: BlockHeight) -> Result<Header, core::convert::Infallible> {
+ unreachable!("fetcher should not be called for min difficulty case");
+ }
+
+ let params = Params::TESTNET3;
+ let height = BlockHeight::from_u32(100); // non-retarget height
+
+ // New block timestamp is more than 2 * 10 minutes = 20 minutes after current header
+ let new_block_timestamp = Some(header.time.to_u32() + 20 * 60 + 1);
+
+ // Should return pow_limit (minimum difficulty)
+ let want = params.max_attainable_target.to_compact_lossy();
+ let got = next_target_after(header, height, ¶ms, new_block_timestamp, fetch_header)
+ .expect("failed to calculate next target");
+ assert_eq!(got, want);
+ }
+
+ #[test]
+ fn next_target_testnet_walk_back_for_real_target() {
+ let current_time: u32 = 1_700_000_000;
+
+ let params = Params::TESTNET3;
+ let pow_limit = params.max_attainable_target.to_compact_lossy();
+ let want = CompactTarget::from_consensus(0x1d00_ffff);
+
+ // Current header is at a retarget boundary (height divisible by 2016) with pow_limit bits
+ let adjustment_interval = u32::try_from(params.difficulty_adjustment_interval()).unwrap();
+ let current_height = BlockHeight::from_u32(adjustment_interval * 5);
+
+ let current_header = Header {
+ version: crate::block::Version::from_consensus(0x2000_0000),
+ prev_blockhash: BlockHash::from_byte_array([1u8; 32]),
+ merkle_root: crate::TxMerkleNode::from_byte_array([2u8; 32]),
+ time: current_time.into(),
+ bits: pow_limit, // Current header has pow_limit
+ nonce: 0,
+ };
+
+ // New block timestamp is within 20 minutes
+ let new_block_timestamp = Some(current_time + 10 * 60);
+
+ // The fetcher: heights at retarget boundaries with pow_limit should be walked back,
+ // until we find one that doesn't have pow_limit or isn't at a retarget boundary.
+ let fetch_header = move |height: BlockHeight| -> Result<Header, core::convert::Infallible> {
+ assert_eq!(height.to_u32(), 10_079);
+
+ Ok(Header {
+ version: crate::block::Version::from_consensus(0x2000_0000),
+ prev_blockhash: BlockHash::from_byte_array([1u8; 32]),
+ merkle_root: crate::TxMerkleNode::from_byte_array([2u8; 32]),
+ time: (current_time - 600).into(),
+ bits: want,
+ nonce: 0,
+ })
+ };
+
+ let got = next_target_after(current_header, current_height, ¶ms, new_block_timestamp, fetch_header)
+ .expect("failed to calculate next target");
+
+ // Should return the real_target from the walked-back header
+ assert_eq!(got, want);
+ }
}
#[cfg(kani)]
diff --git a/units/src/block.rs b/units/src/block.rs
index a5f45ee5..85614b33 100644
--- a/units/src/block.rs
+++ b/units/src/block.rs
@@ -123,6 +123,24 @@ impl BlockHeight {
pub fn checked_add(self, other: BlockHeightInterval) -> Option<Self> {
self.to_u32().checked_add(other.to_u32()).map(Self)
}
+
+ /// Saturating integer addition.
+ ///
+ /// Computes self + rhs, saturating at `BlockHeight::MAX` instead of overflowing.
+ #[inline]
+ #[must_use]
+ pub const fn saturating_add(self, rhs: BlockHeightInterval) -> Self {
+ Self::from_u32(self.to_u32().saturating_add(rhs.to_u32()))
+ }
+
+ /// Saturating integer subtraction.
+ ///
+ /// Computes self - rhs, saturating at `BlockHeight::MIN` instead of overflowing.
+ #[inline]
+ #[must_use]
+ pub const fn saturating_sub(self, rhs: BlockHeightInterval) -> Self {
+ Self::from_u32(self.to_u32().saturating_sub(rhs.to_u32()))
+ }
}
impl From<absolute::Height> for BlockHeight {
@@ -812,4 +830,70 @@ mod tests {
serde_roundtrip_test!(block_height_interval_serde_round_trip, BlockHeightInterval);
serde_roundtrip_test!(block_mtp_serde_round_trip, BlockMtp);
serde_roundtrip_test!(block_mtp_interval_serde_round_trip, BlockMtpInterval);
+
+ #[test]
+ fn block_height_saturating_add() {
+ // Normal addition
+ assert_eq!(
+ BlockHeight(100).saturating_add(BlockHeightInterval(50)),
+ BlockHeight(150),
+ );
+ assert_eq!(
+ BlockHeight::ZERO.saturating_add(BlockHeightInterval(1)),
+ BlockHeight(1),
+ );
+
+ // Saturates at MAX instead of overflowing
+ assert_eq!(
+ BlockHeight::MAX.saturating_add(BlockHeightInterval(1)),
+ BlockHeight::MAX,
+ );
+ assert_eq!(
+ BlockHeight::MAX.saturating_add(BlockHeightInterval(100)),
+ BlockHeight::MAX,
+ );
+ assert_eq!(
+ BlockHeight(u32::MAX - 10).saturating_add(BlockHeightInterval(20)),
+ BlockHeight::MAX,
+ );
+
+ // Adding zero
+ assert_eq!(
+ BlockHeight(500).saturating_add(BlockHeightInterval::ZERO),
+ BlockHeight(500),
+ );
+ }
+
+ #[test]
+ fn block_height_saturating_sub() {
+ // Normal subtraction
+ assert_eq!(
+ BlockHeight(100).saturating_sub(BlockHeightInterval(50)),
+ BlockHeight(50),
+ );
+ assert_eq!(
+ BlockHeight(100).saturating_sub(BlockHeightInterval(100)),
+ BlockHeight(0),
+ );
+
+ // Saturates at MIN instead of underflowing
+ assert_eq!(
+ BlockHeight::MIN.saturating_sub(BlockHeightInterval(1)),
+ BlockHeight::MIN,
+ );
+ assert_eq!(
+ BlockHeight::ZERO.saturating_sub(BlockHeightInterval(100)),
+ BlockHeight::ZERO,
+ );
+ assert_eq!(
+ BlockHeight(10).saturating_sub(BlockHeightInterval(20)),
+ BlockHeight::ZERO,
+ );
+
+ // Subtracting zero
+ assert_eq!(
+ BlockHeight(500).saturating_sub(BlockHeightInterval::ZERO),
+ BlockHeight(500),
+ );
+ }
}
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.