Skip to main content

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}