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..393b1e8b81 100644 --- a/crates/rbitcoin-rpc/src/methods/mine.rs +++ b/crates/rbitcoin-rpc/src/methods/mine.rs @@ -645,11 +645,14 @@ 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); + // 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 { 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]