diff --git a/Cargo.lock b/Cargo.lock index 565f492b537..5250b0222ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4816,7 +4816,7 @@ dependencies = [ [[package]] name = "mithril-stm" -version = "0.11.1" +version = "0.11.2" dependencies = [ "anyhow", "blake2 0.10.6", diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index 2f55f725e43..f4d4d00ee34 100644 --- a/mithril-common/Cargo.toml +++ b/mithril-common/Cargo.toml @@ -46,7 +46,7 @@ fixed = "1.31.0" hex = { workspace = true } kes-summed-ed25519 = { version = "0.2.1", features = ["serde_enabled", "sk_clone_enabled"] } mithril-merkle-tree = { path = "../internal/mithril-merkle-tree", version = "0.1.4" } -mithril-stm = { path = "../mithril-stm", version = "0.11.1", default-features = false } +mithril-stm = { path = "../mithril-stm", version = "0.11.2", default-features = false } nom = "8.0.0" rand_chacha = { workspace = true } rand_core = { workspace = true } diff --git a/mithril-stm/CHANGELOG.md b/mithril-stm/CHANGELOG.md index 60c20cddf8f..eb44e089299 100644 --- a/mithril-stm/CHANGELOG.md +++ b/mithril-stm/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.11.2 (07-20-2026) + +### Changed + +- Typed the non-recursive (certificate) circuit's `Relation` error boundary: `StmCertificateCircuit::Error` is now `StmCircuitError` (with a `Backend(String)` catch-all and a `From` conversion) instead of `plonk::Error`, removing the `to_synthesis_error` flattening adapter so domain-guard errors stay typed end-to-end + ## 0.11.1 (07-20-2026) ### Changed diff --git a/mithril-stm/Cargo.toml b/mithril-stm/Cargo.toml index c286e026459..f421adfff97 100644 --- a/mithril-stm/Cargo.toml +++ b/mithril-stm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-stm" -version = "0.11.1" +version = "0.11.2" edition = { workspace = true } authors = { workspace = true } homepage = { workspace = true } diff --git a/mithril-stm/src/circuits/halo2/circuit.rs b/mithril-stm/src/circuits/halo2/circuit.rs index c2f03b33e61..d02dab0eb88 100644 --- a/mithril-stm/src/circuits/halo2/circuit.rs +++ b/mithril-stm/src/circuits/halo2/circuit.rs @@ -1,14 +1,13 @@ -use anyhow::{Context, anyhow}; +use anyhow::Context; use ff::Field; use group::Group; use midnight_circuits::ecc::curves::CircuitCurve as CircuitCurveTrait; use midnight_circuits::instructions::{AssignmentInstructions, PublicInputInstructions}; use midnight_circuits::types::{AssignedNative, AssignedNativePoint}; use midnight_proofs::circuit::{Layouter, Value}; -use midnight_proofs::plonk::Error; use midnight_zk_stdlib::{Relation, ZkStdLib, ZkStdLibArch}; -use crate::circuits::halo2::errors::{StmCircuitError, to_synthesis_error}; +use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::gadgets::{ LOTTERY_BIT_BOUND, MerklePathInputs, UniqueSchnorrSignatureInputs, assert_lottery_index_in_bounds, assert_lottery_won, assert_strictly_increasing_lottery_index, @@ -48,13 +47,13 @@ impl StmCertificateCircuit { /// /// Enforces `k < m <= 2^LOTTERY_BIT_BOUND - 1`, returning /// `StmCircuitError::InvalidCircuitParameters` when violated. - pub(crate) fn validate_parameters(&self) -> StmResult<()> { + pub(crate) fn validate_parameters(&self) -> Result<(), StmCircuitError> { let max_m = (1u32 << LOTTERY_BIT_BOUND) - 1; if self.k >= self.m || self.m > max_m { - return Err(anyhow!(StmCircuitError::InvalidCircuitParameters { + return Err(StmCircuitError::InvalidCircuitParameters { k: self.k, m: self.m, - })); + }); } Ok(()) @@ -64,13 +63,13 @@ impl StmCertificateCircuit { /// /// This precondition prevents shape mismatches; failures return /// `StmCircuitError::WitnessLengthMismatch`. - pub(crate) fn validate_witness_length(&self, actual: usize) -> StmResult<()> { + pub(crate) fn validate_witness_length(&self, actual: usize) -> Result<(), StmCircuitError> { let expected_k = self.k as usize; if actual != expected_k { - return Err(anyhow!(StmCircuitError::WitnessLengthMismatch { + return Err(StmCircuitError::WitnessLengthMismatch { expected_k: self.k, actual: Self::checked_len_u32(actual), - })); + }); } Ok(()) @@ -80,20 +79,20 @@ impl StmCertificateCircuit { /// /// The circuit uses [`LOTTERY_BIT_BOUND`]-bit comparison constraints, so each /// index must fit in that range and must satisfy `index < m`. - pub(crate) fn validate_lottery_index(&self, index: LotteryIndex) -> StmResult<()> { + pub(crate) fn validate_lottery_index( + &self, + index: LotteryIndex, + ) -> Result<(), StmCircuitError> { let max_supported = ((1u64 << LOTTERY_BIT_BOUND) - 1) as LotteryIndex; if index > max_supported { - return Err(anyhow!(StmCircuitError::LotteryIndexTooLarge { + return Err(StmCircuitError::LotteryIndexTooLarge { index, max_supported, - })); + }); } if index >= self.m as LotteryIndex { - return Err(anyhow!(StmCircuitError::LotteryIndexOutOfBounds { - index, - m: self.m, - })); + return Err(StmCircuitError::LotteryIndexOutOfBounds { index, m: self.m }); } Ok(()) @@ -103,13 +102,16 @@ impl StmCertificateCircuit { /// /// This guards against inconsistent witness paths and returns /// `StmCircuitError::MerkleSiblingLengthMismatch` on mismatch. - pub(crate) fn validate_merkle_sibling_length(&self, actual: usize) -> StmResult<()> { + pub(crate) fn validate_merkle_sibling_length( + &self, + actual: usize, + ) -> Result<(), StmCircuitError> { let expected_depth = self.merkle_tree_depth as usize; if actual != expected_depth { - return Err(anyhow!(StmCircuitError::MerkleSiblingLengthMismatch { + return Err(StmCircuitError::MerkleSiblingLengthMismatch { expected_depth: self.merkle_tree_depth, actual: Self::checked_len_u32(actual), - })); + }); } Ok(()) @@ -119,13 +121,16 @@ impl StmCertificateCircuit { /// /// Under the current witness shape, this cannot fail independently from sibling-length /// validation because both lengths derive from `x.siblings`; returns `StmCircuitError::MerklePositionLengthMismatch`. - pub(crate) fn validate_merkle_position_length(&self, actual: usize) -> StmResult<()> { + pub(crate) fn validate_merkle_position_length( + &self, + actual: usize, + ) -> Result<(), StmCircuitError> { let expected_depth = self.merkle_tree_depth as usize; if actual != expected_depth { - return Err(anyhow!(StmCircuitError::MerklePositionLengthMismatch { + return Err(StmCircuitError::MerklePositionLengthMismatch { expected_depth: self.merkle_tree_depth, actual: Self::checked_len_u32(actual), - })); + }); } Ok(()) @@ -160,11 +165,11 @@ impl StmCertificateCircuit { } impl Relation for StmCertificateCircuit { - type Error = Error; + type Error = StmCircuitError; type Instance = CircuitInstance; type Witness = CircuitWitness; - fn format_instance(instance: &Self::Instance) -> Result, Error> { + fn format_instance(instance: &Self::Instance) -> Result, StmCircuitError> { Ok(vec![instance.0.into(), instance.1.into()]) } @@ -174,17 +179,16 @@ impl Relation for StmCertificateCircuit { layouter: &mut impl Layouter, instance: Value, witness: Value, - ) -> Result<(), Error> { - self.validate_parameters().map_err(to_synthesis_error)?; + ) -> Result<(), StmCircuitError> { + self.validate_parameters()?; let witness = witness - .map_with_result(|witness| -> StmResult<_> { + .map_with_result(|witness| -> Result<_, StmCircuitError> { self.validate_witness_length(witness.len())?; witness .iter() .try_for_each(|entry| self.validate_lottery_index(entry.lottery_index))?; Ok(witness) - }) - .map_err(to_synthesis_error)? + })? .transpose_vec(self.k as usize); let merkle_tree_commitment: AssignedNative = @@ -278,6 +282,7 @@ impl Relation for StmCertificateCircuit { // m can be put as a public instance or a constant let m = std_lib.assign_fixed(layouter, CircuitBase::from(self.m as u64))?; assert_lottery_index_in_bounds(std_lib, layouter, &previous_lottery_index, &m) + .map_err(StmCircuitError::from) } fn used_chips(&self) -> ZkStdLibArch { diff --git a/mithril-stm/src/circuits/halo2/errors.rs b/mithril-stm/src/circuits/halo2/errors.rs index 650296f9191..33f4eb3a164 100644 --- a/mithril-stm/src/circuits/halo2/errors.rs +++ b/mithril-stm/src/circuits/halo2/errors.rs @@ -1,8 +1,6 @@ use midnight_proofs::plonk::Error as PlonkError; use thiserror::Error; -use crate::StmError; - /// Circuit-scoped errors for Halo2 STM validation and execution. #[cfg_attr(not(test), allow(dead_code))] #[derive(Debug, Error, Clone, PartialEq, Eq)] @@ -118,19 +116,29 @@ pub enum StmCircuitError { /// Proof was generated but rejected by the verifier. #[error("Proof verification rejected")] VerificationRejected, -} -/// Convert STM-layer errors to Midnight synthesis errors at relation boundaries. -pub(crate) fn to_synthesis_error(error: StmError) -> PlonkError { - let error = match error.downcast::() { - Ok(plonk_error) => return plonk_error, - Err(error) => error, - }; + /// A proving/verification backend error surfaced at the relation boundary. + #[error("Backend error: {0}")] + Backend(String), +} - let error = match error.downcast::() { - Ok(stm_error) => return PlonkError::Synthesis(stm_error.to_string()), - Err(error) => error, - }; +impl From for StmCircuitError { + fn from(error: PlonkError) -> Self { + Self::Backend(error.to_string()) + } +} - PlonkError::Synthesis(error.to_string()) +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn plonk_error_converts_to_backend_variant() { + let plonk_error = PlonkError::ConstraintSystemFailure; + let expected_message = plonk_error.to_string(); + assert_eq!( + StmCircuitError::from(plonk_error), + StmCircuitError::Backend(expected_message), + ); + } } diff --git a/mithril-stm/src/circuits/halo2/gadgets/comparison.rs b/mithril-stm/src/circuits/halo2/gadgets/comparison.rs index 2d9b06d9828..247b33ddd7e 100644 --- a/mithril-stm/src/circuits/halo2/gadgets/comparison.rs +++ b/mithril-stm/src/circuits/halo2/gadgets/comparison.rs @@ -1,9 +1,9 @@ use midnight_circuits::instructions::{BinaryInstructions, EqualityInstructions}; use midnight_circuits::types::{AssignedBit, AssignedNative}; use midnight_proofs::circuit::Layouter; -use midnight_proofs::plonk::Error; use midnight_zk_stdlib::ZkStdLib; +use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::gadgets::comparison_helpers::decompose_unsafe; use crate::circuits::halo2::types::CircuitBase; @@ -13,7 +13,7 @@ pub(super) fn lower_than_native( layouter: &mut impl Layouter, x: &AssignedNative, y: &AssignedNative, -) -> Result, Error> { +) -> Result, StmCircuitError> { let (x_low_assigned, x_high_assigned) = decompose_unsafe(std_lib, layouter, x)?; let (y_low_assigned, y_high_assigned) = decompose_unsafe(std_lib, layouter, y)?; @@ -22,13 +22,16 @@ pub(super) fn lower_than_native( let is_less_high = std_lib.lower_than(layouter, &x_high_assigned, &y_high_assigned, 128)?; let low_less = std_lib.and(layouter, &[is_equal_high, is_less_low])?; - std_lib.or(layouter, &[is_less_high, low_less]) + std_lib + .or(layouter, &[is_less_high, low_less]) + .map_err(StmCircuitError::from) } #[cfg(test)] mod tests { use midnight_circuits::instructions::AssignmentInstructions; + use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::tests::test_helpers::{ assert_relation_rejected, comparison_used_chips, impl_focused_test_relation, prove_and_verify_relation, @@ -42,13 +45,14 @@ mod tests { impl_focused_test_relation!( ComparisonLessThanRelation, ComparisonWitness, + error = StmCircuitError, comparison_used_chips(), |std_lib, layouter, witness| { let x = std_lib.assign(layouter, witness.map(|(x, _)| x.into()))?; let y = std_lib.assign(layouter, witness.map(|(_, y)| y.into()))?; let is_less = lower_than_native(std_lib, layouter, &x, &y)?; - std_lib.assert_true(layouter, &is_less) + std_lib.assert_true(layouter, &is_less).map_err(StmCircuitError::from) } ); diff --git a/mithril-stm/src/circuits/halo2/gadgets/comparison_helpers.rs b/mithril-stm/src/circuits/halo2/gadgets/comparison_helpers.rs index 41a9686f961..940d8b2b76b 100644 --- a/mithril-stm/src/circuits/halo2/gadgets/comparison_helpers.rs +++ b/mithril-stm/src/circuits/halo2/gadgets/comparison_helpers.rs @@ -1,4 +1,3 @@ -use anyhow::anyhow; use ff::{Field, PrimeField}; use midnight_circuits::instructions::{ ArithInstructions, AssertionInstructions, AssignmentInstructions, DecompositionInstructions, @@ -10,22 +9,20 @@ use midnight_zk_stdlib::ZkStdLib; use num_bigint::BigUint; use num_traits::{Num, One}; -use crate::StmResult; use crate::circuits::halo2::errors::StmCircuitError; -use crate::circuits::halo2::errors::to_synthesis_error; use crate::circuits::halo2::types::CircuitBase; /// Splits a field element into `(lower, upper)` limbs at `num_bits` using LE encoding. pub(super) fn split_field_element_into_le_limbs( value: &Fp, num_bits: u32, -) -> StmResult<(Fp, Fp)> { +) -> Result<(Fp, Fp), StmCircuitError> { let field_bits = Fp::NUM_BITS; if num_bits >= field_bits { - return Err(anyhow!(StmCircuitError::InvalidBitDecompositionRange { + return Err(StmCircuitError::InvalidBitDecompositionRange { num_bits, field_bits, - })); + }); } 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( } /// Parses the prime-field modulus into a `BigUint` for limb splitting and reduction helpers. -fn field_modulus_as_biguint() -> StmResult { +fn field_modulus_as_biguint() -> Result { BigUint::from_str_radix(&Fp::MODULUS[2..], 16) - .map_err(|_| anyhow!(StmCircuitError::FieldModulusParseFailed)) + .map_err(|_| StmCircuitError::FieldModulusParseFailed) } /// Reduces a non-negative integer modulo the field modulus and converts it into a field element. -fn big_unsigned_integer_to_field_element(e: BigUint) -> StmResult { +fn big_unsigned_integer_to_field_element( + e: BigUint, +) -> Result { let modulus = field_modulus_as_biguint::()?; let e = e % modulus; Fp::from_str_vartime(&e.to_str_radix(10)[..]) - .ok_or_else(|| anyhow!(StmCircuitError::FieldElementConversionFailed)) + .ok_or(StmCircuitError::FieldElementConversionFailed) } /// Constrains two assigned field elements to share the same least-significant-bit parity. @@ -69,12 +68,11 @@ pub(super) fn decompose_unsafe( std_lib: &ZkStdLib, layouter: &mut impl Layouter, x: &AssignedNative, -) -> Result<(AssignedNative, AssignedNative), Error> { +) -> Result<(AssignedNative, AssignedNative), StmCircuitError> { let x_value = x.value(); let base127 = CircuitBase::from_u128(1_u128 << 127); let (x_low, x_high) = x_value - .map_with_result(|v| split_field_element_into_le_limbs(v, 127)) - .map_err(to_synthesis_error)? + .map_with_result(|v| split_field_element_into_le_limbs(v, 127))? .unzip(); let x_low_assigned: AssignedNative<_> = std_lib.assign(layouter, x_low)?; diff --git a/mithril-stm/src/circuits/halo2/gadgets/lottery.rs b/mithril-stm/src/circuits/halo2/gadgets/lottery.rs index 1684659f91c..2cefd04c0c7 100644 --- a/mithril-stm/src/circuits/halo2/gadgets/lottery.rs +++ b/mithril-stm/src/circuits/halo2/gadgets/lottery.rs @@ -4,6 +4,7 @@ use midnight_proofs::circuit::Layouter; use midnight_proofs::plonk::Error; use midnight_zk_stdlib::ZkStdLib; +use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::gadgets::comparison::lower_than_native; use crate::circuits::halo2::types::{CircuitBase, CircuitCurve}; @@ -17,7 +18,7 @@ pub(crate) fn assert_lottery_won( commitment_point: &AssignedNativePoint, lottery_index: &AssignedNative, lottery_target_value: &AssignedNative, -) -> Result<(), Error> { +) -> Result<(), StmCircuitError> { let commitment_point_x = std_lib.jubjub().x_coordinate(commitment_point); let commitment_point_y = std_lib.jubjub().y_coordinate(commitment_point); let lottery_evaluation_value = std_lib.poseidon( @@ -35,7 +36,9 @@ pub(crate) fn assert_lottery_won( lottery_target_value, &lottery_evaluation_value, )?; - std_lib.assert_false(layouter, &is_less) + std_lib + .assert_false(layouter, &is_less) + .map_err(StmCircuitError::from) } /// Constrains the current lottery index to be strictly greater than the previous one. @@ -68,8 +71,10 @@ pub(crate) fn assert_lottery_index_in_bounds( #[cfg(test)] mod tests { use midnight_circuits::instructions::AssignmentInstructions; + use midnight_proofs::plonk::Error; use crate::LotteryIndex; + use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::tests::test_helpers::{ assert_relation_rejected, comparison_used_chips, impl_focused_test_relation, jubjub_poseidon_used_chips, prove_and_verify_relation, @@ -93,6 +98,7 @@ mod tests { impl_focused_test_relation!( LotteryWonRelation, LotteryWonWitness, + error = StmCircuitError, jubjub_poseidon_used_chips(), |std_lib, layouter, witness| { let lottery_prefix = @@ -121,6 +127,7 @@ mod tests { impl_focused_test_relation!( IncreasingIndexRelation, (LotteryIndex, LotteryIndex), + error = Error, comparison_used_chips(), |std_lib, layouter, witness| { let previous_lottery_index = std_lib.assign( @@ -144,6 +151,7 @@ mod tests { impl_focused_test_relation!( InBoundsIndexRelation, (LotteryIndex, u32), + error = Error, comparison_used_chips(), |std_lib, layouter, witness| { let lottery_index = diff --git a/mithril-stm/src/circuits/halo2/gadgets/merkle_path.rs b/mithril-stm/src/circuits/halo2/gadgets/merkle_path.rs index 7dbad1ad5ba..ba511c2297c 100644 --- a/mithril-stm/src/circuits/halo2/gadgets/merkle_path.rs +++ b/mithril-stm/src/circuits/halo2/gadgets/merkle_path.rs @@ -1,13 +1,11 @@ -use anyhow::anyhow; use midnight_circuits::instructions::{ AssertionInstructions, ControlFlowInstructions, EccInstructions, ZeroInstructions, }; use midnight_circuits::types::{AssignedBit, AssignedNative, AssignedNativePoint}; use midnight_proofs::circuit::Layouter; -use midnight_proofs::plonk::Error; use midnight_zk_stdlib::ZkStdLib; -use crate::circuits::halo2::errors::{StmCircuitError, to_synthesis_error}; +use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::types::{CircuitBase, CircuitCurve}; /// Assigned inputs required to verify one Merkle authentication path inside the circuit. @@ -31,7 +29,7 @@ pub(crate) fn verify_merkle_path( std_lib: &ZkStdLib, layouter: &mut impl Layouter, inputs: MerklePathInputs<'_>, -) -> Result<(), Error> { +) -> Result<(), StmCircuitError> { let verification_key_x = std_lib.jubjub().x_coordinate(inputs.verification_key); let verification_key_y = std_lib.jubjub().y_coordinate(inputs.verification_key); let leaf = std_lib.poseidon( @@ -77,22 +75,22 @@ pub(crate) fn verify_merkle_path( // // The first sibling can never be padding so there is not need to check for a 0 value. // This saves a few constraints per loop. - let first_sibling = inputs - .merkle_siblings - .first() - .ok_or(anyhow!(StmCircuitError::MerkleSiblingLengthMismatch { - expected_depth: inputs.merkle_tree_depth, - actual: 0, - })) - .map_err(to_synthesis_error)?; - let first_position = inputs - .merkle_positions - .first() - .ok_or(anyhow!(StmCircuitError::MerklePositionLengthMismatch { - expected_depth: inputs.merkle_tree_depth, - actual: 0, - })) - .map_err(to_synthesis_error)?; + let first_sibling = + inputs + .merkle_siblings + .first() + .ok_or(StmCircuitError::MerkleSiblingLengthMismatch { + expected_depth: inputs.merkle_tree_depth, + actual: 0, + })?; + let first_position = + inputs + .merkle_positions + .first() + .ok_or(StmCircuitError::MerklePositionLengthMismatch { + expected_depth: inputs.merkle_tree_depth, + actual: 0, + })?; let first_left = std_lib.select(layouter, first_position, &leaf, first_sibling)?; let first_right = std_lib.select(layouter, first_position, first_sibling, &leaf)?; let first_node = std_lib.poseidon(layouter, &[first_left, first_right])?; @@ -111,7 +109,9 @@ pub(crate) fn verify_merkle_path( std_lib.select(layouter, &is_zero, &acc, ¤t_node) })?; - std_lib.assert_equal(layouter, &root, inputs.merkle_tree_commitment) + std_lib + .assert_equal(layouter, &root, inputs.merkle_tree_commitment) + .map_err(StmCircuitError::from) } #[cfg(test)] @@ -120,6 +120,7 @@ mod tests { use midnight_proofs::plonk::Error; use crate::circuits::common::merkle::Position; + use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::tests::test_helpers::{ TEST_MERKLE_TREE_DEPTH, TEST_MERKLE_TREE_DEPTH_FOR_PATH_PADDING, assert_relation_rejected, impl_focused_test_relation, jubjub_poseidon_used_chips, prove_and_verify_relation, @@ -133,6 +134,7 @@ mod tests { impl_focused_test_relation!( MerkleRelation, (CircuitWitnessEntry, MerkleRoot), + error = StmCircuitError, jubjub_poseidon_used_chips(), |std_lib, layouter, witness| { let verification_key = std_lib.jubjub().assign( @@ -203,6 +205,7 @@ mod tests { impl_focused_test_relation!( MerklePathRelation, (CircuitWitnessEntry, MerkleRoot), + error = StmCircuitError, jubjub_poseidon_used_chips(), |std_lib, layouter, witness| { let verification_key = std_lib.jubjub().assign( diff --git a/mithril-stm/src/circuits/halo2/gadgets/unique_schnorr_signature.rs b/mithril-stm/src/circuits/halo2/gadgets/unique_schnorr_signature.rs index 0bb14b14e44..7543d6f5470 100644 --- a/mithril-stm/src/circuits/halo2/gadgets/unique_schnorr_signature.rs +++ b/mithril-stm/src/circuits/halo2/gadgets/unique_schnorr_signature.rs @@ -81,6 +81,7 @@ mod tests { use midnight_circuits::ecc::curves::CircuitCurve as CircuitCurveTrait; use midnight_circuits::instructions::{AssignmentInstructions, ConversionInstructions}; use midnight_circuits::types::AssignedNative; + use midnight_proofs::plonk::Error; use crate::circuits::halo2::tests::test_helpers::{ TEST_MERKLE_TREE_DEPTH, assert_relation_rejected, impl_focused_test_relation, @@ -99,6 +100,7 @@ mod tests { impl_focused_test_relation!( UniqueSchnorrSignatureRelation, (CircuitWitnessEntry, MerkleRoot, SignedMessageWithoutPrefix), + error = Error, jubjub_poseidon_used_chips(), |std_lib, layouter, witness| { let merkle_tree_commitment: AssignedNative = std_lib.assign( diff --git a/mithril-stm/src/circuits/halo2/mod.rs b/mithril-stm/src/circuits/halo2/mod.rs index 6f13b6a4e1a..bb5e47db37e 100644 --- a/mithril-stm/src/circuits/halo2/mod.rs +++ b/mithril-stm/src/circuits/halo2/mod.rs @@ -6,7 +6,7 @@ //! - `witness`: circuit-facing witness and instance contract //! - `adapters`: STM-to-circuit conversions for boundary types //! - `gadgets`: reusable constraint logic split by domain -//! - `errors`: typed circuit errors and backend synthesis adaptation +//! - `errors`: typed circuit errors and backend error conversion //! - `tests/golden`: end-to-end circuit scenarios //! - inline `#[cfg(test)]` blocks in `gadgets/*` and `adapters`: focused regression checks //! - `tests/test_helpers`: shared harness for focused gadget tests diff --git a/mithril-stm/src/circuits/halo2/tests/golden/cases/negative.rs b/mithril-stm/src/circuits/halo2/tests/golden/cases/negative.rs index b0bbbcb75a0..09d457475ca 100644 --- a/mithril-stm/src/circuits/halo2/tests/golden/cases/negative.rs +++ b/mithril-stm/src/circuits/halo2/tests/golden/cases/negative.rs @@ -4,10 +4,9 @@ use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::gadgets::LOTTERY_BIT_BOUND; use crate::circuits::halo2::tests::golden::helpers::{ LOTTERIES_PER_K, LeafSelector, StmCircuitScenario, assert_proof_rejected_by_verifier, - assert_proving_backend_message_contains, assert_proving_circuit_error, build_witness, - build_witness_with_fixed_signer, build_witness_with_indices, create_default_merkle_tree, - create_merkle_tree_with_leaf_selector, find_two_distinct_witness_entries, - prove_and_verify_result, setup_stm_circuit_env, + assert_proving_circuit_error, build_witness, build_witness_with_fixed_signer, + build_witness_with_indices, create_default_merkle_tree, create_merkle_tree_with_leaf_selector, + find_two_distinct_witness_entries, prove_and_verify_result, setup_stm_circuit_env, }; use crate::circuits::halo2::witness::{ CircuitMerkleTreeLeaf, CircuitWitnessEntry, SignedMessageWithoutPrefix, @@ -265,12 +264,13 @@ mod slow { .expect("index_out_of_bounds witness build should succeed"); let scenario = StmCircuitScenario::new(merkle_tree_commitment, message, witness); - assert_proving_backend_message_contains( - prove_and_verify_result(&env, scenario), - &format!( - "Circuit::validate_lottery_index failed: index ({}) must be lower than m ({m})", - m as LotteryIndex - ), + let error = assert_proving_circuit_error(prove_and_verify_result(&env, scenario)); + assert_eq!( + error, + StmCircuitError::LotteryIndexOutOfBounds { + index: m as LotteryIndex, + m, + } ); } @@ -294,11 +294,13 @@ mod slow { .expect("index_too_large_for_circuit_range witness build should succeed"); let scenario = StmCircuitScenario::new(merkle_tree_commitment, message, witness); - assert_proving_backend_message_contains( - prove_and_verify_result(&env, scenario), - &format!( - "Circuit::validate_lottery_index failed: index ({too_large}) exceeds max supported ({max_supported})", - ), + let error = assert_proving_circuit_error(prove_and_verify_result(&env, scenario)); + assert_eq!( + error, + StmCircuitError::LotteryIndexTooLarge { + index: too_large, + max_supported, + } ); } @@ -381,13 +383,13 @@ mod slow { witness[0].merkle_path.siblings.pop(); let scenario = StmCircuitScenario::new(merkle_tree_commitment, message, witness); - // Relation boundary flattens this typed guard error into `PlonkError::Synthesis(String)`. - assert_proving_backend_message_contains( - prove_and_verify_result(&env, scenario), - &format!( - "Circuit::validate_merkle_sibling_length failed: expected depth {expected_depth}, got {}", - expected_depth - 1 - ), + let error = assert_proving_circuit_error(prove_and_verify_result(&env, scenario)); + assert_eq!( + error, + StmCircuitError::MerkleSiblingLengthMismatch { + expected_depth, + actual: expected_depth - 1, + } ); } @@ -416,13 +418,13 @@ mod slow { .push((Position::Left, SignedMessageWithoutPrefix::ZERO)); let scenario = StmCircuitScenario::new(merkle_tree_commitment, message, witness); - // Relation boundary flattens this typed guard error into `PlonkError::Synthesis(String)`. - assert_proving_backend_message_contains( - prove_and_verify_result(&env, scenario), - &format!( - "Circuit::validate_merkle_sibling_length failed: expected depth {expected_depth}, got {}", - expected_depth + 1 - ), + let error = assert_proving_circuit_error(prove_and_verify_result(&env, scenario)); + assert_eq!( + error, + StmCircuitError::MerkleSiblingLengthMismatch { + expected_depth, + actual: expected_depth + 1, + } ); } @@ -560,10 +562,13 @@ mod slow { witness.pop(); let scenario = StmCircuitScenario::new(merkle_tree_commitment, message, witness); - // Relation boundary flattens this typed guard error into `PlonkError::Synthesis(String)`. - assert_proving_backend_message_contains( - prove_and_verify_result(&env, scenario), - &format!("Circuit::validate_witness_length failed: expected k {K}, got 2"), + let error = assert_proving_circuit_error(prove_and_verify_result(&env, scenario)); + assert_eq!( + error, + StmCircuitError::WitnessLengthMismatch { + expected_k: K, + actual: 2, + } ); } @@ -588,10 +593,13 @@ mod slow { witness.push(extra); let scenario = StmCircuitScenario::new(merkle_tree_commitment, message, witness); - // Relation boundary flattens this typed guard error into `PlonkError::Synthesis(String)`. - assert_proving_backend_message_contains( - prove_and_verify_result(&env, scenario), - &format!("Circuit::validate_witness_length failed: expected k {K}, got 4"), + let error = assert_proving_circuit_error(prove_and_verify_result(&env, scenario)); + assert_eq!( + error, + StmCircuitError::WitnessLengthMismatch { + expected_k: K, + actual: 4, + } ); } diff --git a/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs b/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs index 399b367f306..e9f3dc410ec 100644 --- a/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs +++ b/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs @@ -8,7 +8,6 @@ use std::time::Instant; use anyhow::{Context, anyhow}; use midnight_circuits::hash::poseidon::PoseidonState; use midnight_curves::Bls12; -use midnight_proofs::plonk::Error as PlonkError; use midnight_proofs::poly::kzg::params::ParamsKZG; use midnight_proofs::utils::SerdeFormat; use midnight_zk_stdlib as zk; @@ -34,7 +33,7 @@ use crate::membership_commitment::{ use crate::signature_scheme::{ BaseFieldElement, SchnorrSigningKey, SchnorrVerificationKey, UniqueSchnorrSignature, }; -use crate::{LotteryIndex, LotteryTargetValue, Parameters, StmError, StmResult}; +use crate::{LotteryIndex, LotteryTargetValue, Parameters, StmResult}; /// Default number of signers used in golden test environments. const DEFAULT_NUM_SIGNERS: usize = 3000; @@ -88,34 +87,10 @@ pub(crate) fn assert_proving_circuit_error(result: StmResult) -> StmCircui } } -/// Assert proving failed with a backend synthesis message containing `expected`. -/// -/// This is intended for test-only checks when Midnight returns untyped synthesis strings. -/// At the relation boundary, typed `StmCircuitError` guard failures are flattened into -/// `PlonkError::Synthesis(String)`, so message checks are the only robust assertion path. -pub(crate) fn assert_proving_backend_message_contains(result: StmResult, expected: &str) { - match result { - Err(error) => { - let message = error.chain().find_map(|err| match err.downcast_ref::() { - Some(PlonkError::Synthesis(message)) => Some(message), - _ => None, - }); - let message = message.unwrap_or_else(|| { - panic!("expected PlonkError::Synthesis in source chain, got: {error}") - }); - assert!( - message.contains(expected), - "expected backend message to contain '{expected}', got '{message}'" - ); - } - _ => panic!("expected circuit proving failure"), - } -} - fn validate_relation_for_setup(relation: &StmCertificateCircuit) -> StmResult<()> { relation .validate_parameters() - .context("Circuit parameter validation failed before setup") + .with_context(|| "Circuit parameter validation failed before setup") } /// Cache key derived from the STM circuit configuration. @@ -614,7 +589,7 @@ pub(crate) fn prove_and_verify_result( scenario.witness, &mut rng, ) - .map_err(map_proving_backend_error)?; + .with_context(|| "Proving step failed")?; let duration = start.elapsed(); println!("\nProof generation took: {:?}", duration); println!("Proof size: {:?}", proof.len()); @@ -634,14 +609,10 @@ pub(crate) fn prove_and_verify_result( Ok(()) } else { Err(anyhow!(StmCircuitError::VerificationRejected)) - .context(format!("Proof verification step failed: {verify_result:?}")) + .with_context(|| format!("Proof verification step failed: {verify_result:?}")) } } -fn map_proving_backend_error(error: PlonkError) -> StmError { - anyhow::Error::new(error).context("Proving step failed") -} - /// Run a case using the default message (SignedMessageWithoutPrefix::from(DEFAULT_TEST_MSG)). pub(crate) fn run_stm_circuit_case_default( case_name: &str, diff --git a/mithril-stm/src/circuits/halo2/tests/test_helpers.rs b/mithril-stm/src/circuits/halo2/tests/test_helpers.rs index 6f53b3e2901..fcdd0c3c2c0 100644 --- a/mithril-stm/src/circuits/halo2/tests/test_helpers.rs +++ b/mithril-stm/src/circuits/halo2/tests/test_helpers.rs @@ -109,18 +109,18 @@ pub(crate) fn sample_valid_circuit_witness_entry( /// Declares a tiny test-only relation with no public inputs and a single witness payload. macro_rules! impl_focused_test_relation { - ($name:ident, $witness:ty, $chips:expr, |$std_lib:ident, $layouter:ident, $witness_value:ident| $body:block) => { + ($name:ident, $witness:ty, error = $error:ty, $chips:expr, |$std_lib:ident, $layouter:ident, $witness_value:ident| $body:block) => { #[derive(Clone, Default, Debug)] struct $name; impl midnight_zk_stdlib::Relation for $name { - type Error = midnight_proofs::plonk::Error; + type Error = $error; type Instance = (); type Witness = $witness; fn format_instance( _instance: &Self::Instance, - ) -> Result, midnight_proofs::plonk::Error> { + ) -> Result, Self::Error> { Ok(vec![]) } @@ -130,7 +130,7 @@ macro_rules! impl_focused_test_relation { $layouter: &mut impl midnight_proofs::circuit::Layouter, _instance: midnight_proofs::circuit::Value<()>, $witness_value: midnight_proofs::circuit::Value<$witness>, - ) -> Result<(), midnight_proofs::plonk::Error> $body + ) -> Result<(), Self::Error> $body fn used_chips(&self) -> midnight_zk_stdlib::ZkStdLibArch { $chips diff --git a/mithril-stm/src/circuits/halo2/witness_assignments.rs b/mithril-stm/src/circuits/halo2/witness_assignments.rs index 227ab52dbae..ef2b35c0f31 100644 --- a/mithril-stm/src/circuits/halo2/witness_assignments.rs +++ b/mithril-stm/src/circuits/halo2/witness_assignments.rs @@ -11,9 +11,8 @@ use midnight_proofs::circuit::{Layouter, Value}; use midnight_proofs::plonk::Error; use midnight_zk_stdlib::ZkStdLib; -use crate::StmResult; use crate::circuits::halo2::circuit::StmCertificateCircuit; -use crate::circuits::halo2::errors::to_synthesis_error; +use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::types::{CircuitBase, CircuitCurve}; use crate::circuits::halo2::witness::CircuitWitnessEntry; use crate::signature_scheme::PrimeOrderProjectivePoint; @@ -56,7 +55,7 @@ pub(crate) fn assign_witness_entry( std_lib: &ZkStdLib, layouter: &mut impl Layouter, witness_entry: Value, -) -> Result { +) -> Result { let verification_key = assign_verification_key(std_lib, layouter, witness_entry.clone())?; let lottery_target_value = std_lib.assign( @@ -85,11 +84,14 @@ fn assign_verification_key( std_lib: &ZkStdLib, layouter: &mut impl Layouter, witness_entry: Value, -) -> Result, Error> { - std_lib.jubjub().assign( - layouter, - witness_entry.map(|entry| entry.leaf.verification_key_point().0), - ) +) -> Result, StmCircuitError> { + std_lib + .jubjub() + .assign( + layouter, + witness_entry.map(|entry| entry.leaf.verification_key_point().0), + ) + .map_err(StmCircuitError::from) } /// Assigns and validates the Merkle authentication path carried by one witness entry. @@ -98,12 +100,12 @@ fn assign_merkle_path( std_lib: &ZkStdLib, layouter: &mut impl Layouter, witness_entry: Value, -) -> Result { +) -> Result { let siblings = std_lib.assign_many( layouter, witness_entry .clone() - .map_with_result(|entry| -> StmResult<_> { + .map_with_result(|entry| -> Result<_, StmCircuitError> { let merkle_path = entry.merkle_path; circuit.validate_merkle_sibling_length(merkle_path.siblings.len())?; Ok(merkle_path @@ -111,8 +113,7 @@ fn assign_merkle_path( .iter() .map(|sibling| sibling.1.into()) .collect::>()) - }) - .map_err(to_synthesis_error)? + })? .transpose_vec(circuit.merkle_tree_depth() as usize) .as_slice(), )?; @@ -121,7 +122,7 @@ fn assign_merkle_path( layouter, witness_entry .clone() - .map_with_result(|entry| -> StmResult<_> { + .map_with_result(|entry| -> Result<_, StmCircuitError> { let merkle_path = entry.merkle_path; circuit.validate_merkle_position_length(merkle_path.siblings.len())?; Ok(merkle_path @@ -129,8 +130,7 @@ fn assign_merkle_path( .iter() .map(|sibling| CircuitBase::from(sibling.0)) .collect::>()) - }) - .map_err(to_synthesis_error)? + })? .transpose_vec(circuit.merkle_tree_depth() as usize) .as_slice(), )?; @@ -151,7 +151,7 @@ pub(crate) fn assign_signature_components( std_lib: &ZkStdLib, layouter: &mut impl Layouter, witness_entry: Value, -) -> Result { +) -> Result { let commitment_point_value = witness_entry .clone() .map_with_result(|entry| { @@ -159,7 +159,7 @@ pub(crate) fn assign_signature_components( let (u, v) = signature.commitment_point.get_coordinates(); PrimeOrderProjectivePoint::from_coordinates(u, v).map(|point| point.0) }) - .map_err(to_synthesis_error)?; + .map_err(|error| StmCircuitError::Backend(error.to_string()))?; let commitment_point = std_lib.jubjub().assign(layouter, commitment_point_value)?; let response = std_lib.jubjub().assign( layouter,