clementine_core/
errors.rs

1//! # Errors
2//!
3//! This module defines globally shared error messages, the crate-level error wrapper and extension traits for error/results.
4//! Our error paradigm is as follows:
5//! 1. Modules define their own error types when they need shared error messages. Module-level errors can wrap eyre::Report to capture arbitrary errors.
6//! 2. The crate-level error wrapper (BridgeError) is used to wrap errors from modules and attach extra context (ie. which module caused the error).
7//! 3. External crate errors are always wrapped by the BridgeError and never by module-level errors.
8//! 4. When using external crates inside modules, extension traits are used to convert external-crate errors into BridgeError. This is further wrapped in an eyre::Report to avoid a circular dependency.
9//! 5. BridgeError can be converted to tonic::Status to be returned to the client. Module-level errors can define [`Into<Status>`] to customize the returned status.
10//! 6. BridgeError can be used to share error messages across modules.
11//! 7. When the error cause is not sufficiently explained by the error messages, use `eyre::Context::wrap_err` to add more context. This will not hinder modules that are trying to match the error.
12//!
13//! ## Error wrapper example usage with `TxError`
14//! ```rust
15//! use thiserror::Error;
16//! use clementine_core::errors::{BridgeError, TxError, ErrorExt, ResultExt};
17//!
18//! // Function with external crate signature
19//! pub fn external_crate() -> Result<(), hex::FromHexError> {
20//!     Err(hex::FromHexError::InvalidStringLength)
21//! }
22//!
23//! // Internal function failing with some error
24//! pub fn internal_function_in_another_module() -> Result<(), BridgeError> {
25//!     Err(eyre::eyre!("I just failed").into())
26//! }
27//!
28//!
29//! // This function returns module-level errors
30//! // It can wrap external crate errors, and other crate-level errors
31//! pub fn create_some_txs() -> Result<(), TxError> {
32//!     // Do external things
33//!     // This wraps the external crate error with BridgeError, then boxes inside an eyre::Report. The `?` will convert the eyre::Report into a TxError.
34//!     external_crate().map_to_eyre()?;
35//!
36//!     // Do internal things
37//!     // This will simply wrap in eyre::Report, then rewrap in TxError.
38//!     internal_function_in_another_module().map_to_eyre()?;
39//!
40//!     // Return a module-level error
41//!     Err(TxError::TxInputNotFound)
42//! }
43//!
44//! pub fn test() -> Result<(), BridgeError> {
45//!     create_some_txs()?;
46//!     // This will convert the TxError into a BridgeError, wrapping the error with the message "Failed to build transactions" regardless of the actual error.
47//!
48//!     // Chain will be:
49//!     // 1. External case: BridgeError -> TxError -> eyre::Report -> hex::FromHexError
50//!     // 2. Internal case: BridgeError -> TxError -> eyre::Report -> BridgeError -> eyre::Report (this could've been any other module-level error)
51//!     // 3. Module-level error: BridgeError -> TxError
52//!
53//!
54//!     // error(transparent) ensures that unnecessary error messages are not repeated.
55//!     Ok(())
56//! }
57//!
58//! pub fn main() {
59//!     assert!(test().is_err());
60//! }
61//! ```
62
63use crate::{
64    actor::VerificationError,
65    builder::transaction::input::SpendableTxInError,
66    extended_bitcoin_rpc::BitcoinRPCError,
67    header_chain_prover::HeaderChainProverError,
68    rpc::{aggregator::AggregatorError, ParserError},
69};
70#[cfg(feature = "automation")]
71use crate::{states::StateMachineError, tx_sender::SendTxError};
72use bitcoin::{secp256k1::PublicKey, OutPoint, Txid, XOnlyPublicKey};
73use clap::builder::StyledStr;
74use core::fmt::Debug;
75use hex::FromHexError;
76use http::StatusCode;
77use thiserror::Error;
78use tonic::Status;
79
80pub use crate::builder::transaction::TxError;
81
82/// Errors returned by the bridge.
83#[derive(Debug, Error)]
84#[non_exhaustive]
85pub enum BridgeError {
86    #[error("Header chain prover returned an error: {0}")]
87    Prover(#[from] HeaderChainProverError),
88    #[error("Failed to build transactions: {0}")]
89    Transaction(#[from] TxError),
90    #[cfg(feature = "automation")]
91    #[error("Failed to send transactions: {0}")]
92    SendTx(#[from] SendTxError),
93    #[error("Aggregator error: {0}")]
94    Aggregator(#[from] AggregatorError),
95    #[error("Failed to parse request: {0}")]
96    Parser(#[from] ParserError),
97    #[error("SpendableTxIn error: {0}")]
98    SpendableTxIn(#[from] SpendableTxInError),
99    #[error("Bitcoin RPC error: {0}")]
100    BitcoinRPC(#[from] BitcoinRPCError),
101    #[cfg(feature = "automation")]
102    #[error("State machine error: {0}")]
103    StateMachine(#[from] StateMachineError),
104    #[error("RPC authentication error: {0}")]
105    RPCAuthError(#[from] VerificationError),
106
107    // Shared error messages
108    #[error("Unsupported network")]
109    UnsupportedNetwork,
110    #[error("Invalid configuration: {0}")]
111    ConfigError(String),
112    #[error("Missing environment variable {1}: {0}")]
113    EnvVarNotSet(std::env::VarError, &'static str),
114    #[error("Environment variable {0} is malformed: {1}")]
115    EnvVarMalformed(&'static str, String),
116
117    #[error("Failed to convert between integer types")]
118    IntConversionError,
119    #[error("Failed to encode/decode data using borsh")]
120    BorshError,
121    #[error("Operator x-only public key {0} was not found in the DB")]
122    OperatorNotFound(XOnlyPublicKey),
123    #[error("Verifier with public key {0} was not found among the verifier clients")]
124    VerifierNotFound(PublicKey),
125    #[error("Deposit not found in DB: {0:?}")]
126    DepositNotFound(OutPoint),
127    #[error("Deposit is invalid due to {0}")]
128    InvalidDeposit(String),
129    #[error("Operator data mismatch. Data already stored in DB and received by set_operator doesn't match for xonly_pk: {0}")]
130    OperatorDataMismatch(XOnlyPublicKey),
131    #[error("Deposit data mismatch. Data already stored in DB doesn't match the new data for deposit {0:?}")]
132    DepositDataMismatch(OutPoint),
133    #[error("Operator winternitz public keys mismatch. Data already stored in DB doesn't match the new data for operator {0}")]
134    OperatorWinternitzPublicKeysMismatch(XOnlyPublicKey),
135    #[error("BitVM setup data mismatch. Data already stored in DB doesn't match the new data for operator {0} and deposit {1:?}")]
136    BitvmSetupDataMismatch(XOnlyPublicKey, OutPoint),
137    #[error("BitVM replacement data will exhaust memory. The maximum number of operations is {0}")]
138    BitvmReplacementResourceExhaustion(usize),
139    #[error("Operator challenge ack hashes mismatch. Data already stored in DB doesn't match the new data for operator {0} and deposit {1:?}")]
140    OperatorChallengeAckHashesMismatch(XOnlyPublicKey, OutPoint),
141    #[error("Invalid BitVM public keys")]
142    InvalidBitVMPublicKeys,
143    #[error("Invalid challenge ack hashes")]
144    InvalidChallengeAckHashes,
145    #[error("Invalid operator index")]
146    InvalidOperatorIndex,
147    #[error("Invalid protocol paramset")]
148    InvalidProtocolParamset,
149    #[error("Deposit already signed and move txid {0} is in chain")]
150    DepositAlreadySigned(Txid),
151    #[error("Invalid withdrawal ECDSA verification signature")]
152    InvalidECDSAVerificationSignature,
153    #[error("Withdrawal ECDSA verification signature missing")]
154    ECDSAVerificationSignatureMissing,
155    #[error("Clementine versions or configs are not compatible: {0}")]
156    ClementineNotCompatible(String),
157
158    // External crate error wrappers
159    #[error("Failed to call database: {0}")]
160    DatabaseError(#[from] sqlx::Error),
161    #[error("Failed to convert hex string: {0}")]
162    FromHexError(#[from] FromHexError),
163    #[error("Failed to convert to hash from slice: {0}")]
164    FromSliceError(#[from] bitcoin::hashes::FromSliceError),
165    #[error("Error while calling EVM contract: {0}")]
166    AlloyContract(#[from] alloy::contract::Error),
167    #[error("Error while calling EVM RPC function: {0}")]
168    AlloyRpc(#[from] alloy::transports::RpcError<alloy::transports::TransportErrorKind>),
169    #[error("Error while encoding/decoding EVM type: {0}")]
170    AlloySolTypes(#[from] alloy::sol_types::Error),
171    #[error("{0}")]
172    CLIDisplayAndExit(StyledStr),
173    #[error(transparent)]
174    RPCStatus(#[from] Box<Status>),
175
176    #[error("Arithmetic overflow occurred: {0}")]
177    ArithmeticOverflow(&'static str),
178    #[error("Insufficient funds: {0}")]
179    InsufficientFunds(&'static str),
180
181    // Base wrapper for eyre
182    #[error(transparent)]
183    Eyre(#[from] eyre::Report),
184}
185
186#[derive(Debug, Error)]
187pub(crate) enum FeeErr {
188    #[error("request timed out")]
189    Timeout,
190    #[error("transport/decode error: {0}")]
191    Transport(#[from] reqwest::Error),
192    #[error("http status {0}")]
193    Status(StatusCode),
194    #[error("json decode error: {0}")]
195    JsonDecode(reqwest::Error),
196    #[error("'fastestFee' field not found or invalid in API response")]
197    MissingField,
198}
199
200/// Extension traits for errors to easily convert them to eyre::Report and
201/// tonic::Status through BridgeError.
202pub trait ErrorExt: Sized {
203    /// Converts the error into an eyre::Report, first wrapping in
204    /// BridgeError if necessary. It does not rewrap in eyre::Report if
205    /// the given error is already an eyre::Report.
206    fn into_eyre(self) -> eyre::Report;
207    /// Converts the error into a tonic::Status. Walks the chain of errors and
208    /// returns the first [`tonic::Status`] error. If it can't find one, it will
209    /// return an Status::internal with the Display representation of the error.
210    fn into_status(self) -> tonic::Status;
211}
212
213/// Extension traits for results to easily convert them to eyre::Report and
214/// tonic::Status through BridgeError.
215pub trait ResultExt: Sized {
216    type Output;
217
218    fn map_to_eyre(self) -> Result<Self::Output, eyre::Report>;
219    #[allow(clippy::result_large_err)]
220    fn map_to_status(self) -> Result<Self::Output, tonic::Status>;
221}
222
223impl<T: Into<BridgeError>> ErrorExt for T {
224    fn into_eyre(self) -> eyre::Report {
225        match self.into() {
226            BridgeError::Eyre(report) => report,
227            other => eyre::eyre!(other),
228        }
229    }
230    fn into_status(self) -> tonic::Status {
231        self.into().into()
232    }
233}
234
235impl<U: Sized, T: Into<BridgeError>> ResultExt for Result<U, T> {
236    type Output = U;
237
238    fn map_to_eyre(self) -> Result<Self::Output, eyre::Report> {
239        self.map_err(ErrorExt::into_eyre)
240    }
241
242    fn map_to_status(self) -> Result<Self::Output, tonic::Status> {
243        self.map_err(ErrorExt::into_status)
244    }
245}
246
247impl From<Status> for BridgeError {
248    fn from(status: Status) -> Self {
249        BridgeError::RPCStatus(Box::new(status))
250    }
251}
252
253impl From<BridgeError> for tonic::Status {
254    fn from(val: BridgeError) -> Self {
255        let err = format!("{val:#}");
256        // delete escape characters
257        let flattened = err
258            .replace("\\n", " ") // remove escaped newlines
259            .replace("\n", " ") // remove real newlines
260            .replace("\"", "") // delete quotes
261            .replace("\\", ""); // remove any remaining backslashes
262        let whitespace_removed = flattened.split_whitespace().collect::<Vec<_>>().join(" ");
263        tonic::Status::internal(whitespace_removed)
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use eyre::Context;
270
271    use super::*;
272    #[test]
273    fn test_downcast() {
274        assert_eq!(
275            BridgeError::IntConversionError
276                .into_eyre()
277                .wrap_err("Some other error")
278                .into_eyre()
279                .wrap_err("some other")
280                .downcast_ref::<BridgeError>()
281                .unwrap()
282                .to_string(),
283            BridgeError::IntConversionError.to_string()
284        );
285    }
286
287    #[test]
288    fn test_status_shows_all_errors_in_chain() {
289        let err: BridgeError = Err::<(), BridgeError>(BridgeError::BitcoinRPC(
290            BitcoinRPCError::TransactionNotConfirmed,
291        ))
292        .wrap_err(tonic::Status::deadline_exceeded("Error A"))
293        .wrap_err("Error B")
294        .unwrap_err()
295        .into();
296
297        let status: Status = err.into();
298        assert!(status.message().contains("Error A"));
299        assert!(status.message().contains("Error B"));
300        assert!(status.message().contains("Bitcoin"));
301    }
302}