Skip to main content

dig_blockstore/
error.rs

1//! `BlockStore` error surface (`BlockStoreError`).
2//!
3//! **Requirements**
4//! - [`ERR-001`](../docs/requirements/domains/error_types/specs/ERR-001_blockstoreerror_enum.md) — thirteen
5//!   variants, `thiserror::Error` + `Debug`.
6//! - [`ERR-002`](../docs/requirements/domains/error_types/specs/ERR-002_error_from_conversions.md) —
7//!   `From<rocksdb::Error>` (via `#[from]`), [`From<bincode::Error>`] for [`Serialization`](BlockStoreError::Serialization),
8//!   and explicit zstd / [`std::io::Error`] → [`Compression`](BlockStoreError::Compression) mapping
9//!   ([`BlockStoreError::compression_from_io`]).
10//! - [`ERR-003`](../docs/requirements/domains/error_types/specs/ERR-003_error_display_messages.md) —
11//!   every variant’s [`std::fmt::Display`] (via thiserror `#[error]`) must embed actionable context: hashes as
12//!   hex ([`Bytes32`](chia_protocol::Bytes32) implements [`Display`](std::fmt::Display)), numeric fields inlined,
13//!   and static messages for unit variants ([`NORMATIVE` ERR-003](../docs/requirements/domains/error_types/NORMATIVE.md#err-003-error-display-messages)).
14//! - Normative: [`ERR domain NORMATIVE`](../docs/requirements/domains/error_types/NORMATIVE.md#err-001-blockstoreerror-enum).
15//! - SPEC: [`SPEC.md` §12](../docs/resources/SPEC.md) (error taxonomy; ERR-001 adds `EmptyReorgChain` and
16//!   `PipelineClosed` beyond the SPEC snippet).
17//!
18//! ## Operational errors without a first-class variant
19//!
20//! [`ERR-001`](../docs/requirements/domains/error_types/specs/ERR-001_blockstoreerror_enum.md) caps the enum at
21//! thirteen cases. Some [`STR-004`](../docs/requirements/domains/crate_structure/specs/STR-004.md) guards
22//! (missing read-only path, read-only mutation, double genesis) therefore map to [`BlockStoreError::Serialization`]
23//! with **stable string payloads** documented below so integration tests and future refactors can match
24//! deterministically. If the taxonomy gains dedicated variants later, these constants become the migration
25//! anchor.
26
27use chia_protocol::Bytes32;
28use thiserror::Error;
29
30/// Stable [`BlockStoreError::Serialization`] payload prefix when [`crate::store::BlockStore::open_readonly`]
31/// is called with a path that does not exist on disk.
32pub const ERR_OPEN_READONLY_PATH_MISSING_PREFIX: &str =
33    "open_readonly: database path does not exist: ";
34
35/// Stable [`BlockStoreError::Serialization`] payload when [`crate::store::BlockStore::init_genesis`] runs on
36/// a read-only handle.
37pub const ERR_INIT_GENESIS_READ_ONLY: &str = "init_genesis: block store is read-only";
38
39/// Stable [`BlockStoreError::Serialization`] payload for other mutating APIs ([`crate::store::BlockStore::put`])
40/// on a read-only handle ([`BLK-001`](../docs/requirements/domains/block_storage/specs/BLK-001.md) precursor).
41pub const ERR_MUTATION_READ_ONLY: &str = "mutating API invoked on read-only block store";
42
43/// Stable [`BlockStoreError::Serialization`] payload when genesis metadata is already present.
44pub const ERR_INIT_GENESIS_ALREADY_INITIALIZED: &str =
45    "init_genesis: block store already initialized";
46
47/// Stable [`BlockStoreError::Serialization`] payload prefix when [`crate::store::BlockStore::update_status`] runs but
48/// the in-memory record cache has no entry for the hash ([`BLK-010`](../docs/requirements/domains/block_storage/specs/BLK-010.md) AC §3).
49///
50/// **Why not a dedicated enum variant:** [`ERR-001`](../docs/requirements/domains/error_types/specs/ERR-001_blockstoreerror_enum.md) caps [`BlockStoreError`] at thirteen variants; this follows the same stable-string pattern as
51/// [`ERR_ASYNC_JOIN_PREFIX`] and read-only guards until the taxonomy grows.
52pub const ERR_UPDATE_STATUS_RECORD_NOT_CACHED_PREFIX: &str =
53    "update_status: no BlockRecord cached for block hash ";
54
55/// Stable [`BlockStoreError::Serialization`] payload prefix when a [`tokio::task::spawn_blocking`]
56/// task panics or is cancelled and [`tokio::task::JoinError`] surfaces on `.await`
57/// ([`BLK-007`](../docs/requirements/domains/block_storage/specs/BLK-007.md) AC §6).
58///
59/// **Rationale:** [`ERR-001`](../docs/requirements/domains/error_types/specs/ERR-001_blockstoreerror_enum.md) caps
60/// [`BlockStoreError`] at thirteen variants, so async join failures are folded into [`Serialization`](BlockStoreError::Serialization)
61/// with this discriminating prefix instead of a dedicated enum arm.
62pub const ERR_ASYNC_JOIN_PREFIX: &str = "async blocking task join failed: ";
63
64/// Crate-level error for persistence, chain, and I/O boundaries ([`ERR-001`](../docs/requirements/domains/error_types/specs/ERR-001_blockstoreerror_enum.md)).
65///
66/// **Display:** Each `#[error("…")]` attribute is the contract for logs and user-facing text ([`ERR-003`](../docs/requirements/domains/error_types/specs/ERR-003_error_display_messages.md)).
67///
68/// **Async:** All variants are `Send + Sync` (see `err_001_tests` static assertions).
69#[derive(Debug, Error)]
70pub enum BlockStoreError {
71    /// Wraps an underlying RocksDB error ([`rocksdb::Error`]).
72    ///
73    /// **`#[source]`:** Preserves the inner error for `Error::source()` (ERR-001 test plan).
74    #[error("rocksdb error: {0}")]
75    RocksDb(
76        #[from]
77        #[source]
78        rocksdb::Error,
79    ),
80
81    /// Bincode or other structural encode/decode failure ([`SER-001`](../docs/requirements/domains/serialization/specs/SER-001.md), [`SER-002`](../docs/requirements/domains/serialization/specs/SER-002.md)).
82    #[error("serialization error: {0}")]
83    Serialization(String),
84
85    /// Zstd compress/decompress failure ([`SER-001`](../docs/requirements/domains/serialization/specs/SER-001.md)).
86    #[error("compression error: {0}")]
87    Compression(String),
88
89    /// Requested block hash is not present ([`BLK-002`](../docs/requirements/domains/block_storage/specs/BLK-002.md)).
90    #[error("block not found: {0}")]
91    BlockNotFound(Bytes32),
92
93    /// No checkpoint row for the given epoch ([`CKP-002`](../docs/requirements/domains/checkpoint_storage/specs/CKP-002.md)).
94    #[error("checkpoint not found for epoch {0}")]
95    CheckpointNotFound(u64),
96
97    /// Chain / canonical operation referenced a hash that is not stored ([`CAN-003`](../docs/requirements/domains/canonical_chain/specs/CAN-003.md)).
98    #[error("block not in store: {0}")]
99    BlockNotInStore(Bytes32),
100
101    /// Rollback would violate the pruning floor ([`ROR-001`](../docs/requirements/domains/rollback_reorg/specs/ROR-001.md)).
102    #[error("rollback target {target} is below minimum retained height {min}")]
103    RollbackBelowMin { target: u64, min: u64 },
104
105    /// Rollback target is above the current tip ([`ROR-001`](../docs/requirements/domains/rollback_reorg/specs/ROR-001.md)).
106    #[error("rollback target {target} is above current tip {tip}")]
107    RollbackAboveTip { target: u64, tip: u64 },
108
109    /// Metadata does not contain a tip ([`CAN-007`](../docs/requirements/domains/canonical_chain/specs/CAN-007.md)).
110    #[error("no chain tip set")]
111    NoTip,
112
113    /// On-disk schema / version mismatch ([`TYP-002`](../docs/requirements/domains/storage_types/specs/TYP-002.md) metadata keys).
114    #[error("schema mismatch: expected {expected}, found {found}")]
115    SchemaMismatch { expected: u32, found: u32 },
116
117    /// Operation requires genesis / initialized metadata ([`STR-004`](../docs/requirements/domains/crate_structure/specs/STR-004.md), [`BLK-013`](../docs/requirements/domains/block_storage/specs/BLK-013.md)).
118    #[error("store not initialized")]
119    NotInitialized,
120
121    /// [`apply_reorg`](crate::store::BlockStore) called with an empty new chain ([`ROR-003`](../docs/requirements/domains/rollback_reorg/specs/ROR-003.md)).
122    #[error("empty reorg chain: new_chain_hashes must not be empty")]
123    EmptyReorgChain,
124
125    /// Async write pipeline channel is closed ([`BLK-008`](../docs/requirements/domains/block_storage/specs/BLK-008.md)).
126    #[error("write pipeline closed")]
127    PipelineClosed,
128}
129
130impl BlockStoreError {
131    /// Maps an [`std::io::Error`] produced by **zstd** compress/decompress helpers ([`zstd::encode_all`],
132    /// [`zstd::decode_all`], …) into [`BlockStoreError::Compression`].
133    ///
134    /// **Rationale ([`ERR-002`](../docs/requirements/domains/error_types/specs/ERR-002_error_from_conversions.md)):**
135    /// We intentionally **do not** implement [`From<std::io::Error>`] on [`BlockStoreError`]. Plain
136    /// filesystem I/O (for example `create_dir_all`) is surfaced as [`Serialization`](BlockStoreError::Serialization)
137    /// today ([`ERR-001`](../docs/requirements/domains/error_types/specs/ERR-001_blockstoreerror_enum.md) interim mapping);
138    /// a blanket `From<io::Error>` would make those sites ambiguous or route the wrong variant. Callers
139    /// that know the `io::Error` came from zstd should use this helper with [`Result::map_err`] or a closure.
140    #[must_use]
141    pub fn compression_from_io(err: std::io::Error) -> Self {
142        Self::Compression(err.to_string())
143    }
144}
145
146impl From<bincode::Error> for BlockStoreError {
147    /// Bincode encode/decode failures map one-to-one onto [`BlockStoreError::Serialization`] ([`ERR-002`](../docs/requirements/domains/error_types/specs/ERR-002_error_from_conversions.md)).
148    ///
149    /// **Note:** [`std::io::Error`] is **not** covered here — use [`BlockStoreError::compression_from_io`] for zstd I/O.
150    fn from(err: bincode::Error) -> Self {
151        Self::Serialization(err.to_string())
152    }
153}