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}