bitcoind: a utility to find a block height by block timestamp
We are passed a timestamp and we want to know down to what height we should rescan / roll back. That's helpful for it. It was a bit tricky although it's a simple binary search.. So to make sure i got it right i made it a standalone function in a utils/ module to be able to unit test it. See the TODO, it's theoretically not precisely entirely correct. But it's good enough for now.
This commit is contained in:
parent
6323ae0d0f
commit
51ff7d6734
350
src/bitcoin/d/utils.rs
Normal file
350
src/bitcoin/d/utils.rs
Normal file
@ -0,0 +1,350 @@
|
||||
use crate::bitcoin::{d::BlockStats, BlockChainTip};
|
||||
|
||||
use miniscript::bitcoin;
|
||||
|
||||
// As a standalone function to unit test it.
|
||||
/// Get the last block of the chain before the given date by performing a binary search.
|
||||
pub fn block_before_date<Fh, Fs>(
|
||||
target_timestamp: u32,
|
||||
chain_tip: BlockChainTip,
|
||||
mut get_hash: Fh,
|
||||
mut get_stats: Fs,
|
||||
) -> Option<BlockChainTip>
|
||||
where
|
||||
Fh: FnMut(i32) -> Option<bitcoin::BlockHash>,
|
||||
Fs: FnMut(bitcoin::BlockHash) -> BlockStats,
|
||||
{
|
||||
log::debug!("Looking for the first block before {}", target_timestamp);
|
||||
|
||||
let mut start_height = 0;
|
||||
let mut end_height = chain_tip.height;
|
||||
|
||||
let genesis_stats = get_stats(get_hash(0).expect("Genesis hash"));
|
||||
let tip_stats = get_stats(chain_tip.hash);
|
||||
if !(genesis_stats.time..tip_stats.time).contains(&target_timestamp) {
|
||||
return None;
|
||||
}
|
||||
|
||||
while start_height < end_height {
|
||||
log::debug!("Start: {}, end: {}", start_height, end_height,);
|
||||
let delta = end_height.checked_sub(start_height).unwrap();
|
||||
let current_height = start_height + delta.checked_div(2).unwrap();
|
||||
// We want the last block with a timestamp below, not the first with a higher one.
|
||||
let next_height = current_height.checked_add(1).unwrap();
|
||||
let next_stats = get_stats(get_hash(next_height)?);
|
||||
log::debug!("Current next block: {:?}", next_stats);
|
||||
|
||||
if target_timestamp > next_stats.time {
|
||||
start_height = next_height;
|
||||
} else {
|
||||
assert!(current_height < end_height);
|
||||
end_height = current_height;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: the timestamps in the chain are not strictly ordered. There could technically be a
|
||||
// timestamp above the target a bit down this height. I think we would be safe by scanning the
|
||||
// last 12 blocks and checking their timestamp is below the target. Would we?
|
||||
log::debug!("Result height: {}", start_height);
|
||||
Some(BlockChainTip {
|
||||
height: start_height,
|
||||
hash: get_hash(start_height)?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
// The expected number of seconds in average between two blocks.
|
||||
const EXPECTED_BLOCK_INTERVAL_SECS: u32 = 600;
|
||||
|
||||
// Inefficient dummy implementation of BitcoinD's self.get_block_hash
|
||||
fn get_hash(chain: &[(BlockChainTip, BlockStats)], height: i32) -> Option<bitcoin::BlockHash> {
|
||||
chain
|
||||
.iter()
|
||||
.find(|(tip, _)| tip.height == height)
|
||||
.map(|(tip, _)| tip.hash)
|
||||
}
|
||||
|
||||
// Inefficient dummy implementation of BitcoinD's self.get_block_stats
|
||||
fn get_stats(chain: &[(BlockChainTip, BlockStats)], hash: bitcoin::BlockHash) -> BlockStats {
|
||||
chain
|
||||
.iter()
|
||||
.find(|(tip, _)| tip.hash == hash)
|
||||
.unwrap()
|
||||
.1
|
||||
.clone()
|
||||
}
|
||||
|
||||
macro_rules! bh {
|
||||
($h_str: literal) => {
|
||||
bitcoin::BlockHash::from_str($h_str).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
// Create a dummy BlockStats struct with the given time
|
||||
fn create_stats(time: u32) -> BlockStats {
|
||||
// TODO
|
||||
BlockStats {
|
||||
height: 0,
|
||||
confirmations: 0,
|
||||
previous_blockhash: Some(bh!(
|
||||
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
|
||||
)),
|
||||
blockhash: bh!("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"),
|
||||
time,
|
||||
median_time_past: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blk_before_time() {
|
||||
// A timestamp after the tip's
|
||||
let dummy_chain = [
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 0,
|
||||
hash: bh!("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"),
|
||||
},
|
||||
create_stats(1231006505),
|
||||
),
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 761683,
|
||||
hash: bh!("0000000000000000000560bb21fbab991fe5f7d9a949eb424f9be3c34a55a54f"),
|
||||
},
|
||||
create_stats(1667558116),
|
||||
),
|
||||
];
|
||||
assert!(block_before_date(
|
||||
dummy_chain[0].1.time + 1,
|
||||
dummy_chain[0].0,
|
||||
|h| get_hash(&dummy_chain, h),
|
||||
|h| get_stats(&dummy_chain, h),
|
||||
)
|
||||
.is_none());
|
||||
|
||||
// A timestamp before the genesis
|
||||
let dummy_chain = [
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 0,
|
||||
hash: bh!("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"),
|
||||
},
|
||||
create_stats(1231006505),
|
||||
),
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 761683,
|
||||
hash: bh!("0000000000000000000560bb21fbab991fe5f7d9a949eb424f9be3c34a55a54f"),
|
||||
},
|
||||
create_stats(1667558116),
|
||||
),
|
||||
];
|
||||
assert!(block_before_date(
|
||||
dummy_chain[0].1.time - 1,
|
||||
dummy_chain[0].0,
|
||||
|h| get_hash(&dummy_chain, h),
|
||||
|h| get_stats(&dummy_chain, h),
|
||||
)
|
||||
.is_none());
|
||||
|
||||
// Simulate and detail a full binary search through a dummy chain.
|
||||
let target_timestamp = 1531006505;
|
||||
let dummy_chain = [
|
||||
// Genesis: will be queried at step 0 (0, 761_683)
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 0,
|
||||
hash: bh!("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"),
|
||||
},
|
||||
create_stats(1231006505),
|
||||
),
|
||||
// Step 1 (0, 761_683): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 380_842,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19ba3"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 10),
|
||||
),
|
||||
// Step 4 (380_842, 476_052): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 428_448,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19ba4"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 9),
|
||||
),
|
||||
// Step 6 (428_448, 452_250): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 440_350,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19ba5"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 8),
|
||||
),
|
||||
// Step 7 (440_350, 452_250): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 446_301,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19ba6"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 7),
|
||||
),
|
||||
// Step 8 (446_301, 452_250): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 449_276,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19ba7"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 6),
|
||||
),
|
||||
// Step 9 (449_276, 452_250): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 450_764,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19ba8"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 5),
|
||||
),
|
||||
// Step 10 (450_764, 452_250): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 451_508,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19ba9"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 4),
|
||||
),
|
||||
// Step 11 (451_508, 452_250): timestamp too low (and equal to the previous one).
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 451_880,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bab"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 4),
|
||||
),
|
||||
// Step 12 (451_880, 452_250): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_066,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bac"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 3),
|
||||
),
|
||||
// Step 13 (452_066, 452_250): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_159,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bad"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS * 2),
|
||||
),
|
||||
// Step 14 (452_159, 452_250): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_205,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bae"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS),
|
||||
),
|
||||
// Step 15 (452_205, 452_250): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_228,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19baf"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS),
|
||||
),
|
||||
// Step 17 (452_228, 452_239): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_234,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bb0"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS / 2),
|
||||
),
|
||||
// Step 18 (452_234, 452_239): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_237,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bb1"),
|
||||
},
|
||||
create_stats(target_timestamp - EXPECTED_BLOCK_INTERVAL_SECS / 4),
|
||||
),
|
||||
// Step 21 (452_237, 452_238): timestamp too low.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_238,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bb2"),
|
||||
},
|
||||
create_stats(target_timestamp - 1),
|
||||
),
|
||||
// Step 20 (452_237, 452_239): timestamp too high (first block at this timestamp).
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_239,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bb3"),
|
||||
},
|
||||
create_stats(target_timestamp),
|
||||
),
|
||||
// Step 16 (452_228, 452_250): timestamp too high (timestamp don't necessarily
|
||||
// increase per block height).
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_240,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bb4"),
|
||||
},
|
||||
create_stats(target_timestamp + 1),
|
||||
),
|
||||
// Step 5 (428_448, 476_052): timestamp too high (again equal! That's possible).
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 452_251,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bb5"),
|
||||
},
|
||||
create_stats(target_timestamp),
|
||||
),
|
||||
// Step 3 (380_842, 571_262): timestamp too high (because equal).
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 476_053,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bb6"),
|
||||
},
|
||||
create_stats(target_timestamp),
|
||||
),
|
||||
// Step 2 (380_842, 761_683): timestamp too high.
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 571_263,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bb7"),
|
||||
},
|
||||
create_stats(target_timestamp + EXPECTED_BLOCK_INTERVAL_SECS),
|
||||
),
|
||||
// Tip: will be queried at step 0
|
||||
(
|
||||
BlockChainTip {
|
||||
height: 761_683,
|
||||
hash: bh!("0000000000000000000560bb21fbab991fe5f7d9a949eb424f9be3c34a55a54f"),
|
||||
},
|
||||
create_stats(1667558116),
|
||||
),
|
||||
];
|
||||
assert_eq!(
|
||||
block_before_date(
|
||||
target_timestamp,
|
||||
dummy_chain[dummy_chain.len() - 1].0,
|
||||
|h| get_hash(&dummy_chain, h),
|
||||
|h| get_stats(&dummy_chain, h),
|
||||
)
|
||||
.unwrap(),
|
||||
// Step 21 above
|
||||
BlockChainTip {
|
||||
height: 452_238,
|
||||
hash: bh!("000000000000000005c0655db17fde80f67ff0502a62b7250ed2685619d19bb2"),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user