Skip to main content

pepper_sync/
error.rs

1//! Pepper sync error module
2
3use std::{array::TryFromSliceError, convert::Infallible};
4
5use shardtree::error::ShardTreeError;
6use zcash_primitives::{block::BlockHash, transaction::TxId};
7use zcash_protocol::consensus::BlockHeight;
8use zcash_protocol::{PoolType, ShieldedProtocol};
9
10use crate::wallet::OutputId;
11
12/// Top level error enumerating any error that may occur during sync
13#[derive(Debug, thiserror::Error)]
14pub enum SyncError<E>
15where
16    E: std::fmt::Debug + std::fmt::Display,
17{
18    /// Mempool error.
19    #[error("mempool error. {0}")]
20    MempoolError(#[from] MempoolError),
21    /// Scan error.
22    #[error("scan error. {0}")]
23    ScanError(#[from] ScanError),
24    /// Server error.
25    #[error("server error. {0}")]
26    ServerError(#[from] ServerError),
27    /// Sync mode error.
28    #[error("sync mode error. {0}")]
29    SyncModeError(#[from] SyncModeError),
30    /// Chain error.
31    #[error("wallet height {0} is more than {1} blocks ahead of best chain height {2}")]
32    ChainError(u32, u32, u32),
33    /// Birthday below sapling error.
34    #[error(
35        "birthday {0} below sapling activation height {1}. pre-sapling wallets are not supported!"
36    )]
37    BirthdayBelowSapling(u32, u32),
38    /// Shard tree error.
39    #[error("shard tree error. {0}")]
40    ShardTreeError(#[from] ShardTreeError<Infallible>),
41    /// Critical non-recoverable truncation error due to missing shard tree checkpoints.
42    #[error(
43        "critical non-recoverable truncation error at height {0} due to missing {1} shard tree checkpoints. wallet data cleared. rescan required."
44    )]
45    TruncationError(BlockHeight, PoolType),
46    /// Transparent address derivation error.
47    #[error("transparent address derivation error. {0}")]
48    TransparentAddressDerivationError(bip32::Error),
49    /// Wallet error.
50    #[error("wallet error. {0}")]
51    WalletError(E),
52}
53
54impl<E: std::fmt::Debug + std::fmt::Display> SyncError<E> {
55    /// Returns `true` if this error is likely transient and retrying sync
56    /// (possibly against a different server) may succeed.
57    ///
58    /// Server errors from failed gRPC requests and mempool stream failures
59    /// are recommend_same_server. Configuration errors, wallet corruption, and data
60    /// integrity failures are not.
61    pub fn recommend_same_server(&self) -> bool {
62        match self {
63            // Network/server issues — retry may help, especially with a different server.
64            SyncError::ServerError(e) => e.recommend_same_server(),
65            SyncError::MempoolError(_) => true,
66
67            // Local or configuration errors — retrying won't help.
68            SyncError::ScanError(_)
69            | SyncError::SyncModeError(_)
70            | SyncError::ChainError(..)
71            | SyncError::BirthdayBelowSapling(..)
72            | SyncError::ShardTreeError(_)
73            | SyncError::TruncationError(..)
74            | SyncError::TransparentAddressDerivationError(_)
75            | SyncError::WalletError(_) => false,
76        }
77    }
78}
79
80impl ServerError {
81    /// Returns `true` if this server error is likely transient.
82    ///
83    /// gRPC request failures (timeouts, connection drops) are recommend_same_server.
84    /// Invalid data from the server suggests a bad server that should be
85    /// avoided rather than retried.
86    pub fn recommend_same_server(&self) -> bool {
87        match self {
88            // Internal channel issue — retry may help after restart.
89            ServerError::FetcherDropped => true,
90
91            // gRPC request failure — the server may be down or overloaded.
92            // Switch to a different server rather than retrying the same one.
93            ServerError::RequestFailed(_) => false,
94
95            // Bad data from server — retrying the same server won't help.
96            ServerError::InvalidFrontier(_)
97            | ServerError::InvalidTransaction(_)
98            | ServerError::InvalidSubtreeRoot
99            | ServerError::ChainVerificationError
100            | ServerError::GenesisBlockOnly => false,
101        }
102    }
103}
104
105/// Recommended action when sync fails.
106///
107/// Returned by [`SyncError::recovery_recommendation`] to give callers (zingo-cli,
108/// zingo-mobile, etc.) a concrete decision without needing to match on
109/// error internals.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum SyncRecoveryObservables {
112    /// The error is transient (e.g. timeout, connection drop).
113    /// Retrying sync with the same server may succeed.
114    MaybeRecoverableServer,
115    /// The server returned invalid or unverifiable data.
116    /// A different server should be tried if available.
117    ServerUnavailable,
118    /// The error is not recoverable by retrying or switching servers.
119    /// User intervention is required (e.g. rescan, fix config).
120    Abort,
121}
122
123impl<E: std::fmt::Debug + std::fmt::Display> SyncError<E> {
124    /// Returns the recommended recovery action for this error.
125    ///
126    /// This is the primary entry point for callers that need to decide
127    /// whether to retry, switch servers, or give up.
128    pub fn recovery_recommendation(&self) -> SyncRecoveryObservables {
129        match self {
130            SyncError::ServerError(e) => e.recovery_recommendation(),
131            SyncError::MempoolError(_) => SyncRecoveryObservables::MaybeRecoverableServer,
132
133            SyncError::ScanError(ScanError::ServerError(e)) => e.recovery_recommendation(),
134            SyncError::ScanError(_) => SyncRecoveryObservables::Abort,
135
136            SyncError::SyncModeError(_)
137            | SyncError::ChainError(..)
138            | SyncError::BirthdayBelowSapling(..)
139            | SyncError::ShardTreeError(_)
140            | SyncError::TruncationError(..)
141            | SyncError::TransparentAddressDerivationError(_)
142            | SyncError::WalletError(_) => SyncRecoveryObservables::Abort,
143        }
144    }
145}
146
147impl ServerError {
148    /// Returns the recommended recovery action for this server error.
149    pub fn recovery_recommendation(&self) -> SyncRecoveryObservables {
150        match self {
151            // Internal channel issue — same server may work after restart.
152            ServerError::FetcherDropped => SyncRecoveryObservables::MaybeRecoverableServer,
153            // gRPC request failure or bad data — try a different server.
154            ServerError::RequestFailed(_)
155            | ServerError::InvalidFrontier(_)
156            | ServerError::InvalidTransaction(_)
157            | ServerError::InvalidSubtreeRoot
158            | ServerError::ChainVerificationError => SyncRecoveryObservables::ServerUnavailable,
159            // Empty chain — no point retrying anywhere.
160            ServerError::GenesisBlockOnly => SyncRecoveryObservables::Abort,
161        }
162    }
163}
164
165/// Sync status errors.
166#[derive(Debug, thiserror::Error)]
167pub enum SyncStatusError<E>
168where
169    E: std::fmt::Debug + std::fmt::Display,
170{
171    /// No sync data. Wallet has never been synced with the block chain.
172    #[error("No sync data. Wallet has never been synced with the block chain.")]
173    NoSyncData,
174    /// Wallet error.
175    #[error("wallet error. {0}")]
176    WalletError(E),
177}
178
179/// Mempool errors.
180#[derive(Debug, thiserror::Error)]
181pub enum MempoolError {
182    /// Server error.
183    #[error("server error. {0}")]
184    ServerError(#[from] ServerError),
185    /// Timed out fetching mempool stream during shutdown.
186    #[error(
187        "timed out fetching mempool stream during shutdown.\nNON-CRITICAL: sync completed successfully but may not have scanned transactions in the mempool."
188    )]
189    ShutdownWithoutStream,
190}
191
192/// Scan errors.
193#[derive(Debug, thiserror::Error)]
194pub enum ScanError {
195    /// Server error.
196    #[error("server error. {0}")]
197    ServerError(#[from] ServerError),
198    /// Continuity error.
199    #[error("continuity error. {0}")]
200    ContinuityError(#[from] ContinuityError),
201    /// Zcash client backend scan error
202    #[error("{0}")]
203    EncodingError(#[from] EncodingInvalid),
204    /// Invalid sapling nullifier
205    #[error("invalid sapling nullifier. {0}")]
206    InvalidSaplingNullifier(#[from] TryFromSliceError),
207    /// Invalid orchard nullifier length
208    #[error("invalid orchard nullifier length. should be 32 bytes, found {0}")]
209    InvalidOrchardNullifierLength(usize),
210    /// Invalid orchard nullifier
211    #[error("invalid orchard nullifier")]
212    InvalidOrchardNullifier,
213    /// Invalid sapling output
214    // TODO: add output data
215    #[error("invalid sapling output")]
216    InvalidSaplingOutput,
217    /// Invalid orchard action
218    // TODO: add output data
219    #[error("invalid orchard action")]
220    InvalidOrchardAction,
221    /// Incorrect tree size
222    #[error(
223        "incorrect tree size. {shielded_protocol} tree size recorded in block metadata {block_metadata_size} does not match calculated size {calculated_size}"
224    )]
225    IncorrectTreeSize {
226        /// Shielded protocol
227        shielded_protocol: PoolType,
228        /// Block metadata size
229        block_metadata_size: u32,
230        /// Calculated size
231        calculated_size: u32,
232    },
233    /// Txid of transaction returned by the server does not match requested txid.
234    #[error(
235        "txid of transaction returned by the server does not match requested txid.\ntxid requested: {txid_requested}\ntxid returned: {txid_returned}"
236    )]
237    IncorrectTxid {
238        /// Txid requested
239        txid_requested: TxId,
240        /// Txid returned
241        txid_returned: TxId,
242    },
243    /// Decrypted note nullifier and position data not found.
244    #[error("decrypted note nullifier and position data not found. output id: {0:?}")]
245    DecryptedNoteDataNotFound(OutputId),
246    /// Invalid memo bytes..
247    #[error("invalid memo bytes. {0}")]
248    InvalidMemoBytes(#[from] zcash_protocol::memo::Error),
249    /// Failed to parse encoded address.
250    #[error("failed to parse encoded address. {0}")]
251    AddressParseError(#[from] zcash_address::unified::ParseError),
252}
253
254/// The encoding of a compact Sapling output or compact Orchard action was invalid.
255#[derive(Debug, thiserror::Error)]
256#[error("{pool_type:?} output {index} of transaction {txid} was improperly encoded.")]
257pub struct EncodingInvalid {
258    pub(crate) at_height: BlockHeight,
259    pub(crate) txid: TxId,
260    pub(crate) pool_type: ShieldedProtocol,
261    pub(crate) index: usize,
262    pub(crate) error: CompactFormatError,
263}
264
265/// An error indicating that a field of a compact format structure could not be parsed.
266#[derive(Clone, Debug)]
267pub enum CompactFormatError {
268    /// A byte slice had an invalid length for the expected field.
269    InvalidLength(std::array::TryFromSliceError),
270    /// A field value did not represent a valid protocol element.
271    InvalidValue,
272}
273
274impl std::fmt::Display for CompactFormatError {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        match self {
277            CompactFormatError::InvalidLength(e) => write!(f, "Invalid compact format field: {e}"),
278            CompactFormatError::InvalidValue => {
279                write!(f, "Compact format field is not a valid protocol element")
280            }
281        }
282    }
283}
284
285/// Block continuity errors.
286#[derive(Debug, thiserror::Error)]
287pub enum ContinuityError {
288    /// Height discontinuity.
289    #[error(
290        "height discontinuity. block with height {height} is not continuous with previous block height {previous_block_height}"
291    )]
292    HeightDiscontinuity {
293        /// Block height
294        height: BlockHeight,
295        /// Previous block height
296        previous_block_height: BlockHeight,
297    },
298    /// Hash discontinuity.
299    #[error(
300        "hash discontinuity. block prev_hash {prev_hash} with height {height} does not match previous block hash {previous_block_hash}"
301    )]
302    HashDiscontinuity {
303        /// Block height
304        height: BlockHeight,
305        /// Block's previous block hash data
306        prev_hash: BlockHash,
307        /// Actual previous block hash
308        previous_block_hash: BlockHash,
309    },
310}
311
312/// Server errors.
313///
314/// Errors associated with connecting to the server and receiving invalid data.
315#[derive(Debug, thiserror::Error)]
316pub enum ServerError {
317    /// Server request failed.
318    #[error("server request failed. {0}")]
319    RequestFailed(#[from] tonic::Status),
320    /// Server returned invalid frontier.
321    #[error("server returned invalid frontier. {0}")]
322    InvalidFrontier(std::io::Error),
323    /// Server returned invalid transaction.
324    #[error("server returned invalid transaction. {0}")]
325    InvalidTransaction(std::io::Error),
326    /// Server returned invalid subtree root.
327    // TODO: add more info
328    #[error("server returned invalid subtree root.")]
329    InvalidSubtreeRoot,
330    /// Server returned blocks that could not be verified against wallet block data. Exceeded max verification window.
331    #[error(
332        "server returned blocks that could not be verified against wallet block data. exceeded max verification window. wallet data has been cleared as shard tree data cannot be truncated further. wallet rescan required."
333    )]
334    ChainVerificationError,
335    /// Fetcher task was dropped.
336    #[error("fetcher task was dropped.")]
337    FetcherDropped,
338    /// Server reports only the genesis block exists.
339    #[error("server reports only the genesis block exists.")]
340    GenesisBlockOnly,
341}
342
343/// Sync mode error.
344#[derive(Debug, thiserror::Error)]
345pub enum SyncModeError {
346    /// Invalid sync mode.
347    #[error("invalid sync mode. {0}")]
348    InvalidSyncMode(u8),
349    /// Sync is already running.
350    #[error("sync is already running")]
351    SyncAlreadyRunning,
352    /// Sync is not running.
353    #[error("sync is not running")]
354    SyncNotRunning,
355    /// Sync is not paused.
356    #[error("sync is not paused")]
357    SyncNotPaused,
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    /// Use `String` as the wallet error type for testing.
365    type TestSyncError = SyncError<String>;
366
367    mod recommend_same_server {
368        use super::*;
369
370        mod server_error {
371            use super::*;
372
373            #[test]
374            fn fetcher_dropped() {
375                assert!(ServerError::FetcherDropped.recommend_same_server());
376            }
377        }
378
379        mod sync_error {
380            use super::*;
381
382            #[test]
383            fn mempool_error() {
384                let e: TestSyncError = MempoolError::ShutdownWithoutStream.into();
385                assert!(e.recommend_same_server());
386            }
387        }
388    }
389
390    mod recommend_change_server {
391        use super::*;
392
393        mod server_error {
394            use super::*;
395
396            #[test]
397            fn request_failed() {
398                let e = ServerError::RequestFailed(tonic::Status::deadline_exceeded("timeout"));
399                assert!(!e.recommend_same_server());
400            }
401
402            #[test]
403            fn invalid_frontier() {
404                let e = ServerError::InvalidFrontier(std::io::Error::other("bad frontier"));
405                assert!(!e.recommend_same_server());
406            }
407
408            #[test]
409            fn invalid_transaction() {
410                let e = ServerError::InvalidTransaction(std::io::Error::other("bad tx"));
411                assert!(!e.recommend_same_server());
412            }
413
414            #[test]
415            fn invalid_subtree_root() {
416                assert!(!ServerError::InvalidSubtreeRoot.recommend_same_server());
417            }
418
419            #[test]
420            fn chain_verification_error() {
421                assert!(!ServerError::ChainVerificationError.recommend_same_server());
422            }
423
424            #[test]
425            fn genesis_block_only() {
426                assert!(!ServerError::GenesisBlockOnly.recommend_same_server());
427            }
428        }
429
430        mod sync_error {
431            use super::*;
432
433            #[test]
434            fn server_request_failed() {
435                let e: TestSyncError =
436                    ServerError::RequestFailed(tonic::Status::deadline_exceeded("timeout")).into();
437                assert!(!e.recommend_same_server());
438            }
439
440            #[test]
441            fn sync_mode_error() {
442                let e: TestSyncError = SyncModeError::SyncAlreadyRunning.into();
443                assert!(!e.recommend_same_server());
444            }
445
446            #[test]
447            fn chain_error() {
448                let e: TestSyncError = SyncError::ChainError(100, 50, 50);
449                assert!(!e.recommend_same_server());
450            }
451
452            #[test]
453            fn birthday_below_sapling() {
454                let e: TestSyncError = SyncError::BirthdayBelowSapling(100, 419200);
455                assert!(!e.recommend_same_server());
456            }
457
458            #[test]
459            fn wallet_error() {
460                let e: TestSyncError = SyncError::WalletError("db locked".to_string());
461                assert!(!e.recommend_same_server());
462            }
463        }
464    }
465
466    mod recovery_recommendation {
467        use super::*;
468
469        mod retry_same_server {
470            use super::*;
471
472            #[test]
473            fn fetcher_dropped() {
474                assert_eq!(
475                    ServerError::FetcherDropped.recovery_recommendation(),
476                    SyncRecoveryObservables::MaybeRecoverableServer
477                );
478            }
479
480            #[test]
481            fn mempool_error() {
482                let e: TestSyncError = MempoolError::ShutdownWithoutStream.into();
483                assert_eq!(
484                    e.recovery_recommendation(),
485                    SyncRecoveryObservables::MaybeRecoverableServer
486                );
487            }
488        }
489
490        mod try_different_server {
491            use super::*;
492
493            #[test]
494            fn request_failed() {
495                let e = ServerError::RequestFailed(tonic::Status::deadline_exceeded("timeout"));
496                assert_eq!(
497                    e.recovery_recommendation(),
498                    SyncRecoveryObservables::ServerUnavailable
499                );
500            }
501
502            #[test]
503            fn sync_error_from_request_failed() {
504                let e: TestSyncError =
505                    ServerError::RequestFailed(tonic::Status::unavailable("down")).into();
506                assert_eq!(
507                    e.recovery_recommendation(),
508                    SyncRecoveryObservables::ServerUnavailable
509                );
510            }
511
512            #[test]
513            fn invalid_frontier() {
514                let e = ServerError::InvalidFrontier(std::io::Error::other("bad"));
515                assert_eq!(
516                    e.recovery_recommendation(),
517                    SyncRecoveryObservables::ServerUnavailable
518                );
519            }
520
521            #[test]
522            fn invalid_transaction() {
523                let e = ServerError::InvalidTransaction(std::io::Error::other("bad"));
524                assert_eq!(
525                    e.recovery_recommendation(),
526                    SyncRecoveryObservables::ServerUnavailable
527                );
528            }
529
530            #[test]
531            fn invalid_subtree_root() {
532                assert_eq!(
533                    ServerError::InvalidSubtreeRoot.recovery_recommendation(),
534                    SyncRecoveryObservables::ServerUnavailable
535                );
536            }
537
538            #[test]
539            fn chain_verification_error() {
540                assert_eq!(
541                    ServerError::ChainVerificationError.recovery_recommendation(),
542                    SyncRecoveryObservables::ServerUnavailable
543                );
544            }
545
546            #[test]
547            fn sync_error_from_invalid_frontier() {
548                let e: TestSyncError =
549                    ServerError::InvalidFrontier(std::io::Error::other("bad")).into();
550                assert_eq!(
551                    e.recovery_recommendation(),
552                    SyncRecoveryObservables::ServerUnavailable
553                );
554            }
555
556            #[test]
557            fn scan_error_wrapping_server_error() {
558                let e: TestSyncError =
559                    ScanError::ServerError(ServerError::InvalidSubtreeRoot).into();
560                assert_eq!(
561                    e.recovery_recommendation(),
562                    SyncRecoveryObservables::ServerUnavailable
563                );
564            }
565        }
566
567        mod abort {
568            use super::*;
569
570            #[test]
571            fn genesis_block_only() {
572                assert_eq!(
573                    ServerError::GenesisBlockOnly.recovery_recommendation(),
574                    SyncRecoveryObservables::Abort
575                );
576            }
577
578            #[test]
579            fn sync_mode_error() {
580                let e: TestSyncError = SyncModeError::SyncAlreadyRunning.into();
581                assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
582            }
583
584            #[test]
585            fn chain_error() {
586                let e: TestSyncError = SyncError::ChainError(100, 50, 50);
587                assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
588            }
589
590            #[test]
591            fn wallet_error() {
592                let e: TestSyncError = SyncError::WalletError("db locked".to_string());
593                assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
594            }
595        }
596    }
597}