ic_btc_validation/
header.rs

1use bitcoin::{util::uint::Uint256, BlockHash, BlockHeader, Network};
2
3use crate::{
4    constants::{
5        checkpoints, last_checkpoint, latest_checkpoint_height, max_target, no_pow_retargeting,
6        pow_limit_bits, BLOCKS_IN_ONE_YEAR, DIFFICULTY_ADJUSTMENT_INTERVAL, TEN_MINUTES,
7    },
8    BlockHeight,
9};
10
11/// An error thrown when trying to validate a header.
12#[derive(Debug)]
13pub enum ValidateHeaderError {
14    /// Used when the timestamp in the header is lower than
15    /// the median of timestamps of past 11 headers.
16    HeaderIsOld,
17    /// Used when the header doesn't match with a checkpoint.
18    DoesNotMatchCheckpoint,
19    /// Used when the PoW in the header is invalid as per the target mentioned
20    /// in the header.
21    InvalidPoWForHeaderTarget,
22    /// Used when the PoW in the header is invalid as per the target
23    /// computed based on the previous headers.
24    InvalidPoWForComputedTarget,
25    /// Used when the target in the header is greater than the max possible
26    /// value.
27    TargetDifficultyAboveMax,
28    /// The next height is less than the tip height - 52_596 (one year worth of blocks).
29    HeightTooLow,
30    /// Used when the predecessor of the input header is not found in the
31    /// HeaderStore.
32    PrevHeaderNotFound,
33}
34
35pub trait HeaderStore {
36    /// Retrieves the header from the store.
37    fn get_header(&self, hash: &BlockHash) -> Option<(BlockHeader, BlockHeight)>;
38    /// Retrieves the current height of the block chain.
39    fn get_height(&self) -> BlockHeight;
40    /// Retrieves the initial hash the store starts from.
41    fn get_initial_hash(&self) -> BlockHash;
42}
43
44/// Validates a header. If a failure occurs, a
45/// [ValidateHeaderError](ValidateHeaderError) will be returned.
46pub fn validate_header(
47    network: &Network,
48    store: &impl HeaderStore,
49    header: &BlockHeader,
50) -> Result<(), ValidateHeaderError> {
51    let chain_height = store.get_height();
52    let (prev_header, prev_height) = match store.get_header(&header.prev_blockhash) {
53        Some(result) => result,
54        None => {
55            return Err(ValidateHeaderError::PrevHeaderNotFound);
56        }
57    };
58
59    if !is_header_within_one_year_of_tip(prev_height, chain_height) {
60        return Err(ValidateHeaderError::HeightTooLow);
61    }
62
63    if !is_timestamp_valid(store, header) {
64        return Err(ValidateHeaderError::HeaderIsOld);
65    }
66
67    if !is_checkpoint_valid(network, prev_height, header, chain_height) {
68        return Err(ValidateHeaderError::DoesNotMatchCheckpoint);
69    }
70
71    let header_target = header.target();
72    if header_target > max_target(network) {
73        return Err(ValidateHeaderError::TargetDifficultyAboveMax);
74    }
75
76    if header.validate_pow(&header_target).is_err() {
77        return Err(ValidateHeaderError::InvalidPoWForHeaderTarget);
78    }
79
80    let target = get_next_target(network, store, &prev_header, prev_height, header);
81    if let Err(err) = header.validate_pow(&target) {
82        match err {
83            bitcoin::Error::BlockBadProofOfWork => println!("bad proof of work"),
84            bitcoin::Error::BlockBadTarget => println!("bad target"),
85            _ => {}
86        };
87        return Err(ValidateHeaderError::InvalidPoWForComputedTarget);
88    }
89
90    Ok(())
91}
92
93/// Checks if block height is higher than the last checkpoint height.
94/// By beeing beyond the last checkpoint we are sure that we store the correct chain up to the height
95/// of the last checkpoint.  
96pub fn is_beyond_last_checkpoint(network: &Network, height: BlockHeight) -> bool {
97    match last_checkpoint(network) {
98        Some(last) => last <= height,
99        None => true,
100    }
101}
102
103/// This validates the header against the network's checkpoints.
104/// 1. If the next header is at a checkpoint height, the checkpoint is compared to the next header's block hash.
105/// 2. If the header is not the same height, the function then compares the height to the latest checkpoint.
106///    If the next header's height is less than the last checkpoint's height, the header is invalid.
107fn is_checkpoint_valid(
108    network: &Network,
109    prev_height: BlockHeight,
110    header: &BlockHeader,
111    chain_height: BlockHeight,
112) -> bool {
113    let checkpoints = checkpoints(network);
114    let next_height = prev_height.saturating_add(1);
115    if let Some(next_hash) = checkpoints.get(&next_height) {
116        return *next_hash == header.block_hash();
117    }
118
119    let checkpoint_height = latest_checkpoint_height(network, chain_height);
120    next_height > checkpoint_height
121}
122
123/// This validates that the header has a height that is within 1 year of the tip height.
124fn is_header_within_one_year_of_tip(prev_height: BlockHeight, chain_height: BlockHeight) -> bool {
125    // perhaps checked_add would be preferable here, if the next height would cause an overflow,
126    // we should know about it instead of being swallowed.
127    let header_height = prev_height
128        .checked_add(1)
129        .expect("next height causes an overflow");
130
131    let height_one_year_ago = chain_height.saturating_sub(BLOCKS_IN_ONE_YEAR);
132    header_height >= height_one_year_ago
133}
134
135/// Validates if a header's timestamp is valid.
136/// Bitcoin Protocol Rules wiki https://en.bitcoin.it/wiki/Protocol_rules says,
137/// "Reject if timestamp is the median time of the last 11 blocks or before"
138fn is_timestamp_valid(store: &impl HeaderStore, header: &BlockHeader) -> bool {
139    let mut times = vec![];
140    let mut current_header = *header;
141    let initial_hash = store.get_initial_hash();
142    for _ in 0..11 {
143        if let Some((prev_header, _)) = store.get_header(&current_header.prev_blockhash) {
144            times.push(prev_header.time);
145            if current_header.prev_blockhash == initial_hash {
146                break;
147            }
148            current_header = prev_header;
149        }
150    }
151
152    times.sort_unstable();
153    let median = times[times.len() / 2];
154    header.time > median
155}
156
157/// Gets the next target by doing the following:
158/// * If the network allows blocks to have the max target (testnet & regtest),
159///   the next difficulty is searched for unless the header's timestamp is
160///   greater than 20 minutes from the previous header's timestamp.
161/// * If the network does not allow blocks with the max target, the next
162///   difficulty is computed and then cast into the next target.
163fn get_next_target(
164    network: &Network,
165    store: &impl HeaderStore,
166    prev_header: &BlockHeader,
167    prev_height: BlockHeight,
168    header: &BlockHeader,
169) -> Uint256 {
170    match network {
171        Network::Testnet | Network::Regtest => {
172            if (prev_height + 1) % DIFFICULTY_ADJUSTMENT_INTERVAL != 0 {
173                // This if statements is reached only for Regtest and Testnet networks
174                // Here is the quote from "https://en.bitcoin.it/wiki/Testnet"
175                // "If no block has been found in 20 minutes, the difficulty automatically
176                // resets back to the minimum for a single block, after which it
177                // returns to its previous value."
178                if header.time > prev_header.time + TEN_MINUTES * 2 {
179                    //If no block has been found in 20 minutes, then use the maximum difficulty
180                    // target
181                    max_target(network)
182                } else {
183                    //If the block has been found within 20 minutes, then use the previous
184                    // difficulty target that is not equal to the maximum difficulty target
185                    BlockHeader::u256_from_compact_target(find_next_difficulty_in_chain(
186                        network,
187                        store,
188                        prev_header,
189                        prev_height,
190                    ))
191                }
192            } else {
193                BlockHeader::u256_from_compact_target(compute_next_difficulty(
194                    network,
195                    store,
196                    prev_header,
197                    prev_height,
198                ))
199            }
200        }
201        Network::Bitcoin | Network::Signet => BlockHeader::u256_from_compact_target(
202            compute_next_difficulty(network, store, prev_header, prev_height),
203        ),
204    }
205}
206
207/// This method is only valid when used for testnet and regtest networks.
208/// As per "https://en.bitcoin.it/wiki/Testnet",
209/// "If no block has been found in 20 minutes, the difficulty automatically
210/// resets back to the minimum for a single block, after which it
211/// returns to its previous value." This function is used to compute the
212/// difficulty target in case the block has been found within 20
213/// minutes.
214fn find_next_difficulty_in_chain(
215    network: &Network,
216    store: &impl HeaderStore,
217    prev_header: &BlockHeader,
218    prev_height: BlockHeight,
219) -> u32 {
220    // This is the maximum difficulty target for the network
221    let pow_limit_bits = pow_limit_bits(network);
222    match network {
223        Network::Testnet | Network::Regtest => {
224            let mut current_header = *prev_header;
225            let mut current_height = prev_height;
226            let mut current_hash = prev_header.block_hash();
227            let initial_header_hash = store.get_initial_hash();
228
229            // Keep traversing the blockchain backwards from the recent block to initial
230            // header hash.
231            while current_hash != initial_header_hash {
232                if current_header.bits != pow_limit_bits
233                    || current_height % DIFFICULTY_ADJUSTMENT_INTERVAL == 0
234                {
235                    return current_header.bits;
236                }
237
238                // Traverse to the previous header
239                let header_info = store
240                    .get_header(&current_header.prev_blockhash)
241                    .expect("previous header should be in the header store");
242                current_header = header_info.0;
243                current_height = header_info.1;
244                current_hash = current_header.prev_blockhash;
245            }
246            pow_limit_bits
247        }
248        Network::Bitcoin | Network::Signet => pow_limit_bits,
249    }
250}
251
252/// This function returns the difficult target to be used for the current
253/// header given the previous header
254fn compute_next_difficulty(
255    network: &Network,
256    store: &impl HeaderStore,
257    prev_header: &BlockHeader,
258    prev_height: BlockHeight,
259) -> u32 {
260    // Difficulty is adjusted only once in every interval of 2 weeks (2016 blocks)
261    // If an interval boundary is not reached, then previous difficulty target is
262    // returned Regtest network doesn't adjust PoW difficult levels. For
263    // regtest, simply return the previous difficulty target
264
265    if (prev_height + 1) % DIFFICULTY_ADJUSTMENT_INTERVAL != 0 || no_pow_retargeting(network) {
266        return prev_header.bits;
267    }
268
269    // Computing the last header with height multiple of 2016
270    let mut current_header = *prev_header;
271    for _i in 0..(DIFFICULTY_ADJUSTMENT_INTERVAL - 1) {
272        if let Some((header, _)) = store.get_header(&current_header.prev_blockhash) {
273            current_header = header;
274        }
275    }
276    // last_adjustment_header is the last header with height multiple of 2016
277    let last_adjustment_header = current_header;
278    let last_adjustment_time = last_adjustment_header.time;
279
280    // Computing the time interval between the last adjustment header time and
281    // current time. The expected value actual_interval is 2 weeks assuming
282    // the expected block time is 10 mins. But most of the time, the
283    // actual_interval will deviate slightly from 2 weeks. Our goal is to
284    // readjust the difficulty target so that the expected time taken for the next
285    // 2016 blocks is again 2 weeks.
286    let actual_interval = prev_header.time - last_adjustment_time;
287    let mut adjusted_interval = actual_interval as u32;
288
289    // The target_adjustment_interval_time is 2 weeks of time expressed in seconds
290    let target_adjustment_interval_time: u32 = DIFFICULTY_ADJUSTMENT_INTERVAL * TEN_MINUTES; //Number of seconds in 2 weeks
291
292    // Adjusting the actual_interval to [0.5 week, 8 week] range in case the
293    // actual_interval deviates too much from the expected 2 weeks.
294    adjusted_interval = u32::max(adjusted_interval, target_adjustment_interval_time / 4);
295    adjusted_interval = u32::min(adjusted_interval, target_adjustment_interval_time * 4);
296
297    // Computing new difficulty target.
298    // new difficulty target = old difficult target * (adjusted_interval /
299    // 2_weeks);
300    let mut target = prev_header.target();
301    target = target.mul_u32(adjusted_interval);
302    target = target / Uint256::from_u64(target_adjustment_interval_time as u64).unwrap();
303
304    // Adjusting the newly computed difficulty target so that it doesn't exceed the
305    // max_difficulty_target limit
306    target = Uint256::min(target, max_target(network));
307
308    // Converting the target (Uint256) into a 32 bit representation used by Bitcoin
309    BlockHeader::compact_target_from_u256(&target)
310}
311
312#[cfg(test)]
313mod test {
314
315    use std::{collections::HashMap, path::PathBuf, str::FromStr};
316
317    use bitcoin::{consensus::deserialize, hashes::hex::FromHex, TxMerkleNode};
318    use csv::Reader;
319
320    use super::*;
321    use crate::constants::test::{
322        MAINNET_HEADER_11109, MAINNET_HEADER_11110, MAINNET_HEADER_11111, MAINNET_HEADER_586656,
323        MAINNET_HEADER_705600, MAINNET_HEADER_705601, MAINNET_HEADER_705602,
324        TESTNET_HEADER_2132555, TESTNET_HEADER_2132556,
325    };
326
327    #[derive(Clone)]
328    struct StoredHeader {
329        header: BlockHeader,
330        height: BlockHeight,
331    }
332
333    struct SimpleHeaderStore {
334        headers: HashMap<BlockHash, StoredHeader>,
335        height: BlockHeight,
336        initial_hash: BlockHash,
337    }
338
339    impl SimpleHeaderStore {
340        fn new(initial_header: BlockHeader, height: BlockHeight) -> Self {
341            let initial_hash = initial_header.block_hash();
342            let mut headers = HashMap::new();
343            headers.insert(
344                initial_hash,
345                StoredHeader {
346                    header: initial_header,
347                    height,
348                },
349            );
350
351            Self {
352                headers,
353                height,
354                initial_hash,
355            }
356        }
357
358        fn add(&mut self, header: BlockHeader) {
359            let prev = self
360                .headers
361                .get(&header.prev_blockhash)
362                .expect("prev hash missing");
363            let stored_header = StoredHeader {
364                header,
365                height: prev.height + 1,
366            };
367
368            self.height = stored_header.height;
369            self.headers.insert(header.block_hash(), stored_header);
370        }
371    }
372
373    impl HeaderStore for SimpleHeaderStore {
374        fn get_header(&self, hash: &BlockHash) -> Option<(BlockHeader, BlockHeight)> {
375            self.headers
376                .get(hash)
377                .map(|stored| (stored.header, stored.height))
378        }
379
380        fn get_height(&self) -> BlockHeight {
381            self.height
382        }
383
384        fn get_initial_hash(&self) -> BlockHash {
385            self.initial_hash
386        }
387    }
388
389    fn deserialize_header(encoded_bytes: &str) -> BlockHeader {
390        let bytes = Vec::from_hex(encoded_bytes).expect("failed to decoded bytes");
391        deserialize(bytes.as_slice()).expect("failed to deserialize")
392    }
393
394    /// This function reads `num_headers` headers from `tests/data/headers.csv`
395    /// and returns them.
396    /// This function reads `num_headers` headers from `blockchain_headers.csv`
397    /// and returns them.
398    fn get_bitcoin_headers() -> Vec<BlockHeader> {
399        let rdr = Reader::from_path(
400            PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
401                .join("tests/data/headers.csv"),
402        );
403        assert!(rdr.is_ok(), "Unable to find blockchain_headers.csv file");
404        let mut rdr = rdr.unwrap();
405        let mut headers = vec![];
406        for result in rdr.records() {
407            let record = result.unwrap();
408            let header = BlockHeader {
409                version: i32::from_str_radix(record.get(0).unwrap(), 16).unwrap(),
410                prev_blockhash: BlockHash::from_str(record.get(1).unwrap()).unwrap(),
411                merkle_root: TxMerkleNode::from_str(record.get(2).unwrap()).unwrap(),
412                time: u32::from_str_radix(record.get(3).unwrap(), 16).unwrap(),
413                bits: u32::from_str_radix(record.get(4).unwrap(), 16).unwrap(),
414                nonce: u32::from_str_radix(record.get(5).unwrap(), 16).unwrap(),
415            };
416            headers.push(header);
417        }
418        headers
419    }
420
421    #[test]
422    fn test_simple_mainnet() {
423        let header_705600 = deserialize_header(MAINNET_HEADER_705600);
424        let header_705601 = deserialize_header(MAINNET_HEADER_705601);
425        let store = SimpleHeaderStore::new(header_705600, 705_600);
426        let result = validate_header(&Network::Bitcoin, &store, &header_705601);
427        assert!(result.is_ok());
428    }
429
430    #[test]
431    fn test_simple_testnet() {
432        let header_2132555 = deserialize_header(TESTNET_HEADER_2132555);
433        let header_2132556 = deserialize_header(TESTNET_HEADER_2132556);
434        let store = SimpleHeaderStore::new(header_2132555, 2_132_555);
435        let result = validate_header(&Network::Testnet, &store, &header_2132556);
436        assert!(result.is_ok());
437    }
438
439    #[test]
440    fn test_is_header_valid() {
441        let header_586656 = deserialize_header(MAINNET_HEADER_586656);
442        let mut store = SimpleHeaderStore::new(header_586656, 586_656);
443        let headers = get_bitcoin_headers();
444        for (i, header) in headers.iter().enumerate() {
445            let result = validate_header(&Network::Bitcoin, &store, header);
446            assert!(
447                result.is_ok(),
448                "Failed to validate header on line {}: {:?}",
449                i,
450                result
451            );
452            store.add(*header);
453        }
454    }
455
456    #[test]
457    fn test_is_timestamp_valid() {
458        let header_705600 = deserialize_header(MAINNET_HEADER_705600);
459        let header_705601 = deserialize_header(MAINNET_HEADER_705601);
460        let header_705602 = deserialize_header(MAINNET_HEADER_705602);
461        let mut store = SimpleHeaderStore::new(header_705600, 705_600);
462        store.add(header_705601);
463        store.add(header_705602);
464
465        let mut header = BlockHeader {
466            version: 0x20800004,
467            prev_blockhash: BlockHash::from_hex(
468                "00000000000000000001eea12c0de75000c2546da22f7bf42d805c1d2769b6ef",
469            )
470            .unwrap(),
471            merkle_root: TxMerkleNode::from_hex(
472                "c120ff2ae1363593a0b92e0d281ec341a0cc989b4ee836dc3405c9f4215242a6",
473            )
474            .unwrap(),
475            time: 1634590600,
476            bits: 0x170e0408,
477            nonce: 0xb48e8b0a,
478        };
479        assert!(is_timestamp_valid(&store, &header));
480
481        // Monday, October 18, 2021 20:26:40
482        header.time = 1634588800;
483        assert!(!is_timestamp_valid(&store, &header));
484
485        let result = validate_header(&Network::Bitcoin, &store, &header);
486        assert!(matches!(result, Err(ValidateHeaderError::HeaderIsOld)));
487    }
488
489    #[test]
490    fn test_is_header_valid_missing_prev_header() {
491        let header_705600 = deserialize_header(MAINNET_HEADER_705600);
492        let header_705602 = deserialize_header(MAINNET_HEADER_705602);
493        let store = SimpleHeaderStore::new(header_705600, 705_600);
494        let result = validate_header(&Network::Bitcoin, &store, &header_705602);
495        assert!(matches!(
496            result,
497            Err(ValidateHeaderError::PrevHeaderNotFound)
498        ));
499    }
500
501    #[test]
502    fn test_is_header_valid_checkpoint_valid_at_height() {
503        let network = Network::Bitcoin;
504        let header_11110 = deserialize_header(MAINNET_HEADER_11110);
505        let mut header_11111 = deserialize_header(MAINNET_HEADER_11111);
506        let store = SimpleHeaderStore::new(header_11110, 11110);
507        let (_, prev_height) = store.get_header(&header_11111.prev_blockhash).unwrap();
508
509        assert!(is_checkpoint_valid(
510            &network,
511            prev_height,
512            &header_11111,
513            store.get_height()
514        ));
515
516        // Change time to slightly modify the block hash to make it invalid for the
517        // checkpoint.
518        header_11111.time -= 1;
519
520        let result = validate_header(&network, &store, &header_11111);
521        assert!(matches!(
522            result,
523            Err(ValidateHeaderError::DoesNotMatchCheckpoint)
524        ));
525    }
526
527    #[test]
528    fn test_is_header_valid_checkpoint_valid_detect_fork_around_11111() {
529        let network = Network::Bitcoin;
530        let header_11109 = deserialize_header(MAINNET_HEADER_11109);
531        let header_11110 = deserialize_header(MAINNET_HEADER_11110);
532        let header_11111 = deserialize_header(MAINNET_HEADER_11111);
533        // Make a header for height 11110 that would cause a fork.
534        let mut bad_header_11110 = header_11110;
535        bad_header_11110.time -= 1;
536
537        let mut store = SimpleHeaderStore::new(header_11109, 11109);
538        store.add(header_11110);
539
540        let (_, prev_height) = store.get_header(&header_11111.prev_blockhash).unwrap();
541
542        assert!(is_checkpoint_valid(
543            &network,
544            prev_height,
545            &header_11111,
546            store.get_height()
547        ));
548
549        store.add(header_11111);
550
551        // This should return false as bad_header_11110 is a fork.
552        let (_, prev_height) = store.get_header(&header_11111.prev_blockhash).unwrap();
553        assert!(!is_checkpoint_valid(
554            &network,
555            prev_height,
556            &bad_header_11110,
557            store.get_height()
558        ));
559    }
560
561    #[test]
562    fn test_is_header_valid_checkpoint_valid_detect_fork_around_705600() {
563        let network = Network::Bitcoin;
564        let header_705600 = deserialize_header(MAINNET_HEADER_705600);
565        let header_705601 = deserialize_header(MAINNET_HEADER_705601);
566        let store = SimpleHeaderStore::new(header_705600, 705_600);
567        let (_, prev_height) = store.get_header(&header_705601.prev_blockhash).unwrap();
568
569        assert!(is_checkpoint_valid(
570            &network,
571            prev_height,
572            &header_705601,
573            store.get_height()
574        ));
575    }
576
577    #[test]
578    fn test_is_header_valid_invalid_header_target() {
579        let header_705600 = deserialize_header(MAINNET_HEADER_705600);
580        let mut header = deserialize_header(MAINNET_HEADER_705601);
581        header.bits = pow_limit_bits(&Network::Bitcoin);
582        let store = SimpleHeaderStore::new(header_705600, 705_600);
583        let result = validate_header(&Network::Bitcoin, &store, &header);
584        assert!(matches!(
585            result,
586            Err(ValidateHeaderError::InvalidPoWForHeaderTarget)
587        ));
588    }
589
590    #[test]
591    fn test_is_header_valid_invalid_computed_target() {
592        let header_705600 = deserialize_header(MAINNET_HEADER_705600);
593        let header = deserialize_header(MAINNET_HEADER_705601);
594        let store = SimpleHeaderStore::new(header_705600, 705_600);
595        let result = validate_header(&Network::Regtest, &store, &header);
596        assert!(matches!(
597            result,
598            Err(ValidateHeaderError::InvalidPoWForComputedTarget)
599        ));
600    }
601
602    #[test]
603    fn test_is_header_valid_target_difficulty_above_max() {
604        let header_705600 = deserialize_header(MAINNET_HEADER_705600);
605        let mut header = deserialize_header(MAINNET_HEADER_705601);
606        header.bits = pow_limit_bits(&Network::Regtest);
607        let store = SimpleHeaderStore::new(header_705600, 705_600);
608        let result = validate_header(&Network::Bitcoin, &store, &header);
609        assert!(matches!(
610            result,
611            Err(ValidateHeaderError::TargetDifficultyAboveMax)
612        ));
613    }
614
615    #[test]
616    fn test_is_header_within_one_year_of_tip_next_height_is_above_the_minimum() {
617        assert!(
618            is_header_within_one_year_of_tip(700_000, 650_000),
619            "next height is above the one year minimum"
620        );
621        assert!(
622            is_header_within_one_year_of_tip(700_000, 750_000),
623            "next height is within the one year range"
624        );
625        assert!(
626            !is_header_within_one_year_of_tip(700_000, 800_000),
627            "next height is below the one year minimum"
628        );
629    }
630
631    #[test]
632    #[should_panic(expected = "next height causes an overflow")]
633    fn test_is_header_within_one_year_of_tip_should_panic_as_next_height_is_too_high() {
634        is_header_within_one_year_of_tip(BlockHeight::MAX, 0);
635    }
636
637    #[test]
638    fn test_is_header_within_one_year_of_tip_chain_height_is_less_than_one_year() {
639        assert!(
640            is_header_within_one_year_of_tip(1, 0),
641            "chain height is less than one year"
642        );
643        assert!(
644            is_header_within_one_year_of_tip(1, BLOCKS_IN_ONE_YEAR + 2),
645            "chain height difference is exactly one year"
646        );
647        assert!(
648            !is_header_within_one_year_of_tip(1, BLOCKS_IN_ONE_YEAR + 3),
649            "chain height difference is one year + 1 block"
650        );
651    }
652}