sc_consensus_nakamoto/verification/
header_verify.rs

1use crate::chain_params::{ChainParams, MEDIAN_TIME_SPAN};
2use bitcoin::blockdata::block::{Header as BitcoinHeader, ValidationError};
3use bitcoin::consensus::Params;
4use bitcoin::hashes::Hash;
5use bitcoin::pow::U256;
6use bitcoin::{BlockHash, Target};
7use sc_client_api::AuxStore;
8use sp_blockchain::HeaderBackend;
9use sp_runtime::traits::Block as BlockT;
10use std::marker::PhantomData;
11use std::sync::Arc;
12use std::time::{SystemTime, UNIX_EPOCH};
13use subcoin_primitives::BackendExt;
14
15// 2 hours
16const MAX_FUTURE_BLOCK_TIME: u32 = 2 * 60 * 60;
17
18/// Block header error.
19#[derive(Debug, thiserror::Error)]
20pub enum Error {
21    /// Block's difficulty is invalid.
22    #[error("Incorrect proof-of-work: {{ got: {got:?}, expected: {expected:?} }}")]
23    BadDifficultyBits { got: Target, expected: Target },
24    /// Block's proof-of-work is invalid.
25    #[error("proof-of-work validation failed: {0:?}")]
26    InvalidProofOfWork(ValidationError),
27    /// Block's timestamp is too far in the future.
28    #[error("Block time is too far in the future")]
29    TooFarInFuture,
30    /// Block's timestamp is too old.
31    #[error("Time is the median time of last 11 blocks or before")]
32    TimeTooOld,
33    #[error("Outdated version")]
34    BadVersion,
35    /// An error occurred in the client.
36    #[error(transparent)]
37    Client(#[from] sp_blockchain::Error),
38}
39
40/// A struct responsible for verifying block header.
41pub struct HeaderVerifier<Block, Client> {
42    client: Arc<Client>,
43    chain_params: ChainParams,
44    _phantom: PhantomData<Block>,
45}
46
47impl<Block, Client> Clone for HeaderVerifier<Block, Client> {
48    fn clone(&self) -> Self {
49        Self {
50            client: self.client.clone(),
51            chain_params: self.chain_params.clone(),
52            _phantom: self._phantom,
53        }
54    }
55}
56
57impl<Block, Client> HeaderVerifier<Block, Client> {
58    /// Constructs a new instance of [`HeaderVerifier`].
59    pub fn new(client: Arc<Client>, chain_params: ChainParams) -> Self {
60        Self {
61            client,
62            chain_params,
63            _phantom: Default::default(),
64        }
65    }
66}
67
68impl<Block, Client> HeaderVerifier<Block, Client>
69where
70    Block: BlockT,
71    Client: HeaderBackend<Block> + AuxStore,
72{
73    /// Validates the header and returns the block time, which is used for verifying the finality of
74    /// transactions.
75    ///
76    /// The validation process includes:
77    /// - Checking the proof of work.
78    /// - Validating the block's timestamp:
79    ///     - The time must not be more than 2 hours in the future.
80    ///     - The time must be greater than the median time of the last 11 blocks.
81    ///
82    /// <https://github.com/bitcoin/bitcoin/blob/6f9db1ebcab4064065ccd787161bf2b87e03cc1f/src/validation.cpp#L4146>
83    pub fn verify(&self, header: &BitcoinHeader) -> Result<u32, Error> {
84        let prev_block_hash = header.prev_blockhash;
85
86        let prev_block_header = self.client.block_header(prev_block_hash).ok_or(
87            sp_blockchain::Error::MissingHeader(prev_block_hash.to_string()),
88        )?;
89
90        let prev_block_height = self
91            .client
92            .block_number(prev_block_hash)
93            .expect("Prev block must exist as we checked before; qed");
94
95        let expected_target = get_next_work_required(
96            prev_block_height,
97            prev_block_header,
98            &self.chain_params.params,
99            &self.client,
100        );
101        let expected_bits = expected_target.to_compact_lossy().to_consensus();
102
103        let actual_target = header.target();
104        let actual_bits = actual_target.to_compact_lossy().to_consensus();
105
106        if actual_bits != expected_bits {
107            return Err(Error::BadDifficultyBits {
108                got: actual_target,
109                expected: expected_target,
110            });
111        }
112
113        header
114            .validate_pow(actual_target)
115            .map_err(Error::InvalidProofOfWork)?;
116
117        // Get the seconds since the UNIX epoch
118        let current_time = SystemTime::now()
119            .duration_since(UNIX_EPOCH)
120            .expect("Time went backwards")
121            .as_secs() as u32;
122
123        if header.time > current_time + MAX_FUTURE_BLOCK_TIME {
124            return Err(Error::TooFarInFuture);
125        }
126
127        let block_number = prev_block_height + 1;
128
129        let version = header.version.to_consensus();
130
131        if version < 2 && block_number >= self.chain_params.params.bip34_height
132            || version < 3 && block_number >= self.chain_params.params.bip66_height
133            || version < 4 && block_number >= self.chain_params.params.bip65_height
134        {
135            return Err(Error::BadVersion);
136        }
137
138        // BIP 113
139        let lock_time_cutoff = if block_number >= self.chain_params.csv_height {
140            let mtp = self.calculate_median_time_past(header);
141            if header.time <= mtp {
142                return Err(Error::TimeTooOld);
143            }
144            mtp
145        } else {
146            header.time
147        };
148
149        Ok(lock_time_cutoff)
150    }
151
152    /// Check if the proof-of-work is valid.
153    pub fn has_valid_proof_of_work(&self, header: &BitcoinHeader) -> bool {
154        let target = header.target();
155
156        if target == Target::ZERO
157            || target > Target::MAX
158            || target > self.chain_params.params.max_attainable_target
159        {
160            return false;
161        }
162
163        header.validate_pow(target).is_ok()
164    }
165
166    /// Calculates the median time of the previous few blocks prior to the header (inclusive).
167    fn calculate_median_time_past(&self, header: &BitcoinHeader) -> u32 {
168        let mut timestamps = Vec::with_capacity(MEDIAN_TIME_SPAN);
169
170        timestamps.push(header.time);
171
172        let zero_hash = BlockHash::all_zeros();
173
174        let mut block_hash = header.prev_blockhash;
175
176        for _ in 0..MEDIAN_TIME_SPAN - 1 {
177            // Genesis block
178            if block_hash == zero_hash {
179                break;
180            }
181
182            let header = self
183                .client
184                .block_header(block_hash)
185                .expect("Parent header must exist; qed");
186
187            timestamps.push(header.time);
188
189            block_hash = header.prev_blockhash;
190        }
191
192        timestamps.sort_unstable();
193
194        timestamps
195            .get(timestamps.len() / 2)
196            .copied()
197            .expect("Timestamps must be non-empty; qed")
198    }
199}
200
201/// Usually, it's just the target of last block. However, if we are in a retarget period,
202/// it will be calculated from the last 2016 blocks (about two weeks for Bitcoin mainnet).
203///
204/// <https://github.com/bitcoin/bitcoin/blob/89b910711c004c21b7d67baa888073742f7f94f0/src/pow.cpp#L13>
205fn get_next_work_required<Block, Client>(
206    last_block_height: u32,
207    last_block: BitcoinHeader,
208    params: &Params,
209    client: &Arc<Client>,
210) -> Target
211where
212    Block: BlockT,
213    Client: HeaderBackend<Block> + AuxStore,
214{
215    if params.no_pow_retargeting {
216        return last_block.target();
217    }
218
219    let height = last_block_height + 1;
220
221    let difficulty_adjustment_interval = params.difficulty_adjustment_interval() as u32;
222
223    // Only change once per difficulty adjustment interval.
224    if height >= difficulty_adjustment_interval && height % difficulty_adjustment_interval == 0 {
225        let last_retarget_height = height - difficulty_adjustment_interval;
226
227        let retarget_header_hash = client
228            .block_hash(last_retarget_height)
229            .expect("Retarget block must be available; qed");
230
231        let retarget_header = client
232            .block_header(retarget_header_hash)
233            .expect("Retarget block must be available; qed");
234
235        let first_block_time = retarget_header.time;
236
237        // timestamp of last block
238        let last_block_time = last_block.time;
239
240        calculate_next_work_required(
241            last_block.target().0,
242            first_block_time.into(),
243            last_block_time.into(),
244            params,
245        )
246    } else {
247        last_block.target()
248    }
249}
250
251// <https://github.com/bitcoin/bitcoin/blob/89b910711c004c21b7d67baa888073742f7f94f0/src/pow.cpp#L49-L72>
252fn calculate_next_work_required(
253    previous_target: U256,
254    first_block_time: u64,
255    last_block_time: u64,
256    params: &Params,
257) -> Target {
258    let mut actual_timespan = last_block_time.saturating_sub(first_block_time);
259
260    let pow_target_timespan = params.pow_target_timespan;
261
262    // Limit adjustment step.
263    //
264    // Note: The new difficulty is in [Difficulty_old * 1/4, Difficulty_old * 4].
265    if actual_timespan < pow_target_timespan / 4 {
266        actual_timespan = pow_target_timespan / 4;
267    }
268
269    if actual_timespan > pow_target_timespan * 4 {
270        actual_timespan = pow_target_timespan * 4;
271    }
272
273    let pow_limit = params.max_attainable_target;
274
275    // Retarget.
276    let target = previous_target * actual_timespan.into();
277    let target = Target(target / pow_target_timespan.into());
278
279    if target > pow_limit {
280        pow_limit
281    } else {
282        target
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use bitcoin::consensus::encode::deserialize_hex;
290
291    #[test]
292    fn test_calculate_next_work_required() {
293        // block_354816
294        let block_354816: BitcoinHeader = deserialize_hex
295            ("020000003f99814a36d2a2043b1d4bf61a410f71828eca1decbf56000000000000000000b3762ed278ac44bb953e24262cfeb952d0abe6d3b7f8b74fd24e009b96b6cb965d674655dd1317186436e79d").unwrap();
296
297        let expected_target = block_354816.target();
298
299        // block_352800, first block in this period.
300        let first_block: BitcoinHeader = deserialize_hex("0200000074c51c1cc53aaf478c643bb612da6bd17b268cd9bdccc4000000000000000000ccc0a2618a1f973dfac37827435b463abd18cbfd0f280a90432d3d78497a36cc02f33355f0171718b72a1dc7").unwrap();
301
302        // block_354815, last block in this period.
303        let last_block: BitcoinHeader = deserialize_hex("030000004c9c1b59250f30b8d360886a5433501120b056a000bdc0160000000000000000caca1bf0c55a5ba2299f9e60d10c01c679bb266c7df815ff776a1b97fd3a199ac1644655f01717182707bd59").unwrap();
304
305        let new_target = calculate_next_work_required(
306            last_block.target().0,
307            first_block.time as u64,
308            last_block.time as u64,
309            &Params::new(bitcoin::Network::Bitcoin),
310        );
311
312        assert_eq!(
313            new_target.to_compact_lossy().to_consensus(),
314            expected_target.to_compact_lossy().to_consensus(),
315            "Difficulty bits must match"
316        );
317    }
318}