Skip to content

Commit 936eadd

Browse files
authored
Merge pull request #3416 from IntersectMBO/hjeljeli32/3382-typed-circuit-errors
refactor(stm): refactor error handling from Midnight library
2 parents a8b7c9c + 976d808 commit 936eadd

16 files changed

Lines changed: 191 additions & 178 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mithril-common/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ fixed = "1.31.0"
4646
hex = { workspace = true }
4747
kes-summed-ed25519 = { version = "0.2.1", features = ["serde_enabled", "sk_clone_enabled"] }
4848
mithril-merkle-tree = { path = "../internal/mithril-merkle-tree", version = "0.1.4" }
49-
mithril-stm = { path = "../mithril-stm", version = "0.11.1", default-features = false }
49+
mithril-stm = { path = "../mithril-stm", version = "0.11.2", default-features = false }
5050
nom = "8.0.0"
5151
rand_chacha = { workspace = true }
5252
rand_core = { workspace = true }

mithril-stm/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## 0.11.2 (07-20-2026)
9+
10+
### Changed
11+
12+
- Typed the non-recursive (certificate) circuit's `Relation` error boundary: `StmCertificateCircuit::Error` is now `StmCircuitError` (with a `Backend(String)` catch-all and a `From<plonk::Error>` conversion) instead of `plonk::Error`, removing the `to_synthesis_error` flattening adapter so domain-guard errors stay typed end-to-end
13+
814
## 0.11.1 (07-20-2026)
915

1016
### Changed

mithril-stm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mithril-stm"
3-
version = "0.11.1"
3+
version = "0.11.2"
44
edition = { workspace = true }
55
authors = { workspace = true }
66
homepage = { workspace = true }

mithril-stm/src/circuits/halo2/circuit.rs

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
use anyhow::{Context, anyhow};
1+
use anyhow::Context;
22
use ff::Field;
33
use group::Group;
44
use midnight_circuits::ecc::curves::CircuitCurve as CircuitCurveTrait;
55
use midnight_circuits::instructions::{AssignmentInstructions, PublicInputInstructions};
66
use midnight_circuits::types::{AssignedNative, AssignedNativePoint};
77
use midnight_proofs::circuit::{Layouter, Value};
8-
use midnight_proofs::plonk::Error;
98
use midnight_zk_stdlib::{Relation, ZkStdLib, ZkStdLibArch};
109

