use std::{array::TryFromSliceError, convert::Infallible};
use shardtree::error::ShardTreeError;
use zcash_primitives::{block::BlockHash, transaction::TxId};
use zcash_protocol::consensus::BlockHeight;
use zcash_protocol::{PoolType, ShieldedProtocol};
use crate::wallet::OutputId;
#[derive(Debug, thiserror::Error)]
pub enum SyncError<E>
where
E: std::fmt::Debug + std::fmt::Display,
{
#[error("mempool error. {0}")]
MempoolError(#[from] MempoolError),
#[error("scan error. {0}")]
ScanError(#[from] ScanError),
#[error("server error. {0}")]
ServerError(#[from] ServerError),
#[error("sync mode error. {0}")]
SyncModeError(#[from] SyncModeError),
#[error("wallet height {0} is more than {1} blocks ahead of best chain height {2}")]
ChainError(u32, u32, u32),
#[error(
"birthday {0} below sapling activation height {1}. pre-sapling wallets are not supported!"
)]
BirthdayBelowSapling(u32, u32),
#[error("shard tree error. {0}")]
ShardTreeError(#[from] ShardTreeError<Infallible>),
#[error(
"critical non-recoverable truncation error at height {0} due to missing {1} shard tree checkpoints. wallet data cleared. rescan required."
)]
TruncationError(BlockHeight, PoolType),
#[error("transparent address derivation error. {0}")]
TransparentAddressDerivationError(bip32::Error),
#[error("wallet error. {0}")]
WalletError(E),
}
impl<E: std::fmt::Debug + std::fmt::Display> SyncError<E> {
pub fn recommend_same_server(&self) -> bool {
match self {
SyncError::ServerError(e) => e.recommend_same_server(),
SyncError::MempoolError(_) => true,
SyncError::ScanError(_)
| SyncError::SyncModeError(_)
| SyncError::ChainError(..)
| SyncError::BirthdayBelowSapling(..)
| SyncError::ShardTreeError(_)
| SyncError::TruncationError(..)
| SyncError::TransparentAddressDerivationError(_)
| SyncError::WalletError(_) => false,
}
}
}
impl ServerError {
pub fn recommend_same_server(&self) -> bool {
match self {
ServerError::FetcherDropped => true,
ServerError::RequestFailed(_) => false,
ServerError::InvalidFrontier(_)
| ServerError::InvalidTransaction(_)
| ServerError::InvalidSubtreeRoot
| ServerError::ChainVerificationError
| ServerError::GenesisBlockOnly => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncRecoveryObservables {
MaybeRecoverableServer,
ServerUnavailable,
Abort,
}
impl<E: std::fmt::Debug + std::fmt::Display> SyncError<E> {
pub fn recovery_recommendation(&self) -> SyncRecoveryObservables {
match self {
SyncError::ServerError(e) => e.recovery_recommendation(),
SyncError::MempoolError(_) => SyncRecoveryObservables::MaybeRecoverableServer,
SyncError::ScanError(ScanError::ServerError(e)) => e.recovery_recommendation(),
SyncError::ScanError(_) => SyncRecoveryObservables::Abort,
SyncError::SyncModeError(_)
| SyncError::ChainError(..)
| SyncError::BirthdayBelowSapling(..)
| SyncError::ShardTreeError(_)
| SyncError::TruncationError(..)
| SyncError::TransparentAddressDerivationError(_)
| SyncError::WalletError(_) => SyncRecoveryObservables::Abort,
}
}
}
impl ServerError {
pub fn recovery_recommendation(&self) -> SyncRecoveryObservables {
match self {
ServerError::FetcherDropped => SyncRecoveryObservables::MaybeRecoverableServer,
ServerError::RequestFailed(_)
| ServerError::InvalidFrontier(_)
| ServerError::InvalidTransaction(_)
| ServerError::InvalidSubtreeRoot
| ServerError::ChainVerificationError => SyncRecoveryObservables::ServerUnavailable,
ServerError::GenesisBlockOnly => SyncRecoveryObservables::Abort,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SyncStatusError<E>
where
E: std::fmt::Debug + std::fmt::Display,
{
#[error("No sync data. Wallet has never been synced with the block chain.")]
NoSyncData,
#[error("wallet error. {0}")]
WalletError(E),
}
#[derive(Debug, thiserror::Error)]
pub enum MempoolError {
#[error("server error. {0}")]
ServerError(#[from] ServerError),
#[error(
"timed out fetching mempool stream during shutdown.\nNON-CRITICAL: sync completed successfully but may not have scanned transactions in the mempool."
)]
ShutdownWithoutStream,
}
#[derive(Debug, thiserror::Error)]
pub enum ScanError {
#[error("server error. {0}")]
ServerError(#[from] ServerError),
#[error("continuity error. {0}")]
ContinuityError(#[from] ContinuityError),
#[error("{0}")]
EncodingError(#[from] EncodingInvalid),
#[error("invalid sapling nullifier. {0}")]
InvalidSaplingNullifier(#[from] TryFromSliceError),
#[error("invalid orchard nullifier length. should be 32 bytes, found {0}")]
InvalidOrchardNullifierLength(usize),
#[error("invalid orchard nullifier")]
InvalidOrchardNullifier,
#[error("invalid sapling output")]
InvalidSaplingOutput,
#[error("invalid orchard action")]
InvalidOrchardAction,
#[error(
"incorrect tree size. {shielded_protocol} tree size recorded in block metadata {block_metadata_size} does not match calculated size {calculated_size}"
)]
IncorrectTreeSize {
shielded_protocol: PoolType,
block_metadata_size: u32,
calculated_size: u32,
},
#[error(
"txid of transaction returned by the server does not match requested txid.\ntxid requested: {txid_requested}\ntxid returned: {txid_returned}"
)]
IncorrectTxid {
txid_requested: TxId,
txid_returned: TxId,
},
#[error("decrypted note nullifier and position data not found. output id: {0:?}")]
DecryptedNoteDataNotFound(OutputId),
#[error("invalid memo bytes. {0}")]
InvalidMemoBytes(#[from] zcash_protocol::memo::Error),
#[error("failed to parse encoded address. {0}")]
AddressParseError(#[from] zcash_address::unified::ParseError),
}
#[derive(Debug, thiserror::Error)]
#[error("{pool_type:?} output {index} of transaction {txid} was improperly encoded.")]
pub struct EncodingInvalid {
pub(crate) at_height: BlockHeight,
pub(crate) txid: TxId,
pub(crate) pool_type: ShieldedProtocol,
pub(crate) index: usize,
pub(crate) error: CompactFormatError,
}
#[derive(Clone, Debug)]
pub enum CompactFormatError {
InvalidLength(std::array::TryFromSliceError),
InvalidValue,
}
impl std::fmt::Display for CompactFormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompactFormatError::InvalidLength(e) => write!(f, "Invalid compact format field: {e}"),
CompactFormatError::InvalidValue => {
write!(f, "Compact format field is not a valid protocol element")
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ContinuityError {
#[error(
"height discontinuity. block with height {height} is not continuous with previous block height {previous_block_height}"
)]
HeightDiscontinuity {
height: BlockHeight,
previous_block_height: BlockHeight,
},
#[error(
"hash discontinuity. block prev_hash {prev_hash} with height {height} does not match previous block hash {previous_block_hash}"
)]
HashDiscontinuity {
height: BlockHeight,
prev_hash: BlockHash,
previous_block_hash: BlockHash,
},
}
#[derive(Debug, thiserror::Error)]
pub enum ServerError {
#[error("server request failed. {0}")]
RequestFailed(#[from] tonic::Status),
#[error("server returned invalid frontier. {0}")]
InvalidFrontier(std::io::Error),
#[error("server returned invalid transaction. {0}")]
InvalidTransaction(std::io::Error),
#[error("server returned invalid subtree root.")]
InvalidSubtreeRoot,
#[error(
"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."
)]
ChainVerificationError,
#[error("fetcher task was dropped.")]
FetcherDropped,
#[error("server reports only the genesis block exists.")]
GenesisBlockOnly,
}
#[derive(Debug, thiserror::Error)]
pub enum SyncModeError {
#[error("invalid sync mode. {0}")]
InvalidSyncMode(u8),
#[error("sync is already running")]
SyncAlreadyRunning,
#[error("sync is not running")]
SyncNotRunning,
#[error("sync is not paused")]
SyncNotPaused,
}
#[cfg(test)]
mod tests {
use super::*;
type TestSyncError = SyncError<String>;
mod recommend_same_server {
use super::*;
mod server_error {
use super::*;
#[test]
fn fetcher_dropped() {
assert!(ServerError::FetcherDropped.recommend_same_server());
}
}
mod sync_error {
use super::*;
#[test]
fn mempool_error() {
let e: TestSyncError = MempoolError::ShutdownWithoutStream.into();
assert!(e.recommend_same_server());
}
}
}
mod recommend_change_server {
use super::*;
mod server_error {
use super::*;
#[test]
fn request_failed() {
let e = ServerError::RequestFailed(tonic::Status::deadline_exceeded("timeout"));
assert!(!e.recommend_same_server());
}
#[test]
fn invalid_frontier() {
let e = ServerError::InvalidFrontier(std::io::Error::other("bad frontier"));
assert!(!e.recommend_same_server());
}
#[test]
fn invalid_transaction() {
let e = ServerError::InvalidTransaction(std::io::Error::other("bad tx"));
assert!(!e.recommend_same_server());
}
#[test]
fn invalid_subtree_root() {
assert!(!ServerError::InvalidSubtreeRoot.recommend_same_server());
}
#[test]
fn chain_verification_error() {
assert!(!ServerError::ChainVerificationError.recommend_same_server());
}
#[test]
fn genesis_block_only() {
assert!(!ServerError::GenesisBlockOnly.recommend_same_server());
}
}
mod sync_error {
use super::*;
#[test]
fn server_request_failed() {
let e: TestSyncError =
ServerError::RequestFailed(tonic::Status::deadline_exceeded("timeout")).into();
assert!(!e.recommend_same_server());
}
#[test]
fn sync_mode_error() {
let e: TestSyncError = SyncModeError::SyncAlreadyRunning.into();
assert!(!e.recommend_same_server());
}
#[test]
fn chain_error() {
let e: TestSyncError = SyncError::ChainError(100, 50, 50);
assert!(!e.recommend_same_server());
}
#[test]
fn birthday_below_sapling() {
let e: TestSyncError = SyncError::BirthdayBelowSapling(100, 419200);
assert!(!e.recommend_same_server());
}
#[test]
fn wallet_error() {
let e: TestSyncError = SyncError::WalletError("db locked".to_string());
assert!(!e.recommend_same_server());
}
}
}
mod recovery_recommendation {
use super::*;
mod retry_same_server {
use super::*;
#[test]
fn fetcher_dropped() {
assert_eq!(
ServerError::FetcherDropped.recovery_recommendation(),
SyncRecoveryObservables::MaybeRecoverableServer
);
}
#[test]
fn mempool_error() {
let e: TestSyncError = MempoolError::ShutdownWithoutStream.into();
assert_eq!(
e.recovery_recommendation(),
SyncRecoveryObservables::MaybeRecoverableServer
);
}
}
mod try_different_server {
use super::*;
#[test]
fn request_failed() {
let e = ServerError::RequestFailed(tonic::Status::deadline_exceeded("timeout"));
assert_eq!(
e.recovery_recommendation(),
SyncRecoveryObservables::ServerUnavailable
);
}
#[test]
fn sync_error_from_request_failed() {
let e: TestSyncError =
ServerError::RequestFailed(tonic::Status::unavailable("down")).into();
assert_eq!(
e.recovery_recommendation(),
SyncRecoveryObservables::ServerUnavailable
);
}
#[test]
fn invalid_frontier() {
let e = ServerError::InvalidFrontier(std::io::Error::other("bad"));
assert_eq!(
e.recovery_recommendation(),
SyncRecoveryObservables::ServerUnavailable
);
}
#[test]
fn invalid_transaction() {
let e = ServerError::InvalidTransaction(std::io::Error::other("bad"));
assert_eq!(
e.recovery_recommendation(),
SyncRecoveryObservables::ServerUnavailable
);
}
#[test]
fn invalid_subtree_root() {
assert_eq!(
ServerError::InvalidSubtreeRoot.recovery_recommendation(),
SyncRecoveryObservables::ServerUnavailable
);
}
#[test]
fn chain_verification_error() {
assert_eq!(
ServerError::ChainVerificationError.recovery_recommendation(),
SyncRecoveryObservables::ServerUnavailable
);
}
#[test]
fn sync_error_from_invalid_frontier() {
let e: TestSyncError =
ServerError::InvalidFrontier(std::io::Error::other("bad")).into();
assert_eq!(
e.recovery_recommendation(),
SyncRecoveryObservables::ServerUnavailable
);
}
#[test]
fn scan_error_wrapping_server_error() {
let e: TestSyncError =
ScanError::ServerError(ServerError::InvalidSubtreeRoot).into();
assert_eq!(
e.recovery_recommendation(),
SyncRecoveryObservables::ServerUnavailable
);
}
}
mod abort {
use super::*;
#[test]
fn genesis_block_only() {
assert_eq!(
ServerError::GenesisBlockOnly.recovery_recommendation(),
SyncRecoveryObservables::Abort
);
}
#[test]
fn sync_mode_error() {
let e: TestSyncError = SyncModeError::SyncAlreadyRunning.into();
assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
}
#[test]
fn chain_error() {
let e: TestSyncError = SyncError::ChainError(100, 50, 50);
assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
}
#[test]
fn wallet_error() {
let e: TestSyncError = SyncError::WalletError("db locked".to_string());
assert_eq!(e.recovery_recommendation(), SyncRecoveryObservables::Abort);
}
}
}
}