From 68866bc14c834a1ac64aed4a1eef95e9a532dfef Mon Sep 17 00:00:00 2001 From: Nitesh Balusu Date: Sun, 18 Oct 2026 17:02:17 -0400 Subject: [PATCH 1/2] rpc: getblocktemplate mintime is one past the tip median mintime was the tip's median time past. A block must be above that median (time-too-old), so a template stamped at mintime was invalid. Core's GetMinimumTime starts from MTP + 1; this fixes that lower bound. Core's extra floor at retarget heights (previous block time minus 600) is not part of this change. Co-Authored-By: Claude Opus 5.5 --- changelog.d/gbt-mintime.md | 7 +++++++ crates/rbitcoin-rpc/src/methods/mine.rs | 4 +++- crates/rbitcoin-rpc/src/methods_tests.rs | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog.d/gbt-mintime.md diff --git a/changelog.d/gbt-mintime.md b/changelog.d/gbt-mintime.md new file mode 100644 index 0000000000..9f4bd5cb91 --- /dev/null +++ b/changelog.d/gbt-mintime.md @@ -0,0 +1,7 @@ +Fixed + +- **`getblocktemplate` `mintime` is one past the tip's median time.** It was + the median itself, which a block cannot use (`time-too-old`). Core also + raises its minimum to the previous block's time minus 600 seconds at + retarget heights; that floor is not part of this fix. The Esplora + `GET /block-template` JSON follows. diff --git a/crates/rbitcoin-rpc/src/methods/mine.rs b/crates/rbitcoin-rpc/src/methods/mine.rs index 9811c3d053..0551da2025 100644 --- a/crates/rbitcoin-rpc/src/methods/mine.rs +++ b/crates/rbitcoin-rpc/src/methods/mine.rs @@ -645,11 +645,13 @@ pub fn gbt_template(ctx: &RpcContext) -> Result { rbitcoin_query::expected_next_bits(ctx.query.as_ref(), ¶ms, Height(next_h), curtime) .map(|c| c.to_consensus()) .unwrap_or(tip_bits); + // Core `GetMinimumTime`: the next block must be above the tip median. let mintime = if tip_h == 0 { tip_time } else { rbitcoin_query::median_time_past(ctx.query.as_ref(), Height(tip_h)).unwrap_or(tip_time) - }; + } + .saturating_add(1); let selected = mempool_block_txs(ctx); let mut fees = 0u64; let mut tx_json = Vec::with_capacity(selected.len()); diff --git a/crates/rbitcoin-rpc/src/methods_tests.rs b/crates/rbitcoin-rpc/src/methods_tests.rs index a77e6f2803..068d02bab1 100644 --- a/crates/rbitcoin-rpc/src/methods_tests.rs +++ b/crates/rbitcoin-rpc/src/methods_tests.rs @@ -3893,6 +3893,24 @@ fn submitblock_store_fault_does_not_cache_block_invalid() { let _ = std::fs::remove_dir_all(&dir); } +/// Core `GetMinimumTime`: `mintime` is the lowest time the next block can +/// carry, one past the tip median. At genesis that median is its time. +#[test] +fn getblocktemplate_mintime_is_one_past_the_tip_median() { + let (ctx, dir, hub) = ctx_from_mature_pad(300_000_000); + let gbt = dispatch(&ctx, "getblocktemplate", vec![json!({"rules": ["segwit"]})]).unwrap(); + let tip = Height(hub.tip_height().unwrap()); + let mtp = rbitcoin_query::median_time_past(hub.query.as_ref(), tip).unwrap(); + assert_eq!(gbt["mintime"], u64::from(mtp) + 1, "{gbt}"); + let _ = std::fs::remove_dir_all(&dir); + + let (ctx, dir, hub) = ctx_regtest_hub(); + let gbt = dispatch(&ctx, "getblocktemplate", vec![json!({"rules": ["segwit"]})]).unwrap(); + let genesis = hub.tip_header().unwrap().time; + assert_eq!(gbt["mintime"], u64::from(genesis) + 1, "{gbt}"); + let _ = std::fs::remove_dir_all(&dir); +} + /// A sibling that spends the coin the current tip also spent is a competing /// block, not a permanent `duplicate-invalid`. #[test] From 3f933f06f92d948d0d2aaebc1098576718866bf2 Mon Sep 17 00:00:00 2001 From: Nitesh Balusu Date: Sun, 18 Oct 2026 19:55:57 -0400 Subject: [PATCH 2/2] rpc: say which part of Core's minimum time mintime takes The comment named GetMinimumTime, which also applies a retarget-height floor. This change takes only the MTP + 1 part. Co-Authored-By: Claude Opus 5.5 --- crates/rbitcoin-rpc/src/methods/mine.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/rbitcoin-rpc/src/methods/mine.rs b/crates/rbitcoin-rpc/src/methods/mine.rs index 0551da2025..393b1e8b81 100644 --- a/crates/rbitcoin-rpc/src/methods/mine.rs +++ b/crates/rbitcoin-rpc/src/methods/mine.rs @@ -645,7 +645,8 @@ pub fn gbt_template(ctx: &RpcContext) -> Result { rbitcoin_query::expected_next_bits(ctx.query.as_ref(), ¶ms, Height(next_h), curtime) .map(|c| c.to_consensus()) .unwrap_or(tip_bits); - // Core `GetMinimumTime`: the next block must be above the tip median. + // The next block must be above the tip median. Core's `GetMinimumTime` + // also raises this at retarget heights; that floor is not applied here. let mintime = if tip_h == 0 { tip_time } else {