Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/gbt-mintime.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 4 additions & 1 deletion crates/rbitcoin-rpc/src/methods/mine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,11 +645,14 @@ pub fn gbt_template(ctx: &RpcContext) -> Result<Value, Value> {
rbitcoin_query::expected_next_bits(ctx.query.as_ref(), &params, 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());
Expand Down
18 changes: 18 additions & 0 deletions crates/rbitcoin-rpc/src/methods_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading