cfxcore/consensus/pivot_hint/mod.rs
1//! Pivot hint provides validation support during blockchain synchronization and
2//! historical data execution by leveraging trusted pivot chain information from
3//! authoritative sources.
4//!
5//! During the synchronization process of archive nodes, when processing
6//! historical data, pivot hints help prevent execution on forked branches and
7//! protect against problematic historical states that occurred on
8//! mainnet/testnet chains.
9//!
10//! # File Structure
11//! The pivot hint file consists of three main parts:
12//! * Header Part (28 bytes): Contains configuration parameters
13//! * Page Digests Part: A list of keccak hashes for each page
14//! * Pages Part: Sequential storage of all pages
15//!
16//! # Page Organization
17//! Blocks are organized into pages based on several configurable parameters:
18//! * `range_max`: Upper bound (exclusive) of block heights for stored hashes
19//! * `page_interval`: Number of consecutive blocks in each page
20//! * Each page contains:
21//! - Major section: Full hashes for blocks every `major_interval` heights
22//! - Minor section: Hash prefixes (in length of `minor_hash_length`) for
23//! blocks every `minor_interval` heights
24//!
25//! # Parameter Constraints
26//! The following parameters must maintain integer multiple relationships:
27//! * `range_max` must be a multiple of `page_interval`
28//! * `page_interval` must be a multiple of `major_interval`
29//! * `major_interval` must be a multiple of `minor_interval`
30//!
31//! # Fork Validation
32//! During fork validation, when the consensus layer attempts to switch the
33//! pivot chain from branch A to branch B, it must provide:
34//! * `fork_at`: The first block height where branch A and B diverge
35//! * `me_height`: The last block height of branch B
36//! * A query interface to retrieve block hashes on branch B within range
37//! [fork_at, me_height]
38//!
39//! The validation process follows these rules:
40//! * If [fork_at, me_height] covers with major section records, validation uses
41//! the last recorded full hash
42//! * If no major section records covered but minor section overlap exists,
43//! validation uses minor section records (note: this may allow switching to
44//! branches that aren't on the final main chain)
45//! * If neither major nor minor section overlap exists, the switch is allowed
46//!
47//! When `fork_at` exceeds `range_max`, it indicates the fork point is beyond
48//! the static file records, and the switch is automatically allowed.
49//!
50//! # Loading Process
51//! 1. Load and validate Header Part parameters
52//! 2. Load Page Digests Part and verify against predetermined Pivot Hint
53//! Checksum
54//! 3. Keep Page Digests in memory
55//! 4. Verify each page against Page Digests when loading to prevent corruption
56
57mod config;
58mod header;
59mod page;
60#[cfg(test)]
61mod tests;
62
63pub use config::PivotHintConfig;
64use header::{PivotHintHeader, HEADER_LENGTH};
65use page::PivotHintPage;
66
67use std::{
68 fs::File,
69 io::{Read, Seek, SeekFrom},
70 sync::atomic::{AtomicBool, Ordering},
71};
72
73use crate::hash::{keccak, H256};
74use lru_time_cache::LruCache;
75use parking_lot::RwLock;
76
77/// Manages pivot block hash records for chain fork validation during sync
78/// process.
79pub struct PivotHint {
80 /// Path to the pivot hint file
81 file_path: String,
82
83 /// Module status flag. Set to false if error occurs, disabling the module
84 /// without thread panic.
85 active: AtomicBool,
86
87 /// Pivot hint header with configuration parameters
88 header: PivotHintHeader,
89
90 /// LRU cache storing loaded pivot hint pages
91 pages: RwLock<LruCache<u64, PivotHintPage>>,
92
93 /// Keccak hashes of all pages, kept in memory for integrity verification
94 page_digests: Vec<H256>,
95}
96
97impl PivotHint {
98 /// Creates a new PivotHint instance by loading and validating the pivot
99 /// hint file.
100 ///
101 /// # Steps
102 /// 1. Loads and validates the header
103 /// 2. Loads page digests and verifies against provided checksum
104 /// 3. Initializes LRU cache for page data
105 ///
106 /// # Arguments
107 /// * `conf` - Configuration containing file path and expected checksum
108 ///
109 /// # Errors
110 /// * File open/read errors
111 /// * Header parsing errors
112 /// * Checksum mismatch
113 pub fn new(conf: &PivotHintConfig) -> Result<Self, String> {
114 let mut file = File::open(&conf.file_path)
115 .map_err(|e| format!("Cannot open file: {:?}", e))?;
116 let mut raw_header = [0u8; HEADER_LENGTH];
117 file.read_exact(&mut raw_header)
118 .map_err(|e| format!("Cannot load header: {:?}", e))?;
119 let header = PivotHintHeader::from_raw(raw_header)
120 .map_err(|e| format!("Cannot parse and check header: {}", e))?;
121
122 let mut raw_page_digests = vec![0u8; header.page_number() * 32];
123 file.read_exact(&mut raw_page_digests)
124 .map_err(|e| format!("Cannot load page digests: {:?}", e))?;
125 let file_checksum = keccak(&raw_page_digests);
126 if file_checksum != conf.checksum {
127 return Err("Incorrect checksum".into());
128 }
129
130 let page_digests = raw_page_digests
131 .chunks_exact(32)
132 .map(H256::from_slice)
133 .collect();
134
135 Ok(Self {
136 file_path: conf.file_path.clone(),
137 active: AtomicBool::new(true),
138 header,
139 pages: RwLock::new(LruCache::with_capacity(5)),
140 page_digests,
141 })
142 }
143
144 /// Validates if switching to a target branch is allowed based on pivot hint
145 /// records.
146 ///
147 /// # Arguments
148 /// * `fork_at` - First block height where the current chain and target
149 /// branch diverge
150 /// * `me_height` - Last block height of the target branch
151 /// * `ancestor_hash_at` - Callback to retrieve block hash at specified
152 /// height on target branch
153 ///
154 /// # Returns
155 /// Returns whether switching to the fork branch is allowed.
156 pub fn allow_switch(
157 &self, fork_at: u64, me_height: u64,
158 ancestor_hash_at: impl FnOnce(u64) -> H256,
159 ) -> bool {
160 if !self.active.load(Ordering::Acquire) {
161 return true;
162 }
163
164 if fork_at >= self.header.range_max {
165 return true;
166 }
167
168 let check_height = if let Some(check_height) =
169 self.header.compute_check_height(fork_at, me_height)
170 {
171 check_height
172 } else {
173 return true;
174 };
175
176 let actual_hash = ancestor_hash_at(check_height);
177 let result = self.check_hash(check_height, actual_hash);
178 debug!("Pivot hint check switch result {result}. fork_at: {fork_at}, me_height: {me_height}, check_height: {check_height}, fetch_hash: {actual_hash:?}");
179 result
180 }
181
182 pub fn allow_extend(&self, height: u64, hash: H256) -> bool {
183 if !self.active.load(Ordering::Acquire) {
184 return true;
185 }
186
187 if height >= self.header.range_max {
188 return true;
189 }
190
191 if height % self.header.minor_interval != 0 {
192 return true;
193 }
194
195 let page_number = height / self.header.page_interval;
196 let page_offset = height % self.header.page_interval;
197
198 let result = self.check_with_page(page_number, |page| {
199 page.check_hash_at_height(page_offset, hash)
200 });
201 debug!("Pivot hint check extend result {result}. me_height: {height}, fetch_hash: {hash:?}");
202 result
203 }
204
205 pub fn is_active(&self) -> bool { self.active.load(Ordering::Acquire) }
206
207 fn check_hash(&self, height: u64, hash: H256) -> bool {
208 let page_number = height / self.header.page_interval;
209 let page_offset = height % self.header.page_interval;
210
211 self.check_with_page(page_number, |page| {
212 page.check_hash_at_height(page_offset, hash)
213 })
214 }
215
216 fn check_with_page(
217 &self, page_number: u64, check: impl Fn(&PivotHintPage) -> bool,
218 ) -> bool {
219 let mut guard = self.pages.write();
220 if let Some(page) = guard.get(&page_number) {
221 check(page)
222 } else {
223 info!("Loading pivot hint page {}", page_number);
224 let page = match self.load_page(page_number) {
225 Ok(page) => page,
226 Err(e) => {
227 warn!(
228 "Failed to load pivot hint page {}, pivot hint check disabled: {}",
229 page_number, e
230 );
231 self.active.store(false, Ordering::Release);
232 return true;
233 }
234 };
235 let result = check(&page);
236 guard.insert(page_number, page);
237 result
238 }
239 }
240
241 fn load_page(&self, page_number: u64) -> Result<PivotHintPage, String> {
242 let page_bytes = self.header.page_bytes();
243 let start_pos = HEADER_LENGTH as u64
244 + self.page_digests.len() as u64 * 32
245 + page_number * page_bytes as u64;
246
247 let mut file = File::open(&self.file_path)
248 .map_err(|e| format!("Cannot open pivot hint file: {:?}", e))?;
249
250 file.seek(SeekFrom::Start(start_pos))
251 .map_err(|e| format!("Cannot seek to start position: {:?}", e))?;
252
253 let mut page_content = vec![0u8; page_bytes];
254 file.read_exact(&mut page_content[..])
255 .map_err(|e| format!("Cannot load the page: {:?}", e))?;
256
257 let expected_page_checksum =
258 if let Some(hash) = self.page_digests.get(page_number as usize) {
259 hash
260 } else {
261 return Err("Empty page checksum".into());
262 };
263
264 let actual_page_checksum = keccak(&page_content);
265 if expected_page_checksum != &actual_page_checksum {
266 return Err("Incorrect checksum".into());
267 }
268 Ok(PivotHintPage::new(page_content, self.header))
269 }
270}