reth_snap_sync/error.rs
1//! Failures raised while assembling a snap state generation.
2
3use alloy_primitives::B256;
4use reth_downloaders::snap::{
5 InvalidAccountRange, InvalidBlockAccessListRequest, InvalidStorageRangeRequest,
6};
7use reth_network_p2p::error::RequestError;
8use reth_storage_api::SnapAttemptId;
9use reth_storage_errors::{db::DatabaseError, provider::ProviderError};
10
11/// Error returned while assembling a snap state generation.
12#[derive(Debug, thiserror::Error)]
13pub enum SnapSyncError {
14 /// A header lookup failed.
15 #[error(transparent)]
16 Provider(#[from] ProviderError),
17 /// A request for state failed.
18 #[error(transparent)]
19 Request(#[from] RequestError),
20 /// An account range was requested with its origin past its limit.
21 #[error(transparent)]
22 AccountRangeRequest(#[from] InvalidAccountRange),
23 /// A storage request did not match the accounts it was built from.
24 #[error(transparent)]
25 StorageRequest(#[from] InvalidStorageRangeRequest),
26 /// A block access list request did not match the headers it was built from.
27 #[error(transparent)]
28 BlockAccessListRequest(#[from] InvalidBlockAccessListRequest),
29 /// The storage layout keys state by address, which snap cannot fill in without preimages.
30 #[error("snap synchronization requires the hashed state layout")]
31 UnsupportedStorage,
32 /// No attempt owns the persisted state.
33 #[error("no snap attempt owns the persisted state")]
34 NoAttempt,
35 /// A write was presented for an attempt or pivot that no longer owns the persisted state.
36 #[error("stale snap write for attempt {attempt} at state version {state_version}")]
37 StaleWrite {
38 /// Attempt the rejected write claims.
39 attempt: SnapAttemptId,
40 /// State version the rejected write was proved against.
41 state_version: u64,
42 },
43 /// A range was proved against a root other than the one the attempt downloads.
44 #[error("range proved against {got} while the attempt downloads {expected}")]
45 RootMismatch {
46 /// Root the attempt currently downloads.
47 expected: B256,
48 /// Root the range was proved against.
49 got: B256,
50 },
51 /// A generation carries a state root other than the one its target header commits to.
52 #[error("block {block} commits to state root {expected}, not {got}")]
53 HeaderRootMismatch {
54 /// Target block of the generation.
55 block: u64,
56 /// State root of the canonical header.
57 expected: B256,
58 /// State root the generation carries.
59 got: B256,
60 },
61 /// No catch-up progress is recorded for the attempt.
62 #[error("no catch-up progress is recorded for the attempt")]
63 NoCatchUpProgress,
64 /// A block the catch-up needs has no header.
65 #[error("block {block} has no header to authenticate its access list against")]
66 MissingHeader {
67 /// Block the header is missing for.
68 block: u64,
69 },
70 /// A list was applied for a block other than the one continuing the applied sequence.
71 #[error("block access list for block {got}, the applied state continues at {expected}")]
72 OutOfOrderBlock {
73 /// Block the applied state continues at.
74 expected: u64,
75 /// Block the list was applied for.
76 got: u64,
77 },
78 /// A list was applied for a block past the pivot.
79 #[error("block access list for block {block} past pivot {pivot}")]
80 BlockPastPivot {
81 /// Block the attempt is anchored to.
82 pivot: u64,
83 /// Block the list was applied for.
84 block: u64,
85 },
86 /// The pivot was moved to a block not past the current one.
87 #[error("pivot {pivot} cannot move to block {target}, which is not past it")]
88 PivotNotAdvanced {
89 /// Block the attempt is anchored to.
90 pivot: u64,
91 /// Block the pivot was moved to.
92 target: u64,
93 },
94 /// Catch-up has not carried the downloaded state to the pivot, which committing storage
95 /// persisted ahead of its range or completing the attempt requires.
96 #[error("catch-up applied block {applied}, the pivot is {pivot}")]
97 CatchUpBehindPivot {
98 /// Last block whose list is applied.
99 applied: u64,
100 /// Block the attempt is anchored to.
101 pivot: u64,
102 },
103 /// A list was applied for a block the canonical chain no longer holds.
104 #[error("block {block} ({hash}) is no longer canonical")]
105 NonCanonicalBlock {
106 /// Number of the block.
107 block: u64,
108 /// Hash of the block the list belongs to.
109 hash: B256,
110 },
111 /// A list was applied for a block building on another chain than the applied state.
112 #[error("block access list for a block building on {got}, the applied state is at {expected}")]
113 ForkedBlock {
114 /// Hash of the last applied block.
115 expected: B256,
116 /// Hash the block the list belongs to builds on.
117 got: B256,
118 },
119 /// No account coverage is recorded for the attempt.
120 #[error("no account coverage is recorded for the attempt")]
121 NoCoverage,
122 /// A persisted progress record this build cannot read.
123 #[error("{key} record version {version:?} is not supported")]
124 UnsupportedRecord {
125 /// Metadata key the record is stored under.
126 key: &'static str,
127 /// Version found on disk, absent when the record carries no numeric version.
128 version: Option<u64>,
129 },
130 /// A persisted progress record that cannot be decoded.
131 #[error("{key} record is unreadable: {source}")]
132 UnreadableRecord {
133 /// Metadata key the record is stored under.
134 key: &'static str,
135 /// Why the record could not be decoded.
136 source: serde_json::Error,
137 },
138 /// A blocking task panicked or the runtime shut down before it finished.
139 #[error("blocking task failed: {0}")]
140 BlockingTask(#[source] Box<dyn core::error::Error + Send + Sync>),
141 /// A range was downloaded from somewhere other than the next key to cover.
142 #[error("account range starts at {got}, coverage continues at {expected:?}")]
143 OutOfOrderRange {
144 /// Key the coverage continues at, or none once it is complete.
145 expected: Option<B256>,
146 /// Key the range was requested from.
147 got: B256,
148 },
149 /// A verified range proved nothing past the key it was requested from.
150 #[error("range made no progress past {origin}")]
151 NoProgress {
152 /// Key the range was requested from.
153 origin: B256,
154 },
155 /// Storage was downloaded from somewhere other than where its contract's progress continues.
156 #[error("storage for {account} from {from} does not continue its persisted progress")]
157 OutOfOrderStorage {
158 /// Hashed address of the contract.
159 account: B256,
160 /// Slot the storage was requested from.
161 from: B256,
162 },
163 /// An account with storage was committed without it.
164 #[error("account {account} has storage that was not downloaded")]
165 MissingStorage {
166 /// Hashed address of the account.
167 account: B256,
168 },
169 /// The storage supplied for an account does not hash to the root the account commits to.
170 #[error("storage supplied for {account} hashes to {got}, its account commits to {expected}")]
171 StorageRootMismatch {
172 /// Hashed address of the account.
173 account: B256,
174 /// Storage root the account commits to.
175 expected: B256,
176 /// Root of the supplied storage.
177 got: B256,
178 },
179 /// Storage was supplied for an account that is not in the range or has none.
180 #[error("storage supplied for {account}, which is not a contract in the range")]
181 UnexpectedStorage {
182 /// Hashed address the storage was supplied for.
183 account: B256,
184 },
185 /// An account's code is neither supplied nor stored.
186 #[error("code {hash} is neither supplied nor stored")]
187 MissingCode {
188 /// Hash of the missing code.
189 hash: B256,
190 },
191 /// Code was supplied under a hash it does not hash to.
192 #[error("code supplied as {expected} hashes to {got}")]
193 CodeMismatch {
194 /// Hash the code was supplied under.
195 expected: B256,
196 /// Hash of the supplied code.
197 got: B256,
198 },
199 /// Scheduled accounts remain to be fetched again at the pivot.
200 #[error("{accounts} accounts remain to be fetched again at the pivot")]
201 PendingRepairs {
202 /// Number of accounts still scheduled.
203 accounts: usize,
204 },
205 /// Account ranges remain to be downloaded.
206 #[error("accounts from {next} are not downloaded yet")]
207 IncompleteAccounts {
208 /// Key the next range is requested from.
209 next: B256,
210 },
211 /// The pivot is the genesis block, whose trie the merkle stage never rebuilds.
212 #[error("snap synchronization cannot anchor to the genesis block")]
213 GenesisPivot,
214 /// Work stopped because its session was cancelled.
215 #[error("snap synchronization was cancelled")]
216 Cancelled,
217 /// A reorg was recovered to a pivot below the last block both branches share.
218 #[error("pivot {target} is below reorg ancestor {ancestor}")]
219 PivotBelowAncestor {
220 /// Last block both branches share.
221 ancestor: u64,
222 /// Block the pivot was moved to.
223 target: u64,
224 },
225}
226
227impl SnapSyncError {
228 /// Wraps the failure of a blocking task, which is a panic or a shutting-down runtime.
229 pub(crate) fn blocking_task(error: impl core::error::Error + Send + Sync + 'static) -> Self {
230 Self::BlockingTask(Box::new(error))
231 }
232
233 /// Whether the node's progress resolves this error, as new peers serve the state or missing
234 /// headers are downloaded.
235 ///
236 /// A closed channel means the network is gone, not that peers lack the state.
237 pub const fn is_transient(&self) -> bool {
238 match self {
239 Self::Request(error) => !error.is_channel_closed(),
240 Self::MissingHeader { .. } => true,
241 _ => false,
242 }
243 }
244
245 /// Whether retrying can't help: storage failed, the node's storage layout or on-disk records
246 /// don't support snap, a blocking task failed, or the network is gone.
247 ///
248 /// Exhaustive so that every new variant is classified on purpose.
249 pub const fn is_fatal(&self) -> bool {
250 match self {
251 Self::Provider(_) |
252 Self::UnsupportedStorage |
253 Self::UnsupportedRecord { .. } |
254 Self::UnreadableRecord { .. } |
255 Self::BlockingTask(_) => true,
256 Self::Request(error) => error.is_channel_closed(),
257 Self::AccountRangeRequest(_) |
258 Self::StorageRequest(_) |
259 Self::BlockAccessListRequest(_) |
260 Self::NoAttempt |
261 Self::StaleWrite { .. } |
262 Self::RootMismatch { .. } |
263 Self::HeaderRootMismatch { .. } |
264 Self::NoCatchUpProgress |
265 Self::MissingHeader { .. } |
266 Self::OutOfOrderBlock { .. } |
267 Self::BlockPastPivot { .. } |
268 Self::PivotNotAdvanced { .. } |
269 Self::CatchUpBehindPivot { .. } |
270 Self::NonCanonicalBlock { .. } |
271 Self::ForkedBlock { .. } |
272 Self::NoCoverage |
273 Self::OutOfOrderRange { .. } |
274 Self::NoProgress { .. } |
275 Self::OutOfOrderStorage { .. } |
276 Self::MissingStorage { .. } |
277 Self::StorageRootMismatch { .. } |
278 Self::UnexpectedStorage { .. } |
279 Self::MissingCode { .. } |
280 Self::CodeMismatch { .. } |
281 Self::PendingRepairs { .. } |
282 Self::IncompleteAccounts { .. } |
283 Self::GenesisPivot |
284 Self::Cancelled |
285 Self::PivotBelowAncestor { .. } => false,
286 }
287 }
288
289 /// Whether a block the attempt builds on left the canonical chain, so its downloaded state
290 /// belongs to another fork.
291 pub const fn is_reorg(&self) -> bool {
292 matches!(self, Self::NonCanonicalBlock { .. } | Self::ForkedBlock { .. })
293 }
294}
295
296impl From<DatabaseError> for SnapSyncError {
297 fn from(error: DatabaseError) -> Self {
298 Self::Provider(error.into())
299 }
300}