migrate from anyhow to thiserror (#197)

* migrate from anyhow to thiserror (#190). pending polish error msgs

* Add backtrace and compartmentalize errors

- Include backtraces in the errors we generate.  To get this we can't
  just return a literal enum, because the backtrace requires a call.
- Related to the previous point: add methods to create errors so
  we can include the backtrace conveniently without changing too much
  the syntax.  So instead of `Err(Error::KeyNotFound(key))` (literal
  enum) it will be `Err(Error::key_not_found(key))` (method call)
- Each error should be local to its scope, and each scope should
  only return its own error.
  - The merkle tree should return `TreeError` and not Error
  - The middleware should return `MiddlewareError` and not Error
- With a global Error we can't easily include backend/frontend types in
  the error fields, so declare a `BackendError` and a `FrontendError`
  and follow the pattern from the previous point
- The Pod traits should be able to return backend errors and will be
  used in the frontend; for that we change them to use trait object
  Error: `dyn std::error::Error`

* fix error

* apply suggestions from @arnaucube

* rename XError and XResult to Error and Result

* reorg signature

* make frontend custom error more ergonomic

* remove unnecessary feature

---------

Co-authored-by: Eduard S. <eduardsanou@posteo.net>
This commit is contained in:
arnaucube 2025-04-22 15:07:04 +02:00 committed by GitHub
parent 58d3c6a236
commit 29545f03fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 696 additions and 273 deletions

View file

@ -1,11 +1,10 @@
#![allow(unused)]
use std::{collections::HashMap, fmt, hash as h, iter, iter::zip, sync::Arc};
use anyhow::{anyhow, Result};
use schemars::JsonSchema;
use crate::{
frontend::{AnchoredKey, Statement, StatementArg},
frontend::{AnchoredKey, Error, Result, Statement, StatementArg},
middleware::{
self, hash_str, CustomPredicate, CustomPredicateBatch, Key, KeyOrWildcard, NativePredicate,
Params, PodId, Predicate, StatementTmpl, StatementTmplArg, ToFields, Value, Wildcard,
@ -131,17 +130,17 @@ impl CustomPredicateBatchBuilder {
sts: &[StatementTmplBuilder],
) -> Result<Predicate> {
if args.len() > params.max_statement_args {
return Err(anyhow!(
"args.len {} is over the limit {}",
return Err(Error::max_length(
"args.len".to_string(),
args.len(),
params.max_statement_args
params.max_statement_args,
));
}
if (args.len() + priv_args.len()) > params.max_custom_predicate_wildcards {
return Err(anyhow!(
"wildcards.len {} is over the limit {}",
return Err(Error::max_length(
"wildcards.len".to_string(),
args.len() + priv_args.len(),
params.max_custom_predicate_wildcards
params.max_custom_predicate_wildcards,
));
}

63
src/frontend/error.rs Normal file
View file

@ -0,0 +1,63 @@
use std::{backtrace::Backtrace, fmt::Debug};
use crate::middleware::{DynError, Statement, StatementTmpl};
pub type Result<T, E = Error> = core::result::Result<T, E>;
#[derive(thiserror::Error, Debug)]
pub enum InnerError {
#[error("{0} {1} is over the limit {2}")]
MaxLength(String, usize, usize),
#[error("{0} doesn't match {1}")]
StatementsDontMatch(Statement, StatementTmpl),
#[error("invalid arguments to {0} operation")]
OpInvalidArgs(String),
// Other
#[error("{0}")]
Custom(String),
}
#[derive(thiserror::Error)]
pub enum Error {
#[error("Inner: {inner}\n{backtrace}")]
Inner {
inner: Box<InnerError>,
backtrace: Box<Backtrace>,
},
#[error(transparent)]
Infallible(#[from] std::convert::Infallible),
#[error(transparent)]
Backend(#[from] Box<DynError>),
#[error(transparent)]
Middleware(#[from] crate::middleware::Error),
}
impl Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
macro_rules! new {
($inner:expr) => {
Error::Inner {
inner: Box::new($inner),
backtrace: Box::new(Backtrace::capture()),
}
};
}
use InnerError::*;
impl Error {
pub(crate) fn custom(s: impl Into<String>) -> Self {
new!(Custom(s.into()))
}
pub(crate) fn op_invalid_args(s: String) -> Self {
new!(OpInvalidArgs(s))
}
pub(crate) fn statements_dont_match(s0: Statement, s1: StatementTmpl) -> Self {
new!(StatementsDontMatch(s0, s1))
}
pub(crate) fn max_length(obj: String, found: usize, expect: usize) -> Self {
new!(MaxLength(obj, found, expect))
}
}

View file

@ -3,7 +3,6 @@
use std::{collections::HashMap, convert::From, fmt};
use anyhow::{anyhow, Result};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
@ -14,9 +13,11 @@ use crate::middleware::{
};
mod custom;
mod error;
mod operation;
mod serialization;
pub use custom::*;
pub use error::*;
pub use operation::*;
use serialization::*;
@ -101,7 +102,7 @@ impl SignedPod {
self.pod.id()
}
pub fn verify(&self) -> Result<()> {
self.pod.verify()
self.pod.verify().map_err(Error::Backend)
}
pub fn kvs(&self) -> &HashMap<Key, Value> {
&self.kvs
@ -276,11 +277,17 @@ impl MainPodBuilder {
let container =
op.1.get(0)
.and_then(|arg| arg.value())
.ok_or(anyhow!("Invalid container argument for op {}.", op))?;
.ok_or(Error::custom(format!(
"Invalid container argument for op {}.",
op
)))?;
let key =
op.1.get(1)
.and_then(|arg| arg.value())
.ok_or(anyhow!("Invalid key argument for op {}.", op))?;
.ok_or(Error::custom(format!(
"Invalid key argument for op {}.",
op
)))?;
let proof = if op_type == &Native(ContainsFromEntries) {
container.prove_existence(key)?.1
} else {
@ -292,7 +299,7 @@ impl MainPodBuilder {
}
}
fn op(&mut self, public: bool, op: Operation) -> Result<Statement, anyhow::Error> {
fn op(&mut self, public: bool, op: Operation) -> Result<Statement> {
use NativeOperation::*;
let mut op = Self::fill_in_aux(Self::lower_op(op))?;
let Operation(op_type, ref mut args, _) = &mut op;
@ -301,7 +308,7 @@ impl MainPodBuilder {
// We are dealing with a copy here.
match (args).first() {
Some(OperationArg::Statement(s)) if args.len() == 1 => Ok(s.predicate().clone()),
_ => Err(anyhow!("Invalid arguments to copy operation: {:?}", args)),
_ => Err(Error::op_invalid_args("copy".to_string())),
}
})?;
@ -312,7 +319,7 @@ impl MainPodBuilder {
CopyStatement => match &args[0] {
OperationArg::Statement(s) => s.args().clone(),
_ => {
return Err(anyhow!("Invalid arguments to copy operation: {}", op));
return Err(Error::op_invalid_args("copy".to_string()));
}
},
EqualFromEntries => self.op_args_entries(public, args)?,
@ -331,14 +338,14 @@ impl MainPodBuilder {
if ak1 == ak2 {
vec![StatementArg::Key(ak0), StatementArg::Key(ak3)]
} else {
return Err(anyhow!(
"Invalid arguments to transitive equality operation"
return Err(Error::op_invalid_args(
"transitivity equality".to_string(),
));
}
}
_ => {
return Err(anyhow!(
"Invalid arguments to transitive equality operation"
return Err(Error::op_invalid_args(
"transitivity equality".to_string(),
));
}
}
@ -348,7 +355,7 @@ impl MainPodBuilder {
vec![StatementArg::Key(ak0), StatementArg::Key(ak1)]
}
_ => {
return Err(anyhow!("Invalid arguments to gt-to-neq operation"));
return Err(Error::op_invalid_args("gt-to-neq".to_string()));
}
},
LtToNotEqual => match args[0].clone() {
@ -356,7 +363,7 @@ impl MainPodBuilder {
vec![StatementArg::Key(ak0), StatementArg::Key(ak1)]
}
_ => {
return Err(anyhow!("Invalid arguments to lt-to-neq operation"));
return Err(Error::op_invalid_args("lt-to-neq".to_string()));
}
},
SumOf => match (args[0].clone(), args[1].clone(), args[2].clone()) {
@ -375,11 +382,11 @@ impl MainPodBuilder {
StatementArg::Key(ak2),
]
} else {
return Err(anyhow!("Invalid arguments to sum-of operation"));
return Err(Error::op_invalid_args("sum-of".to_string()));
}
}
_ => {
return Err(anyhow!("Invalid arguments to sum-of operation"));
return Err(Error::op_invalid_args("sum-of".to_string()));
}
},
ProductOf => match (args[0].clone(), args[1].clone(), args[2].clone()) {
@ -398,11 +405,11 @@ impl MainPodBuilder {
StatementArg::Key(ak2),
]
} else {
return Err(anyhow!("Invalid arguments to product-of operation"));
return Err(Error::op_invalid_args("product-of".to_string()));
}
}
_ => {
return Err(anyhow!("Invalid arguments to product-of operation"));
return Err(Error::op_invalid_args("product-of".to_string()));
}
},
MaxOf => match (args[0].clone(), args[1].clone(), args[2].clone()) {
@ -421,11 +428,11 @@ impl MainPodBuilder {
StatementArg::Key(ak2),
]
} else {
return Err(anyhow!("Invalid arguments to max-of operation"));
return Err(Error::op_invalid_args("max-of".to_string()));
}
}
_ => {
return Err(anyhow!("Invalid arguments to max-of operation"));
return Err(Error::op_invalid_args("max-of".to_string()));
}
},
ContainsFromEntries => self.op_args_entries(public, args)?,
@ -441,17 +448,17 @@ impl MainPodBuilder {
OperationType::Custom(cpr) => {
let pred = &cpr.batch.predicates[cpr.index];
if pred.statements.len() != args.len() {
return Err(anyhow!(
return Err(Error::custom(format!(
"Custom predicate operation needs {} statements but has {}.",
pred.statements.len(),
args.len()
));
)));
}
// All args should be statements to be pattern matched against statement templates.
let args = args.iter().map(
|a| match a {
OperationArg::Statement(s) => Ok(s.clone()),
_ => Err(anyhow!("Invalid argument {} to operation corresponding to custom predicate {:?}.", a, cpr))
_ => Err(Error::custom(format!("Invalid argument {} to operation corresponding to custom predicate {:?}.", a, cpr)))
}
).collect::<Result<Vec<_>>>()?;
@ -462,7 +469,7 @@ impl MainPodBuilder {
for (st_tmpl_arg, st_arg) in st_tmpl.args.iter().zip(&st_args) {
if !check_st_tmpl(st_tmpl_arg, st_arg, &mut wildcard_map) {
// TODO: Add wildcard_map in the error for better context
return Err(anyhow!("{} doesn't match {}", st, st_tmpl));
return Err(Error::statements_dont_match(st.clone(), st_tmpl.clone()));
}
}
}
@ -563,7 +570,11 @@ impl MainPodBuilder {
}
_ => None,
})
.ok_or(anyhow!("Missing POD type information in POD: {:?}", pod))?;
.ok_or(Error::custom(format!(
// TODO use a specific Error
"Missing POD type information in POD: {:?}",
pod
)))?;
// Replace instances of `SELF` with the POD ID for consistency
// with `pub_statements` method.
let public_statements = [type_statement]
@ -676,7 +687,7 @@ impl MainPodCompiler {
op.1.iter()
.flat_map(|arg| self.compile_op_arg(arg))
.collect_vec();
middleware::Operation::op(op.0.clone(), &mop_args, &op.2)
Ok(middleware::Operation::op(op.0.clone(), &mop_args, &op.2)?)
}
fn compile_st_op(&mut self, st: &Statement, op: &Operation, params: &Params) -> Result<()> {
@ -684,11 +695,10 @@ impl MainPodCompiler {
let is_correct = middle_op.check(params, st)?;
if !is_correct {
// todo: improve error handling
Err(anyhow!(
Err(Error::custom(format!(
"Compile failed due to invalid deduction:\n {} ⇏ {}",
middle_op,
st
))
middle_op, st
)))
} else {
self.push_st_op(st.clone(), middle_op);
Ok(())
@ -822,11 +832,10 @@ pub mod tests {
if kvs == embedded_kvs {
Ok(())
} else {
Err(anyhow!(
Err(Error::custom(format!(
"KVs {:?} do not agree with those embedded in the POD: {:?}",
kvs,
embedded_kvs
))
kvs, embedded_kvs
)))
}
}

View file

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use crate::{
backends::plonky2::mock::{mainpod::MockMainPod, signedpod::MockSignedPod},
frontend::{MainPod, SignedPod, Statement},
frontend::{Error, MainPod, SignedPod, Statement},
middleware::{containers::Dictionary, Key, PodId, Value},
};
@ -20,14 +20,14 @@ pub struct SignedPodHelper {
}
impl TryFrom<SignedPodHelper> for SignedPod {
type Error = anyhow::Error;
type Error = Error;
fn try_from(helper: SignedPodHelper) -> Result<SignedPod, Self::Error> {
if helper.pod_class != "Signed" {
return Err(anyhow::anyhow!("pod_class is not Signed"));
return Err(Error::custom("pod_class is not Signed"));
}
if helper.pod_type != "Mock" {
return Err(anyhow::anyhow!("pod_type is not Mock"));
return Err(Error::custom("pod_type is not Mock"));
}
let dict = Dictionary::new(helper.entries.clone())?.clone();
@ -62,18 +62,18 @@ pub struct MainPodHelper {
}
impl TryFrom<MainPodHelper> for MainPod {
type Error = anyhow::Error; // or you can create a custom error type
type Error = Error; // or you can create a custom error type
fn try_from(helper: MainPodHelper) -> Result<Self, Self::Error> {
if helper.pod_class != "Main" {
return Err(anyhow::anyhow!("pod_class is not Main"));
return Err(Error::custom("pod_class is not Main"));
}
if helper.pod_type != "Mock" {
return Err(anyhow::anyhow!("pod_type is not Mock"));
return Err(Error::custom("pod_type is not Mock"));
}
let pod = MockMainPod::deserialize(helper.proof)
.map_err(|e| anyhow::anyhow!("Failed to deserialize proof: {}", e))?;
.map_err(|e| Error::custom(format!("Failed to deserialize proof: {}", e)))?;
Ok(MainPod {
pod: Box::new(pod),
@ -97,12 +97,10 @@ impl From<MainPod> for MainPodHelper {
mod tests {
use std::collections::HashSet;
use anyhow::Result;
// Pretty assertions give nicer diffs between expected and actual values
use pretty_assertions::assert_eq;
use schemars::schema_for;
// use schemars::generate::SchemaSettings;
use super::*;
use crate::{
backends::plonky2::mock::{mainpod::MockProver, signedpod::MockSigner},
@ -110,7 +108,7 @@ mod tests {
eth_dos_pod_builder, eth_friend_signed_pod_builder, zu_kyc_pod_builder,
zu_kyc_sign_pod_builders,
},
frontend::SignedPodBuilder,
frontend::{Result, SignedPodBuilder},
middleware::{
self,
containers::{Array, Set},