11-
use crate::circuits::halo2::errors::{StmCircuitError, to_synthesis_error};
10+
use crate::circuits::halo2::errors::StmCircuitError;
1211
use crate::circuits::halo2::gadgets::{
1312
LOTTERY_BIT_BOUND, MerklePathInputs, UniqueSchnorrSignatureInputs,
1413
assert_lottery_index_in_bounds, assert_lottery_won, assert_strictly_increasing_lottery_index,
@@ -48,13 +47,13 @@ impl StmCertificateCircuit {
4847
///
4948
/// Enforces `k < m <= 2^LOTTERY_BIT_BOUND - 1`, returning
5049
/// `StmCircuitError::InvalidCircuitParameters` when violated.
51-
pub(crate) fn validate_parameters(&self) -> StmResult<()> {
50+
pub(crate) fn validate_parameters(&self) -> Result<(), StmCircuitError> {
5251
let max_m = (1u32 << LOTTERY_BIT_BOUND) - 1;
5352
if self.k >= self.m || self.m > max_m {
54-
return Err(anyhow!(StmCircuitError::InvalidCircuitParameters {
53+
return Err(StmCircuitError::InvalidCircuitParameters {
5554
k: self.k,
5655
m: self.m,
57-
}));
56+
});
5857
}
5958

6059
Ok(())
@@ -64,13 +63,13 @@ impl StmCertificateCircuit {
6463
///
6564
/// This precondition prevents shape mismatches; failures return
6665
/// `StmCircuitError::WitnessLengthMismatch`.
67-
pub(crate) fn validate_witness_length(&self, actual: usize) -> StmResult<()> {
66+
pub(crate) fn validate_witness_length(&self, actual: usize) -> Result<(), StmCircuitError> {
6867
let expected_k = self.k as usize;
6968
if actual != expected_k {
70-
return Err(anyhow!(StmCircuitError::WitnessLengthMismatch {
69+
return Err(StmCircuitError::WitnessLengthMismatch {
7170
expected_k: self.k,
7271
actual: Self::checked_len_u32(actual),
73-
}));
72+
});
7473
}
7574

7675
Ok(())
@@ -80,20 +79,20 @@ impl StmCertificateCircuit {
8079
///
8180
/// The circuit uses [`LOTTERY_BIT_BOUND`]-bit comparison constraints, so each
8281
/// index must fit in that range and must satisfy `index < m`.
83-
pub(crate) fn validate_lottery_index(&self, index: LotteryIndex) -> StmResult<()> {
82+
pub(crate) fn validate_lottery_index(
83+
&self,
84+
index: LotteryIndex,
85+
) -> Result<(), StmCircuitError> {
8486
let max_supported = ((1u64 << LOTTERY_BIT_BOUND) - 1) as LotteryIndex;
8587
if index > max_supported {
86-
return Err(anyhow!(StmCircuitError::LotteryIndexTooLarge {
88+
return Err(StmCircuitError::LotteryIndexTooLarge {
8789
index,
8890
max_supported,
89-
}));
91+
});
9092
}
9193

9294
if index >= self.m as LotteryIndex {
93-
return Err(anyhow!(StmCircuitError::LotteryIndexOutOfBounds {
94-
index,
95-
m: self.m,
96-
}));
95+
return Err(StmCircuitError::LotteryIndexOutOfBounds { index, m: self.m });
9796
}
9897

9998
Ok(())
@@ -103,13 +102,16 @@ impl StmCertificateCircuit {
103102
///
104103
/// This guards against inconsistent witness paths and returns
105104
/// `StmCircuitError::MerkleSiblingLengthMismatch` on mismatch.
106-
pub(crate) fn validate_merkle_sibling_length(&self, actual: usize) -> StmResult<()> {
105+
pub(crate) fn validate_merkle_sibling_length(
106+
&self,
107+
actual: usize,
108+
) -> Result<(), StmCircuitError> {
107109
let expected_depth = self.merkle_tree_depth as usize;
108110
if actual != expected_depth {
109-
return Err(anyhow!(StmCircuitError::MerkleSiblingLengthMismatch {
111+
return Err(StmCircuitError::MerkleSiblingLengthMismatch {
110112
expected_depth: self.merkle_tree_depth,
111113
actual: Self::checked_len_u32(actual),
112-
}));
114+
});
113115
}
114116

115117
Ok(())
@@ -119,13 +121,16 @@ impl StmCertificateCircuit {
119121
///
120122
/// Under the current witness shape, this cannot fail independently from sibling-length
121123
/// validation because both lengths derive from `x.siblings`; returns `StmCircuitError::MerklePositionLengthMismatch`.
122-
pub(crate) fn validate_merkle_position_length(&self, actual: usize) -> StmResult<()> {
124+
pub(crate) fn validate_merkle_position_length(
125+
&self,
126+
actual: usize,
127+
) -> Result<(), StmCircuitError> {
123128
let expected_depth = self.merkle_tree_depth as usize;
124129
if actual != expected_depth {
125-
return Err(anyhow!(StmCircuitError::MerklePositionLengthMismatch {
130+
return Err(StmCircuitError::MerklePositionLengthMismatch {
126131
expected_depth: self.merkle_tree_depth,
127132
actual: Self::checked_len_u32(actual),
128-
}));
133+
});
129134
}
130135

131136
Ok(())
@@ -160,11 +165,11 @@ impl StmCertificateCircuit {
160165
}
161166

162167
impl Relation for StmCertificateCircuit {
163-
type Error = Error;
168+
type Error = StmCircuitError;
164169
type Instance = CircuitInstance;
165170
type Witness = CircuitWitness;
166171

167-
fn format_instance(instance: &Self::Instance) -> Result<Vec<CircuitBase>, Error> {
172+
fn format_instance(instance: &Self::Instance) -> Result<Vec<CircuitBase>, StmCircuitError> {
168173
Ok(vec![instance.0.into(), instance.1.into()])
169174
}
170175

@@ -174,17 +179,16 @@ impl Relation for StmCertificateCircuit {
174179
layouter: &mut impl Layouter<CircuitBase>,
175180
instance: Value<Self::Instance>,
176181
witness: Value<Self::Witness>,
177-
) -> Result<(), Error> {
178-
self.validate_parameters().map_err(to_synthesis_error)?;
182+
) -> Result<(), StmCircuitError> {
183+
self.validate_parameters()?;
179184
let witness = witness
180-
.map_with_result(|witness| -> StmResult<_> {
185+
.map_with_result(|witness| -> Result<_, StmCircuitError> {
181186
self.validate_witness_length(witness.len())?;
182187
witness
183188
.iter()
184189
.try_for_each(|entry| self.validate_lottery_index(entry.lottery_index))?;
185190
Ok(witness)
186-
})
187-
.map_err(to_synthesis_error)?
191+
})?
188192
.transpose_vec(self.k as usize);
189193

190194
let merkle_tree_commitment: AssignedNative<CircuitBase> =
@@ -278,6 +282,7 @@ impl Relation for StmCertificateCircuit {
278282
// m can be put as a public instance or a constant
279283
let m = std_lib.assign_fixed(layouter, CircuitBase::from(self.m as u64))?;
280284
assert_lottery_index_in_bounds(std_lib, layouter, &previous_lottery_index, &m)
285+
.map_err(StmCircuitError::from)
281286
}
282287

283288
fn used_chips(&self) -> ZkStdLibArch {

mithril-stm/src/circuits/halo2/errors.rs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use midnight_proofs::plonk::Error as PlonkError;
22
use thiserror::Error;
33

4-
use crate::StmError;
5-
64
/// Circuit-scoped errors for Halo2 STM validation and execution.
75
#[cfg_attr(not(test), allow(dead_code))]
86
#[derive(Debug, Error, Clone, PartialEq, Eq)]
@@ -118,19 +116,29 @@ pub enum StmCircuitError {
118116
/// Proof was generated but rejected by the verifier.
119117
#[error("Proof verification rejected")]
120118
VerificationRejected,
121-
}
122119

123-
/// Convert STM-layer errors to Midnight synthesis errors at relation boundaries.
124-
pub(crate) fn to_synthesis_error(error: StmError) -> PlonkError {
125-
let error = match error.downcast::<PlonkError>() {
126-
Ok(plonk_error) => return plonk_error,
127-
Err(error) => error,
128-
};
120+
/// A proving/verification backend error surfaced at the relation boundary.
121+
#[error("Backend error: {0}")]
122+
Backend(String),
123+
}
129124

130-
let error = match error.downcast::<StmCircuitError>() {
131-
Ok(stm_error) => return PlonkError::Synthesis(stm_error.to_string()),
132-
Err(error) => error,
133-
};
125+
impl From<PlonkError> for StmCircuitError {
126+
fn from(error: PlonkError) -> Self {
127+
Self::Backend(error.to_string())
128+
}
129+
}
134130

135-
PlonkError::Synthesis(error.to_string())
131+
#[cfg(test)]
132+
mod tests {
133+
use super::*;
134+
135+
#[test]
136+
fn plonk_error_converts_to_backend_variant() {
137+
let plonk_error = PlonkError::ConstraintSystemFailure;
138+
let expected_message = plonk_error.to_string();
139+
assert_eq!(
140+
StmCircuitError::from(plonk_error),
141+
StmCircuitError::Backend(expected_message),
142+
);
143+
}
136144
}

mithril-stm/src/circuits/halo2/gadgets/comparison.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use midnight_circuits::instructions::{BinaryInstructions, EqualityInstructions};
22
use midnight_circuits::types::{AssignedBit, AssignedNative};
33
use midnight_proofs::circuit::Layouter;
4-
use midnight_proofs::plonk::Error;
54
use midnight_zk_stdlib::ZkStdLib;
65

6+
use crate::circuits::halo2::errors::StmCircuitError;
77
use crate::circuits::halo2::gadgets::comparison_helpers::decompose_unsafe;
88
use crate::circuits::halo2::types::CircuitBase;
99

@@ -13,7 +13,7 @@ pub(super) fn lower_than_native(
1313
layouter: &mut impl Layouter<CircuitBase>,
1414
x: &AssignedNative<CircuitBase>,
1515
y: &AssignedNative<CircuitBase>,
16-
) -> Result<AssignedBit<CircuitBase>, Error> {
16+
) -> Result<AssignedBit<CircuitBase>, StmCircuitError> {
1717
let (x_low_assigned, x_high_assigned) = decompose_unsafe(std_lib, layouter, x)?;
1818
let (y_low_assigned, y_high_assigned) = decompose_unsafe(std_lib, layouter, y)?;
1919

@@ -22,13 +22,16 @@ pub(super) fn lower_than_native(
2222
let is_less_high = std_lib.lower_than(layouter, &x_high_assigned, &y_high_assigned, 128)?;
2323

2424
let low_less = std_lib.and(layouter, &[is_equal_high, is_less_low])?;
25-
std_lib.or(layouter, &[is_less_high, low_less])
25+
std_lib
26+
.or(layouter, &[is_less_high, low_less])
27+
.map_err(StmCircuitError::from)
2628
}
2729

2830
#[cfg(test)]
2931
mod tests {
3032
use midnight_circuits::instructions::AssignmentInstructions;
3133

34+
use crate::circuits::halo2::errors::StmCircuitError;
3235
use crate::circuits::halo2::tests::test_helpers::{
3336
assert_relation_rejected, comparison_used_chips, impl_focused_test_relation,
3437
prove_and_verify_relation,
@@ -42,13 +45,14 @@ mod tests {
4245
impl_focused_test_relation!(
4346
ComparisonLessThanRelation,
4447
ComparisonWitness,
48+
error = StmCircuitError,
4549
comparison_used_chips(),
4650
|std_lib, layouter, witness| {
4751
let x = std_lib.assign(layouter, witness.map(|(x, _)| x.into()))?;
4852
let y = std_lib.assign(layouter, witness.map(|(_, y)| y.into()))?;
4953

5054
let is_less = lower_than_native(std_lib, layouter, &x, &y)?;
51-
std_lib.assert_true(layouter, &is_less)
55+
std_lib.assert_true(layouter, &is_less).map_err(StmCircuitError::from)
5256
}
5357
);
5458

mithril-stm/src/circuits/halo2/gadgets/comparison_helpers.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use anyhow::anyhow;
21
use ff::{Field, PrimeField};
32
use midnight_circuits::instructions::{
43
ArithInstructions, AssertionInstructions, AssignmentInstructions, DecompositionInstructions,
@@ -10,22 +9,20 @@ use midnight_zk_stdlib::ZkStdLib;
109
use num_bigint::BigUint;
1110
use num_traits::{Num, One};
1211

13-
use crate::StmResult;
1412
use crate::circuits::halo2::errors::StmCircuitError;
15-
use crate::circuits::halo2::errors::to_synthesis_error;
1613
use crate::circuits::halo2::types::CircuitBase;
1714

1815
/// Splits a field element into `(lower, upper)` limbs at `num_bits` using LE encoding.
1916
pub(super) fn split_field_element_into_le_limbs<Fp: PrimeField>(
2017
value: &Fp,
2118
num_bits: u32,
22-
) -> StmResult<(Fp, Fp)> {
19+
) -> Result<(Fp, Fp), StmCircuitError> {
2320
let field_bits = Fp::NUM_BITS;
2421
if num_bits >= field_bits {
25-
return Err(anyhow!(StmCircuitError::InvalidBitDecompositionRange {
22+
return Err(StmCircuitError::InvalidBitDecompositionRange {
2623
num_bits,
2724
field_bits,
28-
}));
25+
});
2926
}
3027

3128
let value_big = BigUint::from_bytes_le(value.to_repr().as_ref());
@@ -38,17 +35,19 @@ pub(super) fn split_field_element_into_le_limbs<Fp: PrimeField>(
3835
}
3936

4037
/// Parses the prime-field modulus into a `BigUint` for limb splitting and reduction helpers.
41-
fn field_modulus_as_biguint<Fp: PrimeField>() -> StmResult<BigUint> {
38+
fn field_modulus_as_biguint<Fp: PrimeField>() -> Result<BigUint, StmCircuitError> {
4239
BigUint::from_str_radix(&Fp::MODULUS[2..], 16)
43-
.map_err(|_| anyhow!(StmCircuitError::FieldModulusParseFailed))
40+
.map_err(|_| StmCircuitError::FieldModulusParseFailed)
4441
}
4542

4643
/// Reduces a non-negative integer modulo the field modulus and converts it into a field element.
47-
fn big_unsigned_integer_to_field_element<Fp: PrimeField>(e: BigUint) -> StmResult<Fp> {
44+
fn big_unsigned_integer_to_field_element<Fp: PrimeField>(
45+
e: BigUint,
46+
) -> Result<Fp, StmCircuitError> {
4847
let modulus = field_modulus_as_biguint::<Fp>()?;
4948
let e = e % modulus;
5049
Fp::from_str_vartime(&e.to_str_radix(10)[..])
51-
.ok_or_else(|| anyhow!(StmCircuitError::FieldElementConversionFailed))
50+
.ok_or(StmCircuitError::FieldElementConversionFailed)
5251
}
5352

5453
/// Constrains two assigned field elements to share the same least-significant-bit parity.
@@ -69,12 +68,11 @@ pub(super) fn decompose_unsafe(
6968
std_lib: &ZkStdLib,
7069
layouter: &mut impl Layouter<CircuitBase>,
7170
x: &AssignedNative<CircuitBase>,
72-
) -> Result<(AssignedNative<CircuitBase>, AssignedNative<CircuitBase>), Error> {
71+
) -> Result<(AssignedNative<CircuitBase>, AssignedNative<CircuitBase>), StmCircuitError> {
7372
let x_value = x.value();
7473
let base127 = CircuitBase::from_u128(1_u128 << 127);
7574
let (x_low, x_high) = x_value
76-
.map_with_result(|v| split_field_element_into_le_limbs(v, 127))
77-
.map_err(to_synthesis_error)?
75+
.map_with_result(|v| split_field_element_into_le_limbs(v, 127))?
7876
.unzip();
7977

8078
let x_low_assigned: AssignedNative<_> = std_lib.assign(layouter, x_low)?;

0 commit comments

Comments
 (0)