sc_consensus_nakamoto/verification/
header_verify.rs

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