clementine_core/errors.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
//! # Errors
//!
//! This module defines globally shared error messages, the crate-level error wrapper and extension traits for error/results.
//! Our error paradigm is as follows:
//! 1. Modules define their own error types when they need shared error messages. Module-level errors can wrap eyre::Report to capture arbitrary errors.
//! 2. The crate-level error wrapper (BridgeError) is used to wrap errors from modules and attach extra context (ie. which module caused the error).
//! 3. External crate errors are always wrapped by the BridgeError and never by module-level errors.
//! 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.
//! 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.
//! 6. BridgeError can be used to share error messages across modules.
//! 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.
//!
//! ## Error wrapper example usage with `TxError`
//! ```rust
//! use thiserror::Error;
//! use clementine_core::errors::{BridgeError, TxError, ErrorExt, ResultExt};
//!
//! // Function with external crate signature
//! pub fn external_crate() -> Result<(), hex::FromHexError> {
//! Err(hex::FromHexError::InvalidStringLength)
//! }
//!
//! // Internal function failing with some error
//! pub fn internal_function_in_another_module() -> Result<(), BridgeError> {
//! Err(eyre::eyre!("I just failed").into())
//! }
//!
//!
//! // This function returns module-level errors
//! // It can wrap external crate errors, and other crate-level errors
//! pub fn create_some_txs() -> Result<(), TxError> {
//! // Do external things
//! // This wraps the external crate error with BridgeError, then boxes inside an eyre::Report. The `?` will convert the eyre::Report into a TxError.
//! external_crate().map_to_eyre()?;
//!
//! // Do internal things
//! // This will simply wrap in eyre::Report, then rewrap in TxError.
//! internal_function_in_another_module().map_to_eyre()?;
//!
//! // Return a module-level error
//! Err(TxError::TxInputNotFound)
//! }
//!
//! pub fn test() -> Result<(), BridgeError> {
//! create_some_txs()?;
//! // This will convert the TxError into a BridgeError, wrapping the error with the message "Failed to build transactions" regardless of the actual error.
//!
//! // Chain will be:
//! // 1. External case: BridgeError -> TxError -> eyre::Report -> hex::FromHexError
//! // 2. Internal case: BridgeError -> TxError -> eyre::Report -> BridgeError -> eyre::Report (this could've been any other module-level error)
//! // 3. Module-level error: BridgeError -> TxError
//!
//!
//! // error(transparent) ensures that unnecessary error messages are not repeated.
//! Ok(())
//! }
//!
//! pub fn main() {
//! assert!(test().is_err());
//! }
//! ```
use crate::{
actor::VerificationError,
builder::transaction::input::SpendableTxInError,
extended_rpc::BitcoinRPCError,
header_chain_prover::HeaderChainProverError,
rpc::{aggregator::AggregatorError, ParserError},
};
#[cfg(feature = "automation")]
use crate::{states::StateMachineError, tx_sender::SendTxError};
use bitcoin::{secp256k1::PublicKey, OutPoint, Txid, XOnlyPublicKey};
use clap::builder::StyledStr;
use core::fmt::Debug;
use hex::FromHexError;
use thiserror::Error;
use tonic::Status;
pub use crate::builder::transaction::TxError;
/// Errors returned by the bridge.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum BridgeError {
#[error("Header chain prover returned an error: {0}")]
Prover(#[from] HeaderChainProverError),
#[error("Failed to build transactions: {0}")]
Transaction(#[from] TxError),
#[cfg(feature = "automation")]
#[error("Failed to send transactions: {0}")]
SendTx(#[from] SendTxError),
#[error("Aggregator error: {0}")]
Aggregator(#[from] AggregatorError),
#[error("Failed to parse request: {0}")]
Parser(#[from] ParserError),
#[error("SpendableTxIn error: {0}")]
SpendableTxIn(#[from] SpendableTxInError),
#[error("Bitcoin RPC error: {0}")]
BitcoinRPC(#[from] BitcoinRPCError),
#[cfg(feature = "automation")]
#[error("State machine error: {0}")]
StateMachine(#[from] StateMachineError),
#[error("RPC authentication error: {0}")]
RPCAuthError(#[from] VerificationError),
// Shared error messages
#[error("Unsupported network")]
UnsupportedNetwork,
#[error("Invalid configuration: {0}")]
ConfigError(String),
#[error("Missing environment variable {1}: {0}")]
EnvVarNotSet(std::env::VarError, &'static str),
#[error("Environment variable {0} is malformed: {1}")]
EnvVarMalformed(&'static str, String),
#[error("Failed to convert between integer types")]
IntConversionError,
#[error("Failed to encode/decode data using borsh")]
BorshError,
#[error("Operator x-only public key {0} was not found in the DB")]
OperatorNotFound(XOnlyPublicKey),
#[error("Verifier with public key {0} was not found among the verifier clients")]
VerifierNotFound(PublicKey),
#[error("Deposit not found in DB: {0:?}")]
DepositNotFound(OutPoint),
#[error("Deposit is invalid")]
InvalidDeposit,
#[error("Operator data mismatch. Data already stored in DB and received by set_operator doesn't match for xonly_pk: {0}")]
OperatorDataMismatch(XOnlyPublicKey),
#[error("Deposit data mismatch. Data already stored in DB doesn't match the new data for deposit {0:?}")]
DepositDataMismatch(OutPoint),
#[error("Operator winternitz public keys mismatch. Data already stored in DB doesn't match the new data for operator {0}")]
OperatorWinternitzPublicKeysMismatch(XOnlyPublicKey),
#[error("BitVM setup data mismatch. Data already stored in DB doesn't match the new data for operator {0} and deposit {1:?}")]
BitvmSetupDataMismatch(XOnlyPublicKey, OutPoint),
#[error("Operator challenge ack hashes mismatch. Data already stored in DB doesn't match the new data for operator {0} and deposit {1:?}")]
OperatorChallengeAckHashesMismatch(XOnlyPublicKey, OutPoint),
#[error("Invalid BitVM public keys")]
InvalidBitVMPublicKeys,
#[error("Invalid challenge ack hashes")]
InvalidChallengeAckHashes,
#[error("Invalid operator index")]
InvalidOperatorIndex,
#[error("Invalid protocol paramset")]
InvalidProtocolParamset,
#[error("Deposit already signed and move txid {0} is in chain")]
DepositAlreadySigned(Txid),
// External crate error wrappers
#[error("Failed to call database: {0}")]
DatabaseError(#[from] sqlx::Error),
#[error("Failed to convert hex string: {0}")]
FromHexError(#[from] FromHexError),
#[error("Failed to convert to hash from slice: {0}")]
FromSliceError(#[from] bitcoin::hashes::FromSliceError),
#[error("Error while calling EVM contract: {0}")]
AlloyContract(#[from] alloy::contract::Error),
#[error("Error while calling EVM RPC function: {0}")]
AlloyRpc(#[from] alloy::transports::RpcError<alloy::transports::TransportErrorKind>),
#[error("Error while encoding/decoding EVM type: {0}")]
AlloySolTypes(#[from] alloy::sol_types::Error),
#[error("{0}")]
CLIDisplayAndExit(StyledStr),
#[error(transparent)]
RPC(#[from] Status),
// Base wrapper for eyre
#[error(transparent)]
Eyre(#[from] eyre::Report),
}
/// Extension traits for errors to easily convert them to eyre::Report and
/// tonic::Status through BridgeError.
pub trait ErrorExt: Sized {
/// Converts the error into an eyre::Report, first wrapping in
/// BridgeError if necessary. It does not rewrap in eyre::Report if
/// the given error is already an eyre::Report.
fn into_eyre(self) -> eyre::Report;
/// Converts the error into a tonic::Status. Currently defaults to
/// tonic::Status::from_error which will walk the error chain and attempt to
/// find a [`tonic::Status`] in the chain. If it can't find one, it will
/// return an Status::unknown with the Display representation of the error.
///
/// TODO: We should change the implementation to walk the chain of errors
/// and return the first [`TryInto<tonic::Status>`] error. This is
/// impossible to do dynamically, each error must be included in the match
/// arms of the conversion logic.
fn into_status(self) -> tonic::Status;
}
/// Extension traits for results to easily convert them to eyre::Report and
/// tonic::Status through BridgeError.
pub trait ResultExt: Sized {
type Output;
fn map_to_eyre(self) -> Result<Self::Output, eyre::Report>;
fn map_to_status(self) -> Result<Self::Output, tonic::Status>;
}
impl<T: Into<BridgeError>> ErrorExt for T {
fn into_eyre(self) -> eyre::Report {
match self.into() {
BridgeError::Eyre(report) => report,
other => eyre::eyre!(other),
}
}
fn into_status(self) -> tonic::Status {
self.into().into()
}
}
impl<U: Sized, T: Into<BridgeError>> ResultExt for Result<U, T> {
type Output = U;
fn map_to_eyre(self) -> Result<Self::Output, eyre::Report> {
self.map_err(ErrorExt::into_eyre)
}
fn map_to_status(self) -> Result<Self::Output, tonic::Status> {
self.map_err(ErrorExt::into_status)
}
}
impl From<BridgeError> for tonic::Status {
fn from(val: BridgeError) -> Self {
let eyre_report = val.into_eyre();
// eyre::Report can cast any error in the chain to a Status, so we use its downcast method to get the first Status.
eyre_report.downcast::<Status>().unwrap_or_else(|report| {
// We don't want this case to happen, all casts to Status should contain a Status that contains a user-facing error message.
tracing::error!(
"Returning internal error on RPC call, full error: {:?}",
report
);
let mut status = tonic::Status::internal(report.to_string());
status.set_source(Into::into(
Into::<Box<dyn std::error::Error + Send + Sync>>::into(report),
));
status
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_downcast() {
assert_eq!(
BridgeError::IntConversionError
.into_eyre()
.wrap_err("Some other error")
.into_eyre()
.wrap_err("some other")
.downcast_ref::<BridgeError>()
.unwrap()
.to_string(),
BridgeError::IntConversionError.to_string()
);
}
#[test]
fn test_status_in_chain_cast_properly() {
let err: BridgeError = eyre::eyre!("Some problem")
.wrap_err(tonic::Status::deadline_exceeded("Some timer expired"))
.wrap_err("Something else went wrong")
.into();
let status: Status = err.into_status();
assert_eq!(status.code(), tonic::Code::DeadlineExceeded);
assert_eq!(status.message(), "Some timer expired");
}
}