1use 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#[derive(Debug, thiserror::Error)]
14pub enum SyncError<E>
15where
16 E: std::fmt::Debug + std::fmt::Display,
17{
18 #[error("mempool error. {0}")]
20 MempoolError(#[from] MempoolError),
21 #[error("scan error. {0}")]
23 ScanError(#[from] ScanError),
24 #[error("server error. {0}")]
26 ServerError(#[from] ServerError),
27 #[error("sync mode error. {0}")]
29 SyncModeError(#[from] SyncModeError),
30 #[error("wallet height {0} is more than {1} blocks ahead of best chain height {2}")]
32 ChainError(u32, u32, u32),
33 #[error(
35 "birthday {0} below sapling activation height {1}. pre-sapling wallets are not supported!"
36 )]
37 BirthdayBelowSapling(u32, u32),
38 #[error("shard tree error. {0}")]
40 ShardTreeError(#[from] ShardTreeError<Infallible>),
41 #[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 #[error("transparent address derivation error. {0}")]
48 TransparentAddressDerivationError(bip32::Error),
49 #[error("wallet error. {0}")]
51 WalletError(E),
52}
53
54impl<E: std::fmt::Debug + std::fmt::Display> SyncError<E> {
55 pub fn recommend_same_server(&self) -> bool {
62 match self {
63 SyncError::ServerError(e) => e.recommend_same_server(),
65 SyncError::MempoolError(_) => true,
66
67 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 pub fn recommend_same_server(&self) -> bool {
87 match self {
88 ServerError::FetcherDropped => true,
90
91 ServerError::RequestFailed(_) => false,
94
95 ServerError::InvalidFrontier(_)
97 | ServerError::InvalidTransaction(_)
98 | ServerError::InvalidSubtreeRoot
99 | ServerError::ChainVerificationError
100 | ServerError::GenesisBlockOnly => false,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum SyncRecoveryObservables {
112 MaybeRecoverableServer,
115 ServerUnavailable,
118 Abort,
121}
122
123impl<E: std::fmt::Debug + std::fmt::Display> SyncError<E> {
124 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 pub fn recovery_recommendation(&self) -> SyncRecoveryObservables {
150 match self {
151 ServerError::FetcherDropped => SyncRecoveryObservables::MaybeRecoverableServer,
153 ServerError::RequestFailed(_)
155 | ServerError::InvalidFrontier(_)
156 | ServerError::InvalidTransaction(_)
157 | ServerError::InvalidSubtreeRoot
158 | ServerError::ChainVerificationError => SyncRecoveryObservables::ServerUnavailable,
159 ServerError::GenesisBlockOnly => SyncRecoveryObservables::Abort,
161 }
162 }
163}
164
165#[derive(Debug, thiserror::Error)]
167pub enum SyncStatusError<E>
168where
169 E: std::fmt::Debug + std::fmt::Display,
170{
171 #[error("No sync data. Wallet has never been synced with the block chain.")]
173 NoSyncData,
174 #[error("wallet error. {0}")]
176 WalletError(E),
177}
178
179#[derive(Debug, thiserror::Error)]
181pub enum MempoolError {
182 #[error("server error. {0}")]
184 ServerError(#[from] ServerError),
185 #[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#[derive(Debug, thiserror::Error)]
194pub enum ScanError {
195 #[error("server error. {0}")]
197 ServerError(#[from] ServerError),
198 #[error("continuity error. {0}")]
200 ContinuityError(#[from] ContinuityError),
201 #[error("{0}")]
203 EncodingError(#[from] EncodingInvalid),
204 #[error("invalid sapling nullifier. {0}")]
206 InvalidSaplingNullifier(#[from] TryFromSliceError),
207 #[error("invalid orchard nullifier length. should be 32 bytes, found {0}")]
209 InvalidOrchardNullifierLength(usize),
210 #[error("invalid orchard nullifier")]
212 InvalidOrchardNullifier,
213 #[error("invalid sapling output")]
216 InvalidSaplingOutput,
217 #[error("invalid orchard action")]
220 InvalidOrchardAction,
221 #[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: PoolType,
228 block_metadata_size: u32,
230 calculated_size: u32,
232 },
233 #[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: TxId,
240 txid_returned: TxId,
242 },
243 #[error("decrypted note nullifier and position data not found. output id: {0:?}")]
245 DecryptedNoteDataNotFound(OutputId),
246 #[error("invalid memo bytes. {0}")]
248 InvalidMemoBytes(#[from] zcash_protocol::memo::Error),
249 #[error("failed to parse encoded address. {0}")]
251 AddressParseError(#[from] zcash_address::unified::ParseError),
252}
253
254#[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#[derive(Clone, Debug)]
267pub enum CompactFormatError {
268 InvalidLength(std::array::TryFromSliceError),
270 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#[derive(Debug, thiserror::Error)]
287pub enum ContinuityError {
288 #[error(
290 "height discontinuity. block with height {height} is not continuous with previous block height {previous_block_height}"
291 )]
292 HeightDiscontinuity {
293 height: BlockHeight,
295 previous_block_height: BlockHeight,
297 },
298 #[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 height: BlockHeight,
305 prev_hash: BlockHash,
307 previous_block_hash: BlockHash,
309 },
310}
311
312#[derive(Debug, thiserror::Error)]
316pub enum ServerError {
317 #[error("server request failed. {0}")]
319 RequestFailed(#[from] tonic::Status),
320 #[error("server returned invalid frontier. {0}")]
322 InvalidFrontier(std::io::Error),
323 #[error("server returned invalid transaction. {0}")]
325 InvalidTransaction(std::io::Error),
326 #[error("server returned invalid subtree root.")]
329 InvalidSubtreeRoot,
330 #[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 #[error("fetcher task was dropped.")]
337 FetcherDropped,
338 #[error("server reports only the genesis block exists.")]
340 GenesisBlockOnly,
341}
342
343#[derive(Debug, thiserror::Error)]
345pub enum SyncModeError {
346 #[error("invalid sync mode. {0}")]
348 InvalidSyncMode(u8),
349 #[error("sync is already running")]
351 SyncAlreadyRunning,
352 #[error("sync is not running")]
354 SyncNotRunning,
355 #[error("sync is not paused")]
357 SyncNotPaused,
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 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}