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, ShieldedPool};
9
10use crate::wallet::OutputId;
11
12pub(crate) const CAUSE_CHAIN_SEPARATOR: &str = ": ";
14
15pub(crate) fn cause_chain_text(error: &(dyn std::error::Error + 'static)) -> String {
18 let mut texts = vec![error.to_string()];
19 let mut link = error.source();
20 while let Some(cause) = link {
21 texts.push(cause.to_string());
22 link = cause.source();
23 }
24 texts.join(CAUSE_CHAIN_SEPARATOR)
25}
26
27#[derive(Debug, thiserror::Error)]
29pub enum SyncError<E>
30where
31 E: std::fmt::Debug + std::fmt::Display,
32{
33 #[error("mempool error")]
35 MempoolError(#[from] MempoolError),
36 #[error("scan error")]
38 ScanError(#[from] ScanError),
39 #[error("server error")]
41 ServerError(#[from] ServerError),
42 #[error("sync mode error")]
44 SyncModeError(#[from] SyncModeError),
45 #[error("sync status error")]
47 SyncStatusError(#[from] SyncStatusError<E>),
48 #[error("wallet height {0} is more than {1} blocks ahead of best chain height {2}")]
50 ChainError(u32, u32, u32),
51 #[error(
53 "birthday {0} below sapling activation height {1}. pre-sapling wallets are not supported!"
54 )]
55 BirthdayBelowSapling(u32, u32),
56 #[error("shard tree error")]
58 ShardTreeError(#[from] ShardTreeError<Infallible>),
59 #[error(
61 "critical non-recoverable truncation error at height {0} due to missing {1} shard tree checkpoints. wallet data cleared. rescan required."
62 )]
63 TruncationError(BlockHeight, PoolType),
64 #[error(
68 "RESCAN TRIGGERED: the wallet's {pool} records were cleared back to height \
69 {rescan_from} and the next sync rescans from there, because at height {disagreed_at} \
70 the wallet's history accounted for a {pool} commitment tree of {calculated_size} where \
71 the chain reports {block_metadata_size}"
72 )]
73 PoolHistoryReopened {
74 pool: PoolType,
76 rescan_from: BlockHeight,
78 disagreed_at: BlockHeight,
80 block_metadata_size: u32,
82 calculated_size: u32,
84 },
85 #[error("transparent address derivation error. {0}")]
87 TransparentAddressDerivationError(bip32::Error),
88 #[error("wallet error. {0}")]
90 WalletError(E),
91}
92
93impl<E: std::fmt::Debug + std::fmt::Display> SyncError<E> {
94 pub fn recommend_same_server(&self) -> bool {
101 match self {
102 SyncError::ServerError(e) => e.recommend_same_server(),
104 SyncError::MempoolError(_) => true,
105 SyncError::PoolHistoryReopened { .. } => true,
109
110 SyncError::ScanError(_)
112 | SyncError::SyncModeError(_)
113 | SyncError::ChainError(..)
114 | SyncError::BirthdayBelowSapling(..)
115 | SyncError::ShardTreeError(_)
116 | SyncError::TruncationError(..)
117 | SyncError::TransparentAddressDerivationError(_)
118 | SyncError::WalletError(_)
119 | SyncError::SyncStatusError(_) => false,
120 }
121 }
122}
123
124impl ServerError {
125 fn request_hit_network_weather(&self) -> bool {
135 let ServerError::RequestFailed(status) = self else {
136 return false;
137 };
138 status_is_network_weather(status)
139 }
140
141 pub fn recommend_same_server(&self) -> bool {
148 match self {
149 ServerError::FetcherDropped => true,
151 ServerError::ChainHeightBelowScanRange { .. } => true,
154
155 ServerError::RequestFailed(_) if self.request_hit_network_weather() => true,
158 ServerError::RequestFailed(_) => false,
161
162 ServerError::InvalidFrontier(_)
164 | ServerError::InvalidTransaction(_)
165 | ServerError::InvalidSubtreeRoot
166 | ServerError::ChainVerificationError
167 | ServerError::GenesisBlockOnly
168 | ServerError::TreeStateNotServed { .. }
169 | ServerError::UnsupportedProtocolVersion { .. } => false,
170 }
171 }
172}
173
174pub(crate) fn status_is_network_weather(status: &tonic::Status) -> bool {
178 let mut link = std::error::Error::source(status);
179 while let Some(cause) = link {
180 if cause.is::<tonic::transport::Error>()
181 || cause.is::<tonic::TimeoutExpired>()
182 || cause.is::<std::io::Error>()
183 {
184 return true;
185 }
186 link = cause.source();
187 }
188 matches!(
189 status.code(),
190 tonic::Code::Unavailable
191 | tonic::Code::DeadlineExceeded
192 | tonic::Code::Cancelled
193 | tonic::Code::ResourceExhausted
194 | tonic::Code::Aborted
195 )
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum SyncRecoveryObservables {
205 MaybeRecoverableServer,
214 ServerUnavailable,
218 Abort,
221}
222
223impl<E: std::fmt::Debug + std::fmt::Display> SyncError<E> {
224 pub fn recovery_recommendation(&self) -> SyncRecoveryObservables {
229 match self {
230 SyncError::ServerError(e) => e.recovery_recommendation(),
231 SyncError::MempoolError(_) => SyncRecoveryObservables::MaybeRecoverableServer,
232
233 SyncError::ScanError(ScanError::ServerError(e)) => e.recovery_recommendation(),
234 SyncError::ScanError(ScanError::TreeSizeNotReported { .. }) => {
237 SyncRecoveryObservables::ServerUnavailable
238 }
239 SyncError::ScanError(ScanError::TransactionNotServed { .. }) => {
242 SyncRecoveryObservables::ServerUnavailable
243 }
244 SyncError::ScanError(_) => SyncRecoveryObservables::Abort,
245
246 SyncError::PoolHistoryReopened { .. } => {
250 SyncRecoveryObservables::MaybeRecoverableServer
251 }
252
253 SyncError::SyncModeError(_)
254 | SyncError::ChainError(..)
255 | SyncError::BirthdayBelowSapling(..)
256 | SyncError::ShardTreeError(_)
257 | SyncError::TruncationError(..)
258 | SyncError::TransparentAddressDerivationError(_)
259 | SyncError::WalletError(_)
260 | SyncError::SyncStatusError(_) => SyncRecoveryObservables::Abort,
261 }
262 }
263}
264
265impl ServerError {
266 pub fn recovery_recommendation(&self) -> SyncRecoveryObservables {
268 match self {
269 ServerError::FetcherDropped => SyncRecoveryObservables::MaybeRecoverableServer,
271 ServerError::ChainHeightBelowScanRange { .. } => {
275 SyncRecoveryObservables::MaybeRecoverableServer
276 }
277 ServerError::RequestFailed(_) if self.request_hit_network_weather() => {
280 SyncRecoveryObservables::MaybeRecoverableServer
281 }
282 ServerError::RequestFailed(_)
285 | ServerError::InvalidFrontier(_)
286 | ServerError::InvalidTransaction(_)
287 | ServerError::InvalidSubtreeRoot
288 | ServerError::ChainVerificationError
289 | ServerError::TreeStateNotServed { .. }
290 | ServerError::UnsupportedProtocolVersion { .. } => {
291 SyncRecoveryObservables::ServerUnavailable
292 }
293 ServerError::GenesisBlockOnly => SyncRecoveryObservables::Abort,
295 }
296 }
297}
298
299#[derive(Debug, thiserror::Error)]
301pub enum SyncStatusError<E>
302where
303 E: std::fmt::Debug + std::fmt::Display,
304{
305 #[error("No sync data. Wallet has never been synced with the block chain.")]
307 NoSyncData,
308 #[error("Sync progress channel is closed. Receiver has been dropped.")]
310 SyncProgressChannelClosed,
311 #[error("wallet error. {0}")]
313 WalletError(E),
314}
315
316#[derive(Debug, thiserror::Error)]
318pub enum MempoolError {
319 #[error("server error")]
321 ServerError(#[from] ServerError),
322 #[error(
324 "timed out fetching mempool stream during shutdown.\nNON-CRITICAL: sync completed successfully but may not have scanned transactions in the mempool."
325 )]
326 ShutdownWithoutStream,
327}
328
329#[derive(Debug, thiserror::Error)]
331pub enum ScanError {
332 #[error("server error")]
334 ServerError(#[from] ServerError),
335 #[error("continuity error")]
337 ContinuityError(#[from] ContinuityError),
338 #[error(transparent)]
340 EncodingError(#[from] EncodingInvalid),
341 #[error("invalid sapling nullifier")]
343 InvalidSaplingNullifier(#[from] TryFromSliceError),
344 #[error("invalid orchard nullifier length. should be 32 bytes, found {0}")]
346 InvalidOrchardNullifierLength(usize),
347 #[error("invalid orchard nullifier")]
349 InvalidOrchardNullifier,
350 #[error("invalid sapling output")]
353 InvalidSaplingOutput,
354 #[error("invalid orchard action")]
357 InvalidOrchardAction,
358 #[error(
360 "incorrect tree size. at height {height}, {shielded_protocol} tree size recorded in block metadata {block_metadata_size} does not match calculated size {calculated_size}"
361 )]
362 IncorrectTreeSize {
363 shielded_protocol: PoolType,
365 height: BlockHeight,
367 block_metadata_size: u32,
369 calculated_size: u32,
371 },
372 #[error(
374 "tree size not reported. at height {height}, {shielded_protocol} tree size recorded in block metadata is zero where the calculated size is {calculated_size}. connect to a server that serves {shielded_protocol}."
375 )]
376 TreeSizeNotReported {
377 shielded_protocol: PoolType,
379 height: BlockHeight,
381 calculated_size: u32,
383 },
384 #[error(
386 "the server does not serve transaction {txid}, which its own data placed at block {block_height}. connect to a different server and sync again. {status}"
387 )]
388 TransactionNotServed {
389 txid: TxId,
391 block_height: BlockHeight,
393 status: tonic::Status,
395 },
396 #[error(
398 "txid of transaction returned by the server does not match requested txid.\ntxid requested: {txid_requested}\ntxid returned: {txid_returned}"
399 )]
400 IncorrectTxid {
401 txid_requested: TxId,
403 txid_returned: TxId,
405 },
406 #[error("decrypted note nullifier and position data not found. output id: {0:?}")]
408 DecryptedNoteDataNotFound(OutputId),
409 #[error("invalid memo bytes")]
411 InvalidMemoBytes(#[from] zcash_protocol::memo::Error),
412 #[error("failed to parse encoded address")]
414 AddressParseError(#[from] zcash_address::unified::ParseError),
415 #[error(
417 "compact transaction contained transparent output with value {0} which is outside the valid zatoshi range"
418 )]
419 TransparentOutputInvalidValue(u64),
420 #[error("all transparent addresses are already in use")]
422 AllAddressesInUse,
423 #[error("transparent address derivation error. {0}")]
425 TransparentAddressDerivationError(bip32::Error),
426}
427
428#[derive(Debug, thiserror::Error)]
430#[error("{pool_type:?} output {index} of transaction {txid} was improperly encoded.")]
431pub struct EncodingInvalid {
432 pub(crate) at_height: BlockHeight,
433 pub(crate) txid: TxId,
434 pub(crate) pool_type: ShieldedPool,
435 pub(crate) index: usize,
436 pub(crate) error: CompactFormatError,
437}
438
439#[derive(Clone, Debug)]
441pub enum CompactFormatError {
442 InvalidLength(std::array::TryFromSliceError),
444 InvalidValue,
446}
447
448impl std::fmt::Display for CompactFormatError {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 match self {
451 CompactFormatError::InvalidLength(e) => write!(f, "Invalid compact format field: {e}"),
452 CompactFormatError::InvalidValue => {
453 write!(f, "Compact format field is not a valid protocol element")
454 }
455 }
456 }
457}
458
459#[derive(Debug, thiserror::Error)]
461pub enum ContinuityError {
462 #[error(
464 "height discontinuity. block with height {height} is not continuous with previous block height {previous_block_height}"
465 )]
466 HeightDiscontinuity {
467 height: BlockHeight,
469 previous_block_height: BlockHeight,
471 },
472 #[error(
474 "hash discontinuity. block prev_hash {prev_hash} with height {height} does not match previous block hash {previous_block_hash}"
475 )]
476 HashDiscontinuity {
477 height: BlockHeight,
479 prev_hash: BlockHash,
481 previous_block_hash: BlockHash,
483 },
484}
485
486#[derive(Debug, thiserror::Error)]
490pub enum ServerError {
491 #[error("server request failed")]
493 RequestFailed(#[from] tonic::Status),
494 #[error("server returned invalid frontier. {0}")]
496 InvalidFrontier(std::io::Error),
497 #[error("server returned invalid transaction. {0}")]
499 InvalidTransaction(std::io::Error),
500 #[error("server returned invalid subtree root.")]
503 InvalidSubtreeRoot,
504 #[error(
506 "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."
507 )]
508 ChainVerificationError,
509 #[error("fetcher task was dropped.")]
511 FetcherDropped,
512 #[error("server reports only the genesis block exists.")]
514 GenesisBlockOnly,
515 #[error(
518 "server does not serve the {pool} note commitment tree state at height {height}. connect to a server that serves {pool}."
519 )]
520 TreeStateNotServed {
521 pool: PoolType,
523 height: BlockHeight,
525 },
526 #[error(
529 "server lightwallet protocol version '{version}' does not serve transparent and ironwood data in compact blocks. v0.5.0 or later is required. change to a server that supports lightwallet protocol v0.5.0 or later and sync again."
530 )]
531 UnsupportedProtocolVersion {
532 version: String,
534 },
535 #[error(
538 "server chain height {chain_height} is below block {scan_range_end}, the end of the scan range being fetched. sync again to verify the wallet against the server's chain."
539 )]
540 ChainHeightBelowScanRange {
541 chain_height: BlockHeight,
543 scan_range_end: BlockHeight,
545 },
546}
547
548#[derive(Debug, thiserror::Error)]
550pub enum SyncModeError {
551 #[error("invalid sync mode. {0}")]
553 InvalidSyncMode(u8),
554 #[error("sync is already running")]
556 SyncAlreadyRunning,
557 #[error("sync is not running")]
559 SyncNotRunning,
560 #[error("sync is not paused")]
562 SyncNotPaused,
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 type TestSyncError = SyncError<String>;
571
572 const DETAIL_RENDERINGS: usize = 1;
574
575 fn chain_rendering(error: &(dyn std::error::Error + 'static)) -> String {
577 let mut rendered = error.to_string();
578 let mut cursor = error.source();
579 while let Some(cause) = cursor {
580 rendered.push('\n');
581 rendered.push_str(&cause.to_string());
582 cursor = cause.source();
583 }
584 rendered
585 }
586
587 #[test]
592 fn a_failed_server_request_renders_its_status_once() {
593 const DETAIL: &str = "the indexer refused the block range";
594 let failure: TestSyncError =
595 ServerError::RequestFailed(tonic::Status::unavailable(DETAIL)).into();
596 let rendered = chain_rendering(&failure);
597 assert_eq!(
598 rendered.matches(DETAIL).count(),
599 DETAIL_RENDERINGS,
600 "the chain rendered was {rendered}"
601 );
602 }
603
604 #[test]
610 fn the_rescan_announcement_names_the_consequence_and_the_cause() {
611 let announcement: TestSyncError = SyncError::PoolHistoryReopened {
612 pool: PoolType::IRONWOOD,
613 rescan_from: BlockHeight::from_u32(3_000_000),
614 disagreed_at: BlockHeight::from_u32(3_100_000),
615 block_metadata_size: 999,
616 calculated_size: 7,
617 };
618 let rendered = announcement.to_string();
619 assert!(rendered.starts_with("RESCAN TRIGGERED"), "{rendered}");
620 for fact in [
621 "cleared back to height 3000000",
622 "at height 3100000",
623 "commitment tree of 7",
624 "the chain reports 999",
625 ] {
626 assert!(rendered.contains(fact), "missing '{fact}' in: {rendered}");
627 }
628 }
629
630 #[test]
634 fn the_tree_size_refusal_names_the_disagreeing_block() {
635 let refusal = ScanError::IncorrectTreeSize {
636 shielded_protocol: PoolType::IRONWOOD,
637 height: BlockHeight::from_u32(3_100_000),
638 block_metadata_size: 999,
639 calculated_size: 7,
640 };
641 assert!(
642 refusal.to_string().contains("at height 3100000"),
643 "{refusal}"
644 );
645 }
646
647 #[derive(Debug)]
650 struct TransportError(tonic::TimeoutExpired);
651
652 impl std::fmt::Display for TransportError {
653 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
654 write!(f, "transport error")
655 }
656 }
657
658 impl std::error::Error for TransportError {
659 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
660 Some(&self.0)
661 }
662 }
663
664 fn client_timeout() -> ServerError {
667 let status = tonic::Status::from_error(Box::new(TransportError(tonic::TimeoutExpired(()))));
668 assert_eq!(status.code(), tonic::Code::Cancelled);
669 ServerError::RequestFailed(status)
670 }
671
672 fn connection_reset() -> ServerError {
675 let status = tonic::Status::from_error(Box::new(std::io::Error::from(
676 std::io::ErrorKind::ConnectionReset,
677 )));
678 assert_eq!(status.code(), tonic::Code::Unknown);
679 ServerError::RequestFailed(status)
680 }
681
682 fn network_weather_statuses() -> [tonic::Status; 5] {
685 [
686 tonic::Status::unavailable("down"),
687 tonic::Status::deadline_exceeded("timeout"),
688 tonic::Status::cancelled("cancelled"),
689 tonic::Status::resource_exhausted("overloaded"),
690 tonic::Status::aborted("aborted"),
691 ]
692 }
693
694 fn server_refusal_statuses() -> [tonic::Status; 5] {
696 [
697 tonic::Status::unimplemented("no such method"),
698 tonic::Status::invalid_argument("bad request"),
699 tonic::Status::not_found("no such block"),
700 tonic::Status::internal("indexer bug"),
701 tonic::Status::unknown("unknown"),
702 ]
703 }
704
705 mod recommend_same_server {
706 use super::*;
707
708 mod server_error {
709 use super::*;
710
711 #[test]
712 fn fetcher_dropped() {
713 assert!(ServerError::FetcherDropped.recommend_same_server());
714 }
715
716 #[test]
717 fn chain_height_below_scan_range() {
718 assert!(
719 ServerError::ChainHeightBelowScanRange {
720 chain_height: BlockHeight::from_u32(99),
721 scan_range_end: BlockHeight::from_u32(100),
722 }
723 .recommend_same_server()
724 );
725 }
726
727 #[test]
728 fn request_network_weather() {
729 for status in network_weather_statuses() {
730 let code = status.code();
731 let e = ServerError::RequestFailed(status);
732 assert!(e.recommend_same_server(), "{code:?}");
733 }
734 }
735
736 #[test]
737 fn request_client_timeout() {
738 assert!(client_timeout().recommend_same_server());
739 }
740
741 #[test]
742 fn request_connection_reset() {
743 assert!(connection_reset().recommend_same_server());
744 }
745 }
746
747 mod sync_error {
748 use super::*;
749
750 #[test]
751 fn mempool_error() {
752 let e: TestSyncError = MempoolError::ShutdownWithoutStream.into();
753 assert!(e.recommend_same_server());
754 }
755
756 #[test]
761 fn pool_history_reopened() {
762 let e: TestSyncError = SyncError::PoolHistoryReopened {
763 pool: PoolType::IRONWOOD,
764 rescan_from: BlockHeight::from_u32(100),
765 disagreed_at: BlockHeight::from_u32(150),
766 block_metadata_size: 999,
767 calculated_size: 7,
768 };
769 assert!(e.recommend_same_server());
770 }
771 }
772 }
773
774 mod recommend_change_server {
775 use super::*;
776
777 mod server_error {
778 use super::*;
779
780 #[test]
781 fn request_refused_by_server() {
782 for status in server_refusal_statuses() {
783 let code = status.code();
784 let e = ServerError::RequestFailed(status);
785 assert!(!e.recommend_same_server(), "{code:?}");
786 }
787 }
788
789 #[test]
790 fn invalid_frontier() {
791 let e = ServerError::InvalidFrontier(std::io::Error::other("bad frontier"));
792 assert!(!e.recommend_same_server());
793 }
794
795 #[test]
796 fn invalid_transaction() {
797 let e = ServerError::InvalidTransaction(std::io::Error::other("bad tx"));
798 assert!(!e.recommend_same_server());
799 }
800
801 #[test]
802 fn invalid_subtree_root() {
803 assert!(!ServerError::InvalidSubtreeRoot.recommend_same_server());
804 }
805
806 #[test]
807 fn chain_verification_error() {
808 assert!(!ServerError::ChainVerificationError.recommend_same_server());
809 }
810
811 #[test]
812 fn genesis_block_only() {
813 assert!(!ServerError::GenesisBlockOnly.recommend_same_server());
814 }
815 }
816
817 mod sync_error {
818 use super::*;
819
820 #[test]
821 fn server_request_refused() {
822 let e: TestSyncError =
823 ServerError::RequestFailed(tonic::Status::unimplemented("no such method"))
824 .into();
825 assert!(!e.recommend_same_server());
826 }
827
828 #[test]
829 fn sync_mode_error() {
830 let e: TestSyncError = SyncModeError::SyncAlreadyRunning.into();
831 assert!(!e.recommend_same_server());
832 }
833
834 #[test]
835 fn chain_error() {
836 let e: TestSyncError = SyncError::ChainError(100, 50, 50);
837 assert!(!e.recommend_same_server());
838 }
839
840 #[test]
841 fn birthday_below_sapling() {
842 let e: TestSyncError = SyncError::BirthdayBelowSapling(100, 419200);
843 assert!(!e.recommend_same_server());
844 }
845
846 #[test]
847 fn wallet_error() {
848 let e: TestSyncError = SyncError::WalletError("db locked".to_string());
849 assert!(!e.recommend_same_server());
850 }
851 }
852 }
853
854 mod recovery_recommendation {
855 use super::*;
856
857 mod retry_same_server {
858 use super::*;
859
860 #[test]
861 fn fetcher_dropped() {
862 assert_eq!(
863 ServerError::FetcherDropped.recovery_recommendation(),
864 SyncRecoveryObservables::MaybeRecoverableServer
865 );
866 }
867
868 #[test]
869 fn request_network_weather() {
870 for status in network_weather_statuses() {
871 let code = status.code();
872 let e = ServerError::RequestFailed(status);
873 assert_eq!(
874 e.recovery_recommendation(),
875 SyncRecoveryObservables::MaybeRecoverableServer,
876 "{code:?}"
877 );
878 }
879 }
880
881 #[test]
882 fn sync_error_from_unavailable() {
883 let e: TestSyncError =
884 ServerError::RequestFailed(tonic::Status::unavailable("down")).into();
885 assert_eq!(
886 e.recovery_recommendation(),
887 SyncRecoveryObservables::MaybeRecoverableServer
888 );
889 }
890
891 #[test]
894 fn request_connection_reset() {
895 assert_eq!(
896 connection_reset().recovery_recommendation(),
897 SyncRecoveryObservables::MaybeRecoverableServer
898 );
899 }
900
901 #[test]
904 fn sync_error_from_client_timeout() {
905 let e: TestSyncError = client_timeout().into();
906 assert_eq!(
907 e.recovery_recommendation(),
908 SyncRecoveryObservables::MaybeRecoverableServer
909 );
910 }
911
912 #[test]
913 fn mempool_error() {
914 let e: TestSyncError = MempoolError::ShutdownWithoutStream.into();
915 assert_eq!(
916 e.recovery_recommendation(),
917 SyncRecoveryObservables::MaybeRecoverableServer
918 );
919 }
920
921 #[test]
922 fn chain_height_below_scan_range() {
923 let e: TestSyncError = ServerError::ChainHeightBelowScanRange {
924 chain_height: BlockHeight::from_u32(99),
925 scan_range_end: BlockHeight::from_u32(100),
926 }
927 .into();
928 assert_eq!(
929 e.recovery_recommendation(),
930 SyncRecoveryObservables::MaybeRecoverableServer
931 );
932 }
933
934 #[test]
937 fn pool_history_reopened() {
938 let e: TestSyncError = SyncError::PoolHistoryReopened {
939 pool: PoolType::IRONWOOD,
940 rescan_from: BlockHeight::from_u32(100),
941 disagreed_at: BlockHeight::from_u32(150),
942 block_metadata_size: 999,
943 calculated_size: 7,
944 };
945 assert_eq!(
946 e.recovery_recommendation(),
947 SyncRecoveryObservables::MaybeRecoverableServer
948 );
949 }
950 }
951
952 mod try_different_server {
953 use super::*;
954
955 #[test]
956 fn request_refused_by_server() {
957 for status in server_refusal_statuses() {
958 let code = status.code();
959 let e = ServerError::RequestFailed(status);
960 assert_eq!(
961 e.recovery_recommendation(),
962 SyncRecoveryObservables::ServerUnavailable,
963 "{code:?}"
964 );
965 }
966 }
967
968 #[test]
969 fn sync_error_from_request_refused() {
970 let e: TestSyncError =
971 ServerError::RequestFailed(tonic::Status::unimplemented("no such method"))
972 .into();
973 assert_eq!(
974 e.recovery_recommendation(),
975 SyncRecoveryObservables::ServerUnavailable
976 );
977 }
978
979 #[test]
980 fn invalid_frontier() {
981 let e = ServerError::InvalidFrontier(std::io::Error::other("bad"));
982 assert_eq!(
983 e.recovery_recommendation(),
984 SyncRecoveryObservables::ServerUnavailable
985 );
986 }
987
988 #[test]
989 fn invalid_transaction() {
990 let e = ServerError::InvalidTransaction(std::io::Error::other("bad"));
991 assert_eq!(
992 e.recovery_recommendation(),
993 SyncRecoveryObservables::ServerUnavailable
994 );
995 }
996
997 #[test]
998 fn invalid_subtree_root() {
999 assert_eq!(
1000 ServerError::InvalidSubtreeRoot.recovery_recommendation(),
1001 SyncRecoveryObservables::ServerUnavailable
1002 );
1003 }
1004
1005 #[test]
1006 fn chain_verification_error() {
1007 assert_eq!(
1008 ServerError::ChainVerificationError.recovery_recommendation(),
1009 SyncRecoveryObservables::ServerUnavailable
1010 );
1011 }
1012
1013 #[test]
1014 fn sync_error_from_invalid_frontier() {
1015 let e: TestSyncError =
1016 ServerError::InvalidFrontier(std::io::Error::other("bad")).into();
1017 assert_eq!(
1018 e.recovery_recommendation(),
1019 SyncRecoveryObservables::ServerUnavailable
1020 );
1021 }
1022
1023 #[test]
1024 fn scan_error_wrapping_server_error() {
1025 let e: TestSyncError =
1026 ScanError::ServerError(ServerError::InvalidSubtreeRoot).into();
1027 assert_eq!(
1028 e.recovery_recommendation(),
1029 SyncRecoveryObservables::ServerUnavailable
1030 );
1031 }
1032
1033 #[test]
1034 fn scan_error_transaction_not_served() {
1035 let e: TestSyncError = ScanError::TransactionNotServed {
1036 txid: TxId::from_bytes([1; 32]),
1037 block_height: BlockHeight::from_u32(100),
1038 status: tonic::Status::not_found("no such transaction"),
1039 }
1040 .into();
1041 assert_eq!(
1042 e.recovery_recommendation(),
1043 SyncRecoveryObservables::ServerUnavailable
1044 );
1045 }
1046 }
1047
1048 mod abort {
1049 use super::*;
1050
1051 #[test]
1056 fn continuity_error() {
1057 for continuity_error in [
1058 ContinuityError::HashDiscontinuity {
1059 height: BlockHeight::from_u32(100),
1060 prev_hash: BlockHash([1; 32]),
1061 previous_block_hash: BlockHash([2; 32]),
1062 },
1063 ContinuityError::HeightDiscontinuity {
1064 height: BlockHeight::from_u32(102),
1065 previous_block_height: BlockHeight::from_u32(100),
1066 },
1067 ] {
1068 let e: TestSyncError = ScanError::ContinuityError(continuity_error).into();
1069 assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
1070 }
1071 }
1072
1073 #[test]
1074 fn genesis_block_only() {
1075 assert_eq!(
1076 ServerError::GenesisBlockOnly.recovery_recommendation(),
1077 SyncRecoveryObservables::Abort
1078 );
1079 }
1080
1081 #[test]
1082 fn sync_mode_error() {
1083 let e: TestSyncError = SyncModeError::SyncAlreadyRunning.into();
1084 assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
1085 }
1086
1087 #[test]
1088 fn chain_error() {
1089 let e: TestSyncError = SyncError::ChainError(100, 50, 50);
1090 assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
1091 }
1092
1093 #[test]
1094 fn wallet_error() {
1095 let e: TestSyncError = SyncError::WalletError("db locked".to_string());
1096 assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
1097 }
1098 }
1099 }
1100}