Skip to content

Commit 96657ee

Browse files
dairaclaudejvff
authored
fix(rpc): stop eagerly future-dating Testnet getblocktemplate timestamps (#10873)
* fix(rpc): stop eagerly future-dating Testnet getblocktemplate timestamps On Testnet, `getblocktemplate` switched to a minimum-difficulty block template `2 * PoWTargetSpacing` (150s) before the consensus minimum-difficulty threshold, clamping `cur_time` up to just past the threshold. This future-dated the block's timestamp and produced spurious minimum-difficulty blocks that depress Testnet difficulty far below its equilibrium. Switch to a minimum-difficulty template only once `cur_time` reaches the consensus threshold itself. This is a Testnet-only, template-construction (non-consensus) change: it does not alter block validity, and it does not change the difficulty-averaging rule that amplifies each minimum-difficulty block into a large difficulty drop (tracked in zcash/zips#1321). The now-unused public constant `EXTRA_TIME_TO_MINE_A_BLOCK` is removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(state, rpc): cover the Testnet getblocktemplate minimum-difficulty adjustment Add deterministic, fixed-clock unit tests for `adjust_difficulty_and_time_for_testnet`, which previously had no direct coverage: - a candidate time inside the old eager window (previous + 300 .. + 450) stays standard difficulty and is not future-dated; - a candidate time past previous + 450 switches to minimum difficulty; - the gap is a strict `>` (previous + 450 is standard, + 451 is minimum); - the adjustment is a no-op on Mainnet and below the Testnet minimum-difficulty start height. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit acaf1e1) * docs(state): clarify the stale clamp comment in the Testnet min-difficulty adjustment The "We don't need to undo the clamping here" comment referred to the caller's `cur_time.clamp(min_time, max_time)` in `difficulty_time_and_history_tree`, but #5925 later added internal `.clamp` calls directly below it without updating the comment, so it read as though it described those. Reword it to name its real referent and explain why the caller's clamp needs no undoing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix CHANGELOG typo Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Janito Vaqueiro Ferreira Filho <janito.vff@gmail.com>
1 parent 135c136 commit 96657ee

2 files changed

Lines changed: 216 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,29 @@ and this project adheres to [Semantic Versioning](https://semver.org).
2929
[zakura](https://github.com/zakura-core/zakura) authors for the major part of
3030
this implementation.
3131

32+
### Changed
33+
34+
- On Testnet, the `getblocktemplate` RPC no longer switches to a
35+
minimum-difficulty block template early. Zebra previously treated a template
36+
as minimum-difficulty as soon as its `cur_time` came within a fixed 150
37+
seconds (`2 * PoWTargetSpacing` after Blossom) of the consensus
38+
minimum-difficulty threshold, clamping `cur_time` up to just past the
39+
threshold. On Testnet this future-dated the block's timestamp and produced
40+
spurious minimum-difficulty blocks that depress difficulty far below its
41+
equilibrium. Templates now switch to minimum difficulty only once `cur_time`
42+
reaches the consensus threshold itself. This is a Testnet-only,
43+
template-construction (non-consensus) change: it does not alter block
44+
validity, and it does not change the difficulty-averaging rule that amplifies
45+
each minimum-difficulty block into a large difficulty drop (tracked in
46+
[zcash/zips#1321](https://github.com/zcash/zips/issues/1321))
47+
([#10873](https://github.com/ZcashFoundation/zebra/pull/10873))
48+
49+
### Removed
50+
51+
- The public constant `EXTRA_TIME_TO_MINE_A_BLOCK` in `zebra-state`, made unused by
52+
the Testnet `getblocktemplate` change above
53+
([#10873](https://github.com/ZcashFoundation/zebra/pull/10873))
54+
3255
## [Zebra 6.1.0](https://github.com/ZcashFoundation/zebra/releases/tag/v6.1.0) - 2026-07-17
3356

3457
### Added

zebra-state/src/service/read/difficulty.rs

Lines changed: 193 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use chrono::{DateTime, Utc};
77
use zebra_chain::{
88
block::{self, Block, Hash, Height},
99
history_tree::HistoryTree,
10-
parameters::{Network, NetworkUpgrade, POST_BLOSSOM_POW_TARGET_SPACING},
10+
parameters::{Network, NetworkUpgrade},
1111
serialization::{DateTime32, Duration32},
1212
work::difficulty::{CompactDifficulty, PartialCumulativeWork, Work},
1313
};
@@ -32,11 +32,6 @@ use crate::{
3232
BoxError, GetBlockTemplateChainInfo,
3333
};
3434

35-
/// The amount of extra time we allow for a miner to mine a standard difficulty block on testnet.
36-
///
37-
/// This is a Zebra-specific standard rule.
38-
pub const EXTRA_TIME_TO_MINE_A_BLOCK: u32 = POST_BLOSSOM_POW_TARGET_SPACING * 2;
39-
4035
/// Returns the [`GetBlockTemplateChainInfo`] for the current best chain.
4136
///
4237
/// # Panics
@@ -332,21 +327,34 @@ fn adjust_difficulty_and_time_for_testnet(
332327
.checked_add(Duration32::from_seconds(1))
333328
.expect("a valid block time plus a small constant is in-range");
334329

335-
// If a miner is likely to find a block with the cur_time and standard difficulty
336-
// within a target block interval or two, keep the original difficulty.
337-
// Otherwise, try to use the minimum difficulty.
330+
// Offer a minimum-difficulty template only once `cur_time` is strictly past
331+
// `previous_block_time + 6 * PoWTargetSpacing` (the latest time a standard-difficulty
332+
// block may use; a minimum-difficulty block's time must be strictly greater).
333+
//
334+
// A minimum-difficulty block is only consensus-valid if its time is more than
335+
// `6 * PoWTargetSpacing` after the previous block. Switching to a minimum-difficulty template
336+
// before `cur_time` has reached that point would require clamping `cur_time` up to `min_time`,
337+
// i.e. future-dating the block timestamp ahead of real time purely to obtain minimum difficulty.
338338
//
339-
// This is a Zebra-specific standard rule.
339+
// Zebra used to do this, switching 150 seconds early (`2 * PoWTargetSpacing` after Blossom),
340+
// via a former `EXTRA_TIME_TO_MINE_A_BLOCK` constant. On a chain with any sustained hashrate,
341+
// that behaviour produced a stream of future-dated minimum-difficulty blocks, each of which
342+
// sharply reduced the difficulty (the difficulty average is over targets and includes
343+
// minimum-difficulty blocks), depressing the difficulty far below equilibrium. See
344+
// <https://github.com/zcash/zips/issues/1321>.
340345
//
341-
// We don't need to undo the clamping here:
342-
// - if cur_time is clamped to min_time, then we're more likely to have a minimum
343-
// difficulty block, which makes mining easier;
344-
// - if cur_time gets clamped to max_time, this is almost always a minimum difficulty block.
345-
let local_std_difficulty_limit = std_difficulty_max_time
346-
.checked_sub(Duration32::from_seconds(EXTRA_TIME_TO_MINE_A_BLOCK))
347-
.expect("a valid block time minus a small constant is in-range");
348-
349-
if result.cur_time <= local_std_difficulty_limit {
346+
// This does not change the underlying difficulty averaging: a genuine gap of more than
347+
// `6 * PoWTargetSpacing` still yields a minimum-difficulty block that reduces the difficulty.
348+
// It only stops Zebra from proactively generating such blocks by future-dating timestamps.
349+
//
350+
// `cur_time` was already clamped into `[min_time, max_time]` by the caller
351+
// (`difficulty_time_and_history_tree`); we don't restore the un-clamped `now` before
352+
// choosing standard vs. minimum difficulty below, because whichever way that clamp
353+
// moved `cur_time`, the outcome is benign:
354+
// - clamped up to `min_time`: more likely a minimum-difficulty block, which makes
355+
// mining easier;
356+
// - clamped down to `max_time`: almost always a minimum-difficulty block.
357+
if result.cur_time <= std_difficulty_max_time {
350358
// Standard difficulty: the cur and max time need to exclude min difficulty blocks
351359

352360
// The maximum time can only be decreased, and only as far as min_time.
@@ -376,3 +384,169 @@ fn adjust_difficulty_and_time_for_testnet(
376384
.expected_difficulty_threshold();
377385
}
378386
}
387+
388+
#[cfg(test)]
389+
mod tests {
390+
//! Unit tests for the Testnet minimum-difficulty template adjustment. They construct the
391+
//! [`GetBlockTemplateChainInfo`] and block context with fixed times, exercising
392+
//! `adjust_difficulty_and_time_for_testnet` deterministically without reading the real
393+
//! clock (the `DateTime32::now()` call lives only in its caller).
394+
395+
use super::*;
396+
use zebra_chain::work::difficulty::ParameterDifficulty as _;
397+
398+
// A Testnet height at which the minimum-difficulty rule is active (>= 299188) and the
399+
// target spacing is the post-Blossom 75 s, so the minimum-difficulty gap is 6 * 75 = 450 s.
400+
const ACTIVE_HEIGHT: Height = Height(2_000_000);
401+
const PREV: u32 = 1_600_000_000;
402+
const GAP: u32 = 6 * 75;
403+
404+
/// Recent block difficulties and times in reverse order from the tip, with the most
405+
/// recent (first) block time equal to `PREV`. The difficulty thresholds are irrelevant
406+
/// to the minimum-difficulty rule under test.
407+
fn recent_block_data(network: &Network) -> Vec<(CompactDifficulty, DateTime<Utc>)> {
408+
let threshold = network.target_difficulty_limit().to_compact();
409+
(0..POW_ADJUSTMENT_BLOCK_SPAN)
410+
.map(|i| (threshold, DateTime32::from(PREV - i as u32).into()))
411+
.collect()
412+
}
413+
414+
/// A [`GetBlockTemplateChainInfo`] with candidate time `cur_time` and a wide
415+
/// `[min_time, max_time]` window around `PREV`, so the only clamping under test comes
416+
/// from the minimum-difficulty adjustment. `expected_difficulty` starts at a sentinel.
417+
fn chain_info(cur_time: u32) -> GetBlockTemplateChainInfo {
418+
GetBlockTemplateChainInfo {
419+
tip_hash: Hash([0; 32]),
420+
tip_height: Height(0),
421+
chain_history_root: None,
422+
expected_difficulty: CompactDifficulty::default(),
423+
cur_time: DateTime32::from(cur_time),
424+
min_time: DateTime32::from(PREV - 100),
425+
max_time: DateTime32::from(PREV + BLOCK_MAX_TIME_SINCE_MEDIAN),
426+
}
427+
}
428+
429+
/// A candidate time inside the old "eager" window (`PREV + 300 .. PREV + 450`) must stay
430+
/// standard-difficulty and must not be future-dated. Zebra used to switch this to a
431+
/// minimum-difficulty template with `cur_time` clamped up to `PREV + 451`.
432+
#[test]
433+
fn eager_window_stays_standard_difficulty_and_is_not_future_dated() {
434+
let network = Network::new_default_testnet();
435+
let cur = PREV + 400;
436+
let mut result = chain_info(cur);
437+
438+
adjust_difficulty_and_time_for_testnet(
439+
&mut result,
440+
&network,
441+
ACTIVE_HEIGHT,
442+
recent_block_data(&network),
443+
);
444+
445+
// Still standard difficulty: expected_difficulty left untouched (sentinel unchanged).
446+
assert_eq!(result.expected_difficulty, CompactDifficulty::default());
447+
// Not future-dated: cur_time unchanged, still before the consensus threshold.
448+
assert_eq!(result.cur_time, DateTime32::from(cur));
449+
// max_time clamped down to the standard-difficulty boundary (PREV + 450).
450+
assert_eq!(result.max_time, DateTime32::from(PREV + GAP));
451+
}
452+
453+
/// A candidate time strictly past `PREV + 450` switches to a minimum-difficulty template.
454+
#[test]
455+
fn past_the_threshold_switches_to_minimum_difficulty() {
456+
let network = Network::new_default_testnet();
457+
let cur = PREV + 500;
458+
let mut result = chain_info(cur);
459+
460+
adjust_difficulty_and_time_for_testnet(
461+
&mut result,
462+
&network,
463+
ACTIVE_HEIGHT,
464+
recent_block_data(&network),
465+
);
466+
467+
// Minimum difficulty: expected_difficulty recomputed to the PoWLimit.
468+
assert_eq!(
469+
result.expected_difficulty,
470+
network.target_difficulty_limit().to_compact()
471+
);
472+
// min_time raised to PREV + 451; cur_time is already past it, so it is unchanged.
473+
assert_eq!(result.min_time, DateTime32::from(PREV + GAP + 1));
474+
assert_eq!(result.cur_time, DateTime32::from(cur));
475+
}
476+
477+
/// The gap is a strict `>`: exactly `PREV + 450` is standard, `PREV + 451` is minimum.
478+
#[test]
479+
fn threshold_is_inclusive_for_standard_difficulty() {
480+
let network = Network::new_default_testnet();
481+
482+
let mut at_threshold = chain_info(PREV + GAP);
483+
adjust_difficulty_and_time_for_testnet(
484+
&mut at_threshold,
485+
&network,
486+
ACTIVE_HEIGHT,
487+
recent_block_data(&network),
488+
);
489+
assert_eq!(
490+
at_threshold.expected_difficulty,
491+
CompactDifficulty::default()
492+
);
493+
494+
let mut past_threshold = chain_info(PREV + GAP + 1);
495+
adjust_difficulty_and_time_for_testnet(
496+
&mut past_threshold,
497+
&network,
498+
ACTIVE_HEIGHT,
499+
recent_block_data(&network),
500+
);
501+
assert_eq!(
502+
past_threshold.expected_difficulty,
503+
network.target_difficulty_limit().to_compact()
504+
);
505+
}
506+
507+
/// The adjustment only applies to test networks: on Mainnet it is a no-op.
508+
#[test]
509+
fn mainnet_is_a_no_op() {
510+
let network = Network::Mainnet;
511+
let cur = PREV + 400;
512+
let mut result = chain_info(cur);
513+
514+
adjust_difficulty_and_time_for_testnet(
515+
&mut result,
516+
&network,
517+
ACTIVE_HEIGHT,
518+
recent_block_data(&network),
519+
);
520+
521+
assert_eq!(result.expected_difficulty, CompactDifficulty::default());
522+
assert_eq!(result.cur_time, DateTime32::from(cur));
523+
assert_eq!(result.min_time, DateTime32::from(PREV - 100));
524+
assert_eq!(
525+
result.max_time,
526+
DateTime32::from(PREV + BLOCK_MAX_TIME_SINCE_MEDIAN)
527+
);
528+
}
529+
530+
/// Below `TESTNET_MINIMUM_DIFFICULTY_START_HEIGHT` (299188) the rule is inactive, so the
531+
/// adjustment is a no-op even on Testnet.
532+
#[test]
533+
fn below_start_height_is_a_no_op() {
534+
let network = Network::new_default_testnet();
535+
let cur = PREV + 400;
536+
let mut result = chain_info(cur);
537+
538+
adjust_difficulty_and_time_for_testnet(
539+
&mut result,
540+
&network,
541+
Height(100_000),
542+
recent_block_data(&network),
543+
);
544+
545+
assert_eq!(result.expected_difficulty, CompactDifficulty::default());
546+
assert_eq!(result.cur_time, DateTime32::from(cur));
547+
assert_eq!(
548+
result.max_time,
549+
DateTime32::from(PREV + BLOCK_MAX_TIME_SINCE_MEDIAN)
550+
);
551+
}
552+
}

0 commit comments

Comments
 (0)