sc_consensus_nakamoto/verification/
header_verify.rs1use 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
15const MAX_FUTURE_BLOCK_TIME: u32 = 2 * 60 * 60;
17
18#[derive(Debug, thiserror::Error)]
20pub enum Error {
21 #[error("Incorrect proof-of-work: {{ got: {got:?}, expected: {expected:?} }}")]
23 BadDifficultyBits { got: Target, expected: Target },
24 #[error("proof-of-work validation failed: {0:?}")]
26 InvalidProofOfWork(ValidationError),
27 #[error("Block time is too far in the future")]
29 TooFarInFuture,
30 #[error("Time is the median time of last 11 blocks or before")]
32 TimeTooOld,
33 #[error("Outdated version")]
34 BadVersion,
35 #[error(transparent)]
37 Client(#[from] sp_blockchain::Error),
38}
39
40pub 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 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 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 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 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 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 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 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
201fn 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 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 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
251fn 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 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 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 let block_354816: BitcoinHeader = deserialize_hex
295 ("020000003f99814a36d2a2043b1d4bf61a410f71828eca1decbf56000000000000000000b3762ed278ac44bb953e24262cfeb952d0abe6d3b7f8b74fd24e009b96b6cb965d674655dd1317186436e79d").unwrap();
296
297 let expected_target = block_354816.target();
298
299 let first_block: BitcoinHeader = deserialize_hex("0200000074c51c1cc53aaf478c643bb612da6bd17b268cd9bdccc4000000000000000000ccc0a2618a1f973dfac37827435b463abd18cbfd0f280a90432d3d78497a36cc02f33355f0171718b72a1dc7").unwrap();
301
302 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}