Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mithril-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
6 changes: 6 additions & 0 deletions mithril-stm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<plonk::Error>` 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
Expand Down
2 changes: 1 addition & 1 deletion mithril-stm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down
63 changes: 34 additions & 29 deletions mithril-stm/src/circuits/halo2/circuit.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(())
Expand All @@ -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(())
Expand All @@ -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(())
Expand All @@ -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(())
Expand All @@ -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(())
Expand Down Expand Up @@ -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<Vec<CircuitBase>, Error> {
fn format_instance(instance: &Self::Instance) -> Result<Vec<CircuitBase>, StmCircuitError> {
Ok(vec![instance.0.into(), instance.1.into()])
}

Expand All @@ -174,17 +179,16 @@ impl Relation for StmCertificateCircuit {
layouter: &mut impl Layouter<CircuitBase>,
instance: Value<Self::Instance>,
witness: Value<Self::Witness>,
) -> 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<CircuitBase> =
Expand Down Expand Up @@ -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 {
Expand Down
36 changes: 22 additions & 14 deletions mithril-stm/src/circuits/halo2/errors.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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::<PlonkError>() {
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::<StmCircuitError>() {
Ok(stm_error) => return PlonkError::Synthesis(stm_error.to_string()),
Err(error) => error,
};
impl From<PlonkError> 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),
);
}
}
12 changes: 8 additions & 4 deletions mithril-stm/src/circuits/halo2/gadgets/comparison.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -13,7 +13,7 @@ pub(super) fn lower_than_native(
layouter: &mut impl Layouter<CircuitBase>,
x: &AssignedNative<CircuitBase>,
y: &AssignedNative<CircuitBase>,
) -> Result<AssignedBit<CircuitBase>, Error> {
) -> Result<AssignedBit<CircuitBase>, 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)?;

Expand All @@ -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,
Expand All @@ -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)
}
);

Expand Down
24 changes: 11 additions & 13 deletions mithril-stm/src/circuits/halo2/gadgets/comparison_helpers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use anyhow::anyhow;
use ff::{Field, PrimeField};
Comment thread
hjeljeli32 marked this conversation as resolved.
use midnight_circuits::instructions::{
ArithInstructions, AssertionInstructions, AssignmentInstructions, DecompositionInstructions,
Expand All @@ -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<Fp: PrimeField>(
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());
Expand All @@ -38,17 +35,19 @@ pub(super) fn split_field_element_into_le_limbs<Fp: PrimeField>(
}

/// Parses the prime-field modulus into a `BigUint` for limb splitting and reduction helpers.
fn field_modulus_as_biguint<Fp: PrimeField>() -> StmResult<BigUint> {
fn field_modulus_as_biguint<Fp: PrimeField>() -> Result<BigUint, StmCircuitError> {
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<Fp: PrimeField>(e: BigUint) -> StmResult<Fp> {
fn big_unsigned_integer_to_field_element<Fp: PrimeField>(
e: BigUint,
) -> Result<Fp, StmCircuitError> {
let modulus = field_modulus_as_biguint::<Fp>()?;
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.
Expand All @@ -69,12 +68,11 @@ pub(super) fn decompose_unsafe(
std_lib: &ZkStdLib,
layouter: &mut impl Layouter<CircuitBase>,
x: &AssignedNative<CircuitBase>,
) -> Result<(AssignedNative<CircuitBase>, AssignedNative<CircuitBase>), Error> {
) -> Result<(AssignedNative<CircuitBase>, AssignedNative<CircuitBase>), 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)?;
Expand Down
Loading