diff --git a/Cargo.lock b/Cargo.lock index b0188444290..31f790a66e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4507,7 +4507,7 @@ dependencies = [ [[package]] name = "mithril-stm" -version = "0.9.26" +version = "0.9.27" dependencies = [ "anyhow", "blake2 0.10.6", diff --git a/mithril-stm/CHANGELOG.md b/mithril-stm/CHANGELOG.md index 21563a67b8d..78f37aa7835 100644 --- a/mithril-stm/CHANGELOG.md +++ b/mithril-stm/CHANGELOG.md @@ -5,6 +5,17 @@ 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.9.27 (03-11-2026) + +### Changed + +- Introduced circuit-local Halo2 types in `circuits/halo2/types.rs` with shared `CircuitBaseField`, `CircuitBase`, and `CircuitCurve`. +- Replaced duplicated local `F`/`C` aliases with shared circuit types across Halo2 circuit, gadgets, and golden helper code. +- Standardized Halo2 conversion paths using `From`/`Into` implementations for circuit/domain field wrappers. +- Added `circuits/halo2/adapters.rs` to convert STM Merkle paths into Halo2 witness paths for circuit consumption. +- Removed `circuits/halo2/utils/mod.rs` and inlined field-limb split logic into `circuits/halo2/gadgets.rs`. +- Unified synthesis error mapping through `to_synthesis_error` in `circuits/halo2/errors.rs`. + ## 0.9.26 (03-09-2026) ### Changed diff --git a/mithril-stm/Cargo.toml b/mithril-stm/Cargo.toml index 3c457097d3c..eecfb4995ea 100644 --- a/mithril-stm/Cargo.toml +++ b/mithril-stm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-stm" -version = "0.9.26" +version = "0.9.27" edition = { workspace = true } authors = { workspace = true } homepage = { workspace = true } diff --git a/mithril-stm/src/circuits/halo2/adapters.rs b/mithril-stm/src/circuits/halo2/adapters.rs new file mode 100644 index 00000000000..e0eac8f4e91 --- /dev/null +++ b/mithril-stm/src/circuits/halo2/adapters.rs @@ -0,0 +1,48 @@ +//! Adapters for converting STM-side structures to Halo2 circuit witness structures. + +use digest::Digest; +use thiserror::Error; + +use crate::circuits::halo2::types::{MerklePath as Halo2MerklePath, Position}; +use crate::membership_commitment::MerklePath as StmMerklePath; +use crate::signature_scheme::BaseFieldElement; + +/// Errors returned when adapting STM Merkle paths to Halo2 witness paths. +#[derive(Debug, Error)] +pub enum MerklePathAdapterError { + #[error("invalid merkle digest length")] + InvalidDigestLength, + #[error("non-canonical merkle digest")] + NonCanonicalDigest, +} + +impl TryFrom<&StmMerklePath> for Halo2MerklePath { + type Error = MerklePathAdapterError; + + fn try_from(stm_path: &StmMerklePath) -> Result { + let mut siblings = Vec::with_capacity(stm_path.values.len()); + + for (i, value) in stm_path.values.iter().enumerate() { + let bytes: [u8; 32] = value + .as_slice() + .try_into() + .map_err(|_| MerklePathAdapterError::InvalidDigestLength)?; + let node = BaseFieldElement::from_bytes(&bytes) + .ok() + .map(|base| base.into()) + .ok_or(MerklePathAdapterError::NonCanonicalDigest)?; + let bit = (stm_path.index >> i) & 1; + // At level `i`, `bit = (index >> i) & 1`: `0` means current is left, `1` means right. + // STM uses `H(current || sibling)` for `bit == 0`, else `H(sibling || current)`. + // Map `0 -> Position::Right` and `1 -> Position::Left` so Halo2 folds identically. + let position = if bit == 0 { + Position::Right + } else { + Position::Left + }; + siblings.push((position, node)); + } + + Ok(Halo2MerklePath::new(siblings)) + } +} diff --git a/mithril-stm/src/circuits/halo2/circuit.rs b/mithril-stm/src/circuits/halo2/circuit.rs index e04cd3f20c2..4c35e29022f 100644 --- a/mithril-stm/src/circuits/halo2/circuit.rs +++ b/mithril-stm/src/circuits/halo2/circuit.rs @@ -1,7 +1,7 @@ use anyhow::{Context, anyhow}; use ff::Field; use group::Group; -use midnight_circuits::ecc::curves::CircuitCurve; +use midnight_circuits::ecc::curves::CircuitCurve as CircuitCurveTrait; use midnight_circuits::instructions::{ AssignmentInstructions, ConversionInstructions, PublicInputInstructions, }; @@ -12,21 +12,18 @@ use midnight_proofs::circuit::{Layouter, Value}; use midnight_proofs::plonk::Error; use midnight_zk_stdlib::{Relation, ZkStdLib, ZkStdLibArch}; -use crate::circuits::halo2::errors::StmCircuitError; +use crate::circuits::halo2::errors::{StmCircuitError, to_synthesis_error}; use crate::circuits::halo2::gadgets::{ verify_lottery, verify_merkle_path, verify_unique_signature, }; use crate::circuits::halo2::types::{ - Jubjub, JubjubBase, MTLeaf, MerklePath, MerkleRoot, SignedMessageWithoutPrefix, + CircuitBase, CircuitCurve, MTLeaf, MerklePath, MerkleRoot, SignedMessageWithoutPrefix, }; use crate::signature_scheme::{ DOMAIN_SEPARATION_TAG_LOTTERY, DOMAIN_SEPARATION_TAG_SIGNATURE, PrimeOrderProjectivePoint, UniqueSchnorrSignature, }; -use crate::{LotteryIndex, Parameters, StmError, StmResult}; - -type F = JubjubBase; -type C = Jubjub; +use crate::{LotteryIndex, Parameters, StmResult}; #[derive(Clone, Default, Debug)] pub struct StmCircuit { @@ -38,24 +35,6 @@ pub struct StmCircuit { } impl StmCircuit { - /// Adapter at the Halo2 relation boundary. - /// - /// Internal code uses `StmResult` with typed `StmCircuitError`, while the Midnight relation - /// API requires returning `plonk::Error`. - fn synthesis_error(error: StmError) -> Error { - let error = match error.downcast::() { - Ok(plonk_error) => return plonk_error, - Err(error) => error, - }; - - let error = match error.downcast::() { - Ok(stm_error) => return Error::Synthesis(stm_error.to_string()), - Err(error) => error, - }; - - Error::Synthesis(error.to_string()) - } - fn checked_len_u32(actual: usize) -> u32 { u32::try_from(actual).unwrap_or(u32::MAX) } @@ -91,6 +70,29 @@ impl StmCircuit { Ok(()) } + /// Validates witness lottery indices against circuit constraints. + /// + /// The circuit uses 32-bit comparison constraints (`lower_than(..., 32)`), so each + /// index must fit in `u32` and must satisfy `index < num_lotteries`. + pub(crate) fn validate_lottery_index(&self, index: LotteryIndex) -> StmResult<()> { + let max_supported = u32::MAX as LotteryIndex; + if index > max_supported { + return Err(anyhow!(StmCircuitError::LotteryIndexTooLarge { + index, + max_supported, + })); + } + + if index >= self.num_lotteries as LotteryIndex { + return Err(anyhow!(StmCircuitError::LotteryIndexOutOfBounds { + index, + num_lotteries: self.num_lotteries, + })); + } + + Ok(()) + } + /// Validates Merkle sibling path length against `merkle_tree_depth`. /// /// This guards against inconsistent witness paths and returns @@ -157,43 +159,46 @@ impl Relation for StmCircuit { type Instance = (MerkleRoot, SignedMessageWithoutPrefix); type Witness = Vec<(MTLeaf, MerklePath, UniqueSchnorrSignature, LotteryIndex)>; - fn format_instance(instance: &Self::Instance) -> Result, Error> { - Ok(vec![instance.0, instance.1]) + fn format_instance(instance: &Self::Instance) -> Result, Error> { + Ok(vec![instance.0.into(), instance.1.into()]) } fn circuit( &self, std_lib: &ZkStdLib, - layouter: &mut impl Layouter, + layouter: &mut impl Layouter, instance: Value, witness: Value, ) -> Result<(), Error> { - self.validate_parameters().map_err(Self::synthesis_error)?; + self.validate_parameters().map_err(to_synthesis_error)?; let witness = witness .map_with_result(|witness| -> StmResult<_> { self.validate_witness_length(witness.len())?; + witness + .iter() + .try_for_each(|(_, _, _, index)| self.validate_lottery_index(*index))?; Ok(witness) }) - .map_err(Self::synthesis_error)? + .map_err(to_synthesis_error)? .transpose_vec(self.quorum as usize); - let merkle_root: AssignedNative = - std_lib.assign_as_public_input(layouter, instance.map(|(x, _)| x))?; - let msg: AssignedNative = - std_lib.assign_as_public_input(layouter, instance.map(|(_, x)| x))?; + let merkle_root: AssignedNative = + std_lib.assign_as_public_input(layouter, instance.map(|(x, _)| x.into()))?; + let msg: AssignedNative = + std_lib.assign_as_public_input(layouter, instance.map(|(_, x)| x.into()))?; // Compute H_1(merkle_root, msg) let hash = std_lib.hash_to_curve(layouter, &[merkle_root.clone(), msg.clone()])?; - let generator: AssignedNativePoint = std_lib.jubjub().assign_fixed( + let generator: AssignedNativePoint = std_lib.jubjub().assign_fixed( layouter, - ::CryptographicGroup::generator(), + ::CryptographicGroup::generator(), )?; let domain_separation_tag_signature: AssignedNative<_> = - std_lib.assign_fixed(layouter, DOMAIN_SEPARATION_TAG_SIGNATURE.0)?; + std_lib.assign_fixed(layouter, CircuitBase::from(DOMAIN_SEPARATION_TAG_SIGNATURE))?; let domain_separation_tag_lottery: AssignedNative<_> = - std_lib.assign_fixed(layouter, DOMAIN_SEPARATION_TAG_LOTTERY.0)?; + std_lib.assign_fixed(layouter, CircuitBase::from(DOMAIN_SEPARATION_TAG_LOTTERY))?; let lottery_prefix = std_lib.poseidon( layouter, &[ @@ -203,10 +208,13 @@ impl Relation for StmCircuit { ], )?; - let mut pre_index: AssignedNative<_> = std_lib.assign(layouter, Value::known(F::ZERO))?; + let mut pre_index: AssignedNative<_> = + std_lib.assign(layouter, Value::known(CircuitBase::ZERO))?; for (i, wit) in witness.into_iter().enumerate() { - let index: AssignedNative = - std_lib.assign(layouter, wit.clone().map(|(_, _, _, i)| F::from(i)))?; + let index: AssignedNative = std_lib.assign( + layouter, + wit.clone().map(|(_, _, _, i)| CircuitBase::from(i)), + )?; // Check index order if i > 0 { @@ -220,8 +228,8 @@ impl Relation for StmCircuit { .jubjub() .assign(layouter, wit.clone().map(|(x, _, _, _)| x.0.0.0))?; - let target: AssignedNative = - std_lib.assign(layouter, wit.clone().map(|(x, _, _, _)| x.1))?; + let target: AssignedNative = + std_lib.assign(layouter, wit.clone().map(|(x, _, _, _)| x.1.into()))?; // Assign sibling Values. let assigned_merkle_siblings = std_lib.assign_many( @@ -229,9 +237,9 @@ impl Relation for StmCircuit { wit.clone() .map_with_result(|(_, x, _, _)| -> StmResult<_> { self.validate_merkle_sibling_length(x.siblings.len())?; - Ok(x.siblings.iter().map(|sibling| sibling.1).collect::>()) + Ok(x.siblings.iter().map(|sibling| sibling.1.into()).collect::>()) }) - .map_err(Self::synthesis_error)? + .map_err(to_synthesis_error)? .transpose_vec(self.merkle_tree_depth as usize) .as_slice(), )?; @@ -242,9 +250,12 @@ impl Relation for StmCircuit { wit.clone() .map_with_result(|(_, x, _, _)| -> StmResult<_> { self.validate_merkle_position_length(x.siblings.len())?; - Ok(x.siblings.iter().map(|sibling| sibling.0.into()).collect::>()) + Ok(x.siblings + .iter() + .map(|sibling| CircuitBase::from(sibling.0)) + .collect::>()) }) - .map_err(Self::synthesis_error)? + .map_err(to_synthesis_error)? .transpose_vec(self.merkle_tree_depth as usize) .as_slice(), )?; @@ -253,7 +264,7 @@ impl Relation for StmCircuit { let assigned_merkle_positions = assigned_merkle_positions .iter() .map(|pos| std_lib.convert(layouter, pos)) - .collect::>, Error>>()?; + .collect::>, Error>>()?; let sigma_value = wit .clone() @@ -261,13 +272,16 @@ impl Relation for StmCircuit { let (u, v) = sig.commitment_point.get_coordinates(); PrimeOrderProjectivePoint::from_coordinates(u, v).map(|point| point.0) }) - .map_err(Self::synthesis_error)?; + .map_err(to_synthesis_error)?; let sigma: AssignedNativePoint<_> = std_lib.jubjub().assign(layouter, sigma_value)?; - let s: AssignedScalarOfNativeCurve = std_lib + let s: AssignedScalarOfNativeCurve = std_lib .jubjub() .assign(layouter, wit.clone().map(|(_, _, sig, _)| sig.response.0))?; - let c_native = std_lib.assign(layouter, wit.map(|(_, _, sig, _)| sig.challenge.0))?; - let c: AssignedScalarOfNativeCurve = + let c_native = std_lib.assign( + layouter, + wit.map(|(_, _, sig, _)| CircuitBase::from(sig.challenge)), + )?; + let c: AssignedScalarOfNativeCurve = std_lib.jubjub().convert(layouter, &c_native)?; verify_merkle_path( @@ -297,7 +311,7 @@ impl Relation for StmCircuit { } // m can be put as a public instance or a constant - let m = std_lib.assign_fixed(layouter, F::from(self.num_lotteries as u64))?; + let m = std_lib.assign_fixed(layouter, CircuitBase::from(self.num_lotteries as u64))?; let is_less = std_lib.lower_than(layouter, &pre_index, &m, 32)?; std_lib.assert_true(layouter, &is_less) @@ -353,18 +367,18 @@ impl Relation for StmCircuit { #[cfg(test)] mod dst_alignment_tests { - use midnight_circuits::{hash::poseidon::PoseidonChip, instructions::hash::HashCPU}; - use midnight_curves::Fq as JubjubBase; - + use crate::circuits::halo2::types::CircuitBase; use crate::signature_scheme::{ BaseFieldElement, DOMAIN_SEPARATION_TAG_LOTTERY, DOMAIN_SEPARATION_TAG_SIGNATURE, compute_poseidon_digest, }; + use midnight_circuits::{hash::poseidon::PoseidonChip, instructions::hash::HashCPU}; + const REFERENCE_SIGNATURE_DOMAIN_TAG: BaseFieldElement = - BaseFieldElement(JubjubBase::from_raw([0x5349_474E_5F44_5354, 0, 0, 0])); + BaseFieldElement(CircuitBase::from_raw([0x5349_474E_5F44_5354, 0, 0, 0])); const REFERENCE_LOTTERY_DOMAIN_TAG: BaseFieldElement = - BaseFieldElement(JubjubBase::from_raw([0x4C4F_5454_5F44_5354, 0, 0, 0])); + BaseFieldElement(CircuitBase::from_raw([0x4C4F_5454_5F44_5354, 0, 0, 0])); #[test] fn signature_and_lottery_domain_tags_do_not_collide() { @@ -387,12 +401,16 @@ mod dst_alignment_tests { stm_inputs.extend_from_slice(&signature_transcript_inputs); let signature_digest_via_stm = compute_poseidon_digest(&stm_inputs); - let mut signature_digest_manual_inputs = vec![REFERENCE_SIGNATURE_DOMAIN_TAG.0]; - signature_digest_manual_inputs - .extend(signature_transcript_inputs.iter().map(|value| value.0)); + let mut signature_digest_manual_inputs = + vec![CircuitBase::from(REFERENCE_SIGNATURE_DOMAIN_TAG)]; + signature_digest_manual_inputs.extend( + signature_transcript_inputs + .iter() + .map(|value| CircuitBase::from(*value)), + ); let signature_digest_via_reference_formula = BaseFieldElement( - PoseidonChip::::hash(&signature_digest_manual_inputs), + PoseidonChip::::hash(&signature_digest_manual_inputs), ); assert_eq!( @@ -403,14 +421,20 @@ mod dst_alignment_tests { #[test] fn lottery_prefix_matches_reference_lottery_domain_tag_formula() { - let merkle_root = JubjubBase::from(123u64); - let msg = JubjubBase::from(456u64); - - let lottery_prefix_via_stm_constant = - PoseidonChip::::hash(&[DOMAIN_SEPARATION_TAG_LOTTERY.0, merkle_root, msg]); - - let lottery_prefix_via_reference_formula = - PoseidonChip::::hash(&[REFERENCE_LOTTERY_DOMAIN_TAG.0, merkle_root, msg]); + let merkle_root = CircuitBase::from(123u64); + let msg = CircuitBase::from(456u64); + + let lottery_prefix_via_stm_constant = PoseidonChip::::hash(&[ + CircuitBase::from(DOMAIN_SEPARATION_TAG_LOTTERY), + merkle_root, + msg, + ]); + + let lottery_prefix_via_reference_formula = PoseidonChip::::hash(&[ + CircuitBase::from(REFERENCE_LOTTERY_DOMAIN_TAG), + merkle_root, + msg, + ]); assert_eq!( BaseFieldElement(lottery_prefix_via_stm_constant), diff --git a/mithril-stm/src/circuits/halo2/errors.rs b/mithril-stm/src/circuits/halo2/errors.rs index cd2c1549f37..5cef4eac3b5 100644 --- a/mithril-stm/src/circuits/halo2/errors.rs +++ b/mithril-stm/src/circuits/halo2/errors.rs @@ -1,5 +1,8 @@ +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)] @@ -16,6 +19,18 @@ pub enum StmCircuitError { )] WitnessLengthMismatch { expected_quorum: u32, actual: u32 }, + /// Witness lottery index does not fit in the circuit's 32-bit constraint representation. + #[error( + "Circuit::validate_lottery_index failed: index ({index}) exceeds max supported ({max_supported})" + )] + LotteryIndexTooLarge { index: u64, max_supported: u64 }, + + /// Witness lottery index is not a valid in-circuit lottery slot. + #[error( + "Circuit::validate_lottery_index failed: index ({index}) must be lower than num_lotteries ({num_lotteries})" + )] + LotteryIndexOutOfBounds { index: u64, num_lotteries: u32 }, + /// Merkle sibling path length does not match the configured Merkle depth. #[error( "Circuit::validate_merkle_sibling_length failed: expected depth {expected_depth}, got {actual}" @@ -28,6 +43,18 @@ pub enum StmCircuitError { )] MerklePositionLengthMismatch { expected_depth: u32, actual: u32 }, + /// Failed to parse the prime field modulus while splitting field limbs. + #[error("Field modulus parse failed")] + FieldModulusParseFailed, + + /// Failed to convert a reduced integer into a prime field element. + #[error("Field element conversion failed")] + FieldElementConversionFailed, + + /// Bit decomposition range is invalid for the selected prime field. + #[error("Invalid bit decomposition range ({num_bits}) for field size ({field_bits})")] + InvalidBitDecompositionRange { num_bits: u32, field_bits: u32 }, + /// Merkle tree depth does not fit fixture sizing constraints. #[error("Invalid merkle tree depth ({depth})")] InvalidMerkleTreeDepth { depth: u32 }, @@ -56,10 +83,6 @@ pub enum StmCircuitError { #[error("Invalid signer leaf index ({index}) for {num_signers} signers")] InvalidSignerFixtureIndex { index: u32, num_signers: u32 }, - /// Failed to decode lottery target from field bytes. - #[error("Invalid lottery target bytes")] - InvalidLotteryTargetBytes, - /// Failed to decode challenge bytes into a base field element. #[error("Invalid challenge bytes")] InvalidChallengeBytes, @@ -100,3 +123,18 @@ pub enum StmCircuitError { #[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, + }; + + let error = match error.downcast::() { + Ok(stm_error) => return PlonkError::Synthesis(stm_error.to_string()), + Err(error) => error, + }; + + PlonkError::Synthesis(error.to_string()) +} diff --git a/mithril-stm/src/circuits/halo2/gadgets.rs b/mithril-stm/src/circuits/halo2/gadgets.rs index 517d61a90db..f4476124e83 100644 --- a/mithril-stm/src/circuits/halo2/gadgets.rs +++ b/mithril-stm/src/circuits/halo2/gadgets.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use ff::{Field, PrimeField}; use midnight_circuits::instructions::{ ArithInstructions, AssertionInstructions, AssignmentInstructions, BinaryInstructions, @@ -9,18 +10,52 @@ use midnight_circuits::types::{ use midnight_proofs::circuit::Layouter; use midnight_proofs::plonk::Error; use midnight_zk_stdlib::ZkStdLib; +use num_bigint::BigUint; +use num_traits::{Num, One}; + +use crate::StmResult; +use crate::circuits::halo2::errors::{StmCircuitError, to_synthesis_error}; +use crate::circuits::halo2::types::{CircuitBase, CircuitCurve}; + +/// Splits a field element into `(lower, upper)` limbs at `num_bits` using LE encoding. +fn split_field_element_into_le_limbs( + value: &Fp, + num_bits: u32, +) -> StmResult<(Fp, Fp)> { + let field_bits = Fp::NUM_BITS; + if num_bits >= field_bits { + return Err(anyhow!(StmCircuitError::InvalidBitDecompositionRange { + num_bits, + field_bits, + })); + } + + let value_big = BigUint::from_bytes_le(value.to_repr().as_ref()); + let lower_mask = (BigUint::one() << num_bits) - BigUint::one(); + let lower_big = value_big.clone() & &lower_mask; + let upper_big = value_big >> num_bits; + let lower = big_unsigned_integer_to_field_element::(lower_big)?; + let upper = big_unsigned_integer_to_field_element::(upper_big)?; + Ok((lower, upper)) +} -use crate::circuits::halo2::types::{Jubjub, JubjubBase}; -use crate::circuits::halo2::utils::split_field_element_into_le_limbs; +fn field_modulus_as_biguint() -> StmResult { + BigUint::from_str_radix(&Fp::MODULUS[2..], 16) + .map_err(|_| anyhow!(StmCircuitError::FieldModulusParseFailed)) +} -type F = JubjubBase; -type C = Jubjub; +fn big_unsigned_integer_to_field_element(e: BigUint) -> StmResult { + 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)) +} fn assert_equal_parity( std_lib: &ZkStdLib, - layouter: &mut impl Layouter, - x: &AssignedNative, - y: &AssignedNative, + layouter: &mut impl Layouter, + x: &AssignedNative, + y: &AssignedNative, ) -> Result<(), Error> { let sgn0 = std_lib.sgn0(layouter, x)?; let sgn1 = std_lib.sgn0(layouter, y)?; @@ -30,19 +65,15 @@ fn assert_equal_parity( // Decompose a 255-bit value into 127-bit and 128-bit values without checking the bound fn decompose_unsafe( std_lib: &ZkStdLib, - layouter: &mut impl Layouter, - x: &AssignedNative, -) -> Result<(AssignedNative, AssignedNative), Error> { + layouter: &mut impl Layouter, + x: &AssignedNative, +) -> Result<(AssignedNative, AssignedNative), Error> { // Decompose 255-bit value into 127-bit and 128-bit values. let x_value = x.value(); - let base127 = F::from_u128(1_u128 << 127); + 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(|e| { - Error::Synthesis(format!( - "gadgets::decompose_unsafe failed to split field element into little-endian limbs: {e}" - )) - })? + .map_err(to_synthesis_error)? .unzip(); let x_low_assigned: AssignedNative<_> = std_lib.assign(layouter, x_low)?; @@ -50,8 +81,11 @@ fn decompose_unsafe( let x_combined: AssignedNative<_> = std_lib.linear_combination( layouter, - &[(F::ONE, x_low_assigned.clone()), (base127, x_high_assigned.clone())], - F::ZERO, + &[ + (CircuitBase::ONE, x_low_assigned.clone()), + (base127, x_high_assigned.clone()), + ], + CircuitBase::ZERO, )?; std_lib.assert_equal(layouter, x, &x_combined)?; @@ -65,10 +99,10 @@ fn decompose_unsafe( // Compare x < y where x, y are 255-bit fn lower_than_native( std_lib: &ZkStdLib, - layouter: &mut impl Layouter, - x: &AssignedNative, - y: &AssignedNative, -) -> Result, Error> { + layouter: &mut impl Layouter, + x: &AssignedNative, + y: &AssignedNative, +) -> Result, Error> { 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)?; @@ -83,12 +117,12 @@ fn lower_than_native( pub fn verify_merkle_path( std_lib: &ZkStdLib, - layouter: &mut impl Layouter, - vk: &AssignedNativePoint, - target: &AssignedNative, - merkle_root: &AssignedNative, - merkle_siblings: &[AssignedNative], - merkle_positions: &[AssignedBit], + layouter: &mut impl Layouter, + vk: &AssignedNativePoint, + target: &AssignedNative, + merkle_root: &AssignedNative, + merkle_siblings: &[AssignedNative], + merkle_positions: &[AssignedBit], ) -> Result<(), Error> { let vk_x = std_lib.jubjub().x_coordinate(vk); let vk_y = std_lib.jubjub().y_coordinate(vk); @@ -115,15 +149,15 @@ pub fn verify_merkle_path( #[allow(clippy::too_many_arguments)] pub fn verify_unique_signature( std_lib: &ZkStdLib, - layouter: &mut impl Layouter, - dst_signature: &AssignedNative, - generator: &AssignedNativePoint, - vk: &AssignedNativePoint, - s: &AssignedScalarOfNativeCurve, - c: &AssignedScalarOfNativeCurve, - c_native: &AssignedNative, - hash: &AssignedNativePoint, - sigma: &AssignedNativePoint, + layouter: &mut impl Layouter, + dst_signature: &AssignedNative, + generator: &AssignedNativePoint, + vk: &AssignedNativePoint, + s: &AssignedScalarOfNativeCurve, + c: &AssignedScalarOfNativeCurve, + c_native: &AssignedNative, + hash: &AssignedNativePoint, + sigma: &AssignedNativePoint, ) -> Result<(), Error> { // Compute R1 let cap_r_1 = std_lib.jubjub().msm( @@ -173,11 +207,11 @@ pub fn verify_unique_signature( pub fn verify_lottery( std_lib: &ZkStdLib, - layouter: &mut impl Layouter, - lottery_prefix: &AssignedNative, - sigma: &AssignedNativePoint, - index: &AssignedNative, - target: &AssignedNative, + layouter: &mut impl Layouter, + lottery_prefix: &AssignedNative, + sigma: &AssignedNativePoint, + index: &AssignedNative, + target: &AssignedNative, ) -> Result<(), Error> { let sigma_x = std_lib.jubjub().x_coordinate(sigma); let sigma_y = std_lib.jubjub().y_coordinate(sigma); diff --git a/mithril-stm/src/circuits/halo2/mod.rs b/mithril-stm/src/circuits/halo2/mod.rs index 7ecc530d2eb..233e1a498ed 100644 --- a/mithril-stm/src/circuits/halo2/mod.rs +++ b/mithril-stm/src/circuits/halo2/mod.rs @@ -1,10 +1,14 @@ //! Halo2 prototype integration (feature-gated by `future_snark`). -pub mod circuit; -pub(crate) mod errors; -pub mod gadgets; -pub mod types; -pub(crate) mod utils; +pub mod adapters; +// TODO(snark): remove `allow(dead_code)` once Halo2 modules are fully wired into STM. +#[cfg_attr(not(test), allow(dead_code))] +pub(crate) mod circuit; +pub mod errors; +#[cfg_attr(not(test), allow(dead_code))] +pub(crate) mod gadgets; +#[cfg_attr(not(test), allow(dead_code))] +pub(crate) mod types; #[cfg(test)] -pub(crate) mod golden; +pub(crate) mod tests; diff --git a/mithril-stm/src/circuits/halo2/golden/cases/macros.rs b/mithril-stm/src/circuits/halo2/tests/golden/cases/macros.rs similarity index 100% rename from mithril-stm/src/circuits/halo2/golden/cases/macros.rs rename to mithril-stm/src/circuits/halo2/tests/golden/cases/macros.rs diff --git a/mithril-stm/src/circuits/halo2/golden/cases/mod.rs b/mithril-stm/src/circuits/halo2/tests/golden/cases/mod.rs similarity index 100% rename from mithril-stm/src/circuits/halo2/golden/cases/mod.rs rename to mithril-stm/src/circuits/halo2/tests/golden/cases/mod.rs diff --git a/mithril-stm/src/circuits/halo2/golden/cases/negative.rs b/mithril-stm/src/circuits/halo2/tests/golden/cases/negative.rs similarity index 94% rename from mithril-stm/src/circuits/halo2/golden/cases/negative.rs rename to mithril-stm/src/circuits/halo2/tests/golden/cases/negative.rs index e9d2bc3e24d..8c789abda68 100644 --- a/mithril-stm/src/circuits/halo2/golden/cases/negative.rs +++ b/mithril-stm/src/circuits/halo2/tests/golden/cases/negative.rs @@ -1,8 +1,6 @@ -use ff::Field; - use crate::LotteryIndex; use crate::circuits::halo2::errors::StmCircuitError; -use crate::circuits::halo2::golden::helpers::{ +use crate::circuits::halo2::tests::golden::helpers::{ LOTTERIES_PER_QUORUM, 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, @@ -270,7 +268,44 @@ fn index_out_of_bounds() { .expect("index_out_of_bounds witness build should succeed"); let scenario = StmCircuitScenario::new(merkle_root, msg, witness); - assert_proof_rejected_by_verifier(prove_and_verify_result(&env, scenario)); + assert_proving_backend_message_contains( + prove_and_verify_result(&env, scenario), + &format!( + "Circuit::validate_lottery_index failed: index ({}) must be lower than num_lotteries ({m})", + m as LotteryIndex + ), + ); +} + +#[test] +fn index_too_large_for_u32_circuit_range() { + const K: u32 = 13; + const QUORUM: u32 = 3; + let msg = SignedMessageWithoutPrefix::from(42); + let env = setup_stm_circuit_env( + current_function!(), + K, + QUORUM, + QUORUM * LOTTERIES_PER_QUORUM, + ) + .expect("index_too_large_for_u32_circuit_range env setup should succeed"); + let merkle_tree = create_default_merkle_tree(env.num_signers()) + .expect("index_too_large_for_u32_circuit_range tree creation should succeed"); + + let merkle_root = merkle_tree.root(); + let too_large = (u32::MAX as LotteryIndex) + 1; + let indices = vec![6, 14, too_large]; + let witness = build_witness_with_indices(&merkle_tree, merkle_root, msg, &indices) + .expect("index_too_large_for_u32_circuit_range witness build should succeed"); + + let scenario = StmCircuitScenario::new(merkle_root, msg, witness); + assert_proving_backend_message_contains( + prove_and_verify_result(&env, scenario), + &format!( + "Circuit::validate_lottery_index failed: index ({too_large}) exceeds max supported ({})", + u32::MAX as LotteryIndex + ), + ); } #[test] diff --git a/mithril-stm/src/circuits/halo2/golden/cases/positive.rs b/mithril-stm/src/circuits/halo2/tests/golden/cases/positive.rs similarity index 98% rename from mithril-stm/src/circuits/halo2/golden/cases/positive.rs rename to mithril-stm/src/circuits/halo2/tests/golden/cases/positive.rs index b06675fb1dc..a388beeb333 100644 --- a/mithril-stm/src/circuits/halo2/golden/cases/positive.rs +++ b/mithril-stm/src/circuits/halo2/tests/golden/cases/positive.rs @@ -1,7 +1,5 @@ -use ff::Field; - use crate::LotteryIndex; -use crate::circuits::halo2::golden::helpers::{ +use crate::circuits::halo2::tests::golden::helpers::{ LOTTERIES_PER_QUORUM, LeafSelector, StmCircuitScenario, build_witness_with_fixed_signer, build_witness_with_indices, create_default_merkle_tree, create_merkle_tree_with_leaf_selector, prove_and_verify_result, run_stm_circuit_case, run_stm_circuit_case_default, diff --git a/mithril-stm/src/circuits/halo2/golden/helpers.rs b/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs similarity index 89% rename from mithril-stm/src/circuits/halo2/golden/helpers.rs rename to mithril-stm/src/circuits/halo2/tests/golden/helpers.rs index 06fd3e18f38..f9ab2a7fe9d 100644 --- a/mithril-stm/src/circuits/halo2/golden/helpers.rs +++ b/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, LazyLock, RwLock}; use std::time::Instant; use anyhow::{Context, anyhow}; -use ff::Field; +use midnight_curves::Bls12; use midnight_proofs::plonk::Error as PlonkError; use midnight_proofs::poly::kzg::params::ParamsKZG; use midnight_zk_stdlib as zk; @@ -16,7 +16,7 @@ use rand_core::SeedableRng; use crate::circuits::halo2::circuit::StmCircuit; use crate::circuits::halo2::errors::StmCircuitError; -use crate::circuits::halo2::types::{Bls12, JubjubBase, MTLeaf, MerklePath}; +use crate::circuits::halo2::types::{CircuitBase, MTLeaf, MerklePath, SignedMessageWithoutPrefix}; use crate::circuits::test_utils::setup::{generate_params, load_params}; use crate::hash::poseidon::MidnightPoseidonDigest; use crate::membership_commitment::{MerkleTree as StmMerkleTree, MerkleTreeSnarkLeaf}; @@ -25,9 +25,6 @@ use crate::signature_scheme::{ }; use crate::{LotteryIndex, LotteryTargetValue, Parameters, StmError, StmResult}; -/// Base field type used throughout STM circuit golden tests. -type F = JubjubBase; - /// Witness entry tuple used by STM circuit golden tests. type WitnessEntry = (MTLeaf, MerklePath, UniqueSchnorrSignature, LotteryIndex); @@ -144,8 +141,8 @@ pub(crate) struct StmCircuitEnv { /// Concrete STM circuit scenario inputs for proving/verifying in golden tests. pub(crate) struct StmCircuitScenario { - merkle_root: F, - msg: F, + merkle_root: SignedMessageWithoutPrefix, + msg: SignedMessageWithoutPrefix, witness: Vec, } @@ -154,7 +151,7 @@ pub(crate) struct StmCircuitScenario { pub(crate) struct SignerFixture { sk: SchnorrSigningKey, vk: SchnorrVerificationKey, - target_field: F, + target_field: SignedMessageWithoutPrefix, target_value: LotteryTargetValue, } @@ -170,15 +167,17 @@ impl From<&SignerFixture> for MTLeaf { } } -fn target_value_from_field(target: F) -> StmResult { - LotteryTargetValue::from_bytes(&target.to_bytes_le()) - .map_err(|_| anyhow!(StmCircuitError::InvalidLotteryTargetBytes)) +fn target_value_from_field(target: SignedMessageWithoutPrefix) -> LotteryTargetValue { + target.into() } -fn generate_signer_fixture(rng: &mut ChaCha20Rng, target: F) -> StmResult { +fn generate_signer_fixture( + rng: &mut ChaCha20Rng, + target: SignedMessageWithoutPrefix, +) -> StmResult { let stm_sk = SchnorrSigningKey::generate(rng); let stm_vk = SchnorrVerificationKey::new_from_signing_key(stm_sk.clone()); - let target_value = target_value_from_field(target)?; + let target_value = target_value_from_field(target); Ok(SignerFixture { sk: stm_sk, vk: stm_vk, @@ -192,7 +191,7 @@ fn generate_signer_fixture(rng: &mut ChaCha20Rng, target: F) -> StmResult, - root: F, + root: SignedMessageWithoutPrefix, signer_fixtures: Vec, } @@ -207,8 +206,8 @@ pub(crate) enum LeafSelector { } impl StmMerkleTreeWrapper { - /// Return the Merkle root as a JubjubBase field element. - pub(crate) fn root(&self) -> F { + /// Return the Merkle root used by Halo2 as a circuit wrapper field value. + pub(crate) fn root(&self) -> SignedMessageWithoutPrefix { self.root } @@ -234,7 +233,7 @@ impl StmMerkleTreeWrapper { } } -fn decode_merkle_root(root_bytes: &[u8]) -> StmResult { +fn decode_merkle_root(root_bytes: &[u8]) -> StmResult { let actual = root_bytes.len(); let root_array: [u8; 32] = root_bytes.try_into().map_err(|_| { anyhow!(StmCircuitError::InvalidMerkleRootDigestLength { @@ -243,14 +242,14 @@ fn decode_merkle_root(root_bytes: &[u8]) -> StmResult { })?; BaseFieldElement::from_bytes(&root_array) .ok() - .map(|base| base.0) + .map(Into::into) .ok_or_else(|| anyhow!(StmCircuitError::NonCanonicalMerkleRootDigest)) } fn build_merkle_tree_wrapper( n: usize, selected_index: Option, - target: F, + target: SignedMessageWithoutPrefix, ) -> StmResult { if let Some(i) = selected_index && i >= n @@ -267,7 +266,7 @@ fn build_merkle_tree_wrapper( let leaf_target = if selected_index == Some(i) { target } else { - -F::ONE + -SignedMessageWithoutPrefix::ONE }; signer_fixtures.push(generate_signer_fixture(&mut rng, leaf_target)?); } @@ -285,14 +284,14 @@ fn build_merkle_tree_wrapper( /// Build a default Merkle tree with all leaves set to the max target. pub(crate) fn create_default_merkle_tree(n: usize) -> StmResult { - build_merkle_tree_wrapper(n, None, -F::ONE) + build_merkle_tree_wrapper(n, None, -SignedMessageWithoutPrefix::ONE) } /// Build a full tree with one controlled leaf selected by `selector` and return its index. pub(crate) fn create_merkle_tree_with_leaf_selector( depth: u32, selector: LeafSelector, - target: F, + target: SignedMessageWithoutPrefix, ) -> StmResult<(StmMerkleTreeWrapper, usize)> { if depth >= usize::BITS { return Err(anyhow!(StmCircuitError::InvalidMerkleTreeDepth { depth })); @@ -316,16 +315,21 @@ pub(crate) fn create_merkle_tree_with_leaf_selector( Ok((tree, selected_index)) } -fn transcript_message(merkle_root: F, msg: F) -> [BaseFieldElement; 2] { - [BaseFieldElement(merkle_root), BaseFieldElement(msg)] +fn transcript_message( + merkle_root: SignedMessageWithoutPrefix, + msg: SignedMessageWithoutPrefix, +) -> [BaseFieldElement; 2] { + [merkle_root.into(), msg.into()] } fn assert_challenge_endianness(sig: &UniqueSchnorrSignature) -> StmResult<()> { let challenge_bytes = sig.challenge.to_bytes(); - let challenge_native = F::from_bytes_le(&challenge_bytes) + let challenge_native = CircuitBase::from_bytes_le(&challenge_bytes) .into_option() .ok_or_else(|| anyhow!(StmCircuitError::InvalidChallengeBytes))?; - if challenge_native != sig.challenge.0 { + if SignedMessageWithoutPrefix::from(challenge_native) + != SignedMessageWithoutPrefix::from(sig.challenge) + { return Err(anyhow!(StmCircuitError::ChallengeEndiannessMismatch)); } Ok(()) @@ -333,8 +337,8 @@ fn assert_challenge_endianness(sig: &UniqueSchnorrSignature) -> StmResult<()> { fn sign_and_verify_lottery_message( signer_fixture: &SignerFixture, - merkle_root: F, - msg: F, + merkle_root: SignedMessageWithoutPrefix, + msg: SignedMessageWithoutPrefix, rng: &mut ChaCha20Rng, ) -> StmResult { let transcript = transcript_message(merkle_root, msg); @@ -352,11 +356,11 @@ fn sign_and_verify_lottery_message( /// Build a witness with default strictly increasing indices [0..quorum). pub(crate) fn build_witness( merkle_tree: &StmMerkleTreeWrapper, - merkle_root: F, - msg: F, + merkle_root: SignedMessageWithoutPrefix, + msg: SignedMessageWithoutPrefix, quorum: u32, ) -> StmResult> { - let indices: Vec = (0..(quorum as u64)).collect(); + let indices: Vec = (0..quorum).map(u64::from).collect(); build_witness_with_indices(merkle_tree, merkle_root, msg, &indices) } @@ -364,8 +368,8 @@ pub(crate) fn build_witness( /// the circuit is responsible for strict ordering checks in negative tests. pub(crate) fn build_witness_with_indices( merkle_tree: &StmMerkleTreeWrapper, - merkle_root: F, - msg: F, + merkle_root: SignedMessageWithoutPrefix, + msg: SignedMessageWithoutPrefix, indices: &[LotteryIndex], ) -> StmResult> { build_witness_internal( @@ -382,8 +386,8 @@ pub(crate) fn build_witness_with_indices( pub(crate) fn build_witness_with_fixed_signer( merkle_tree: &StmMerkleTreeWrapper, signer_index: usize, - merkle_root: F, - msg: F, + merkle_root: SignedMessageWithoutPrefix, + msg: SignedMessageWithoutPrefix, indices: &[LotteryIndex], ) -> StmResult> { build_witness_internal( @@ -407,8 +411,8 @@ enum WitnessBuildMode<'a> { fn build_witness_internal( merkle_tree: &StmMerkleTreeWrapper, - merkle_root: F, - msg: F, + merkle_root: SignedMessageWithoutPrefix, + msg: SignedMessageWithoutPrefix, mode: WitnessBuildMode<'_>, ) -> StmResult> { let mut rng = ChaCha20Rng::from_seed([0u8; 32]); @@ -488,7 +492,11 @@ impl StmCircuitEnv { impl StmCircuitScenario { /// Construct a new STM circuit scenario from its instance and witness data. - pub(crate) fn new(merkle_root: F, msg: F, witness: Vec) -> Self { + pub(crate) fn new( + merkle_root: SignedMessageWithoutPrefix, + msg: SignedMessageWithoutPrefix, + witness: Vec, + ) -> Self { Self { merkle_root, msg, @@ -589,13 +597,23 @@ fn map_proving_backend_error(error: PlonkError) -> StmError { anyhow::Error::new(error).context("Proving step failed") } -/// Run a case using the default message (F::from(DEFAULT_TEST_MSG)). +/// Run a case using the default message (SignedMessageWithoutPrefix::from(DEFAULT_TEST_MSG)). pub(crate) fn run_stm_circuit_case_default(case_name: &str, k: u32, quorum: u32) -> StmResult<()> { - run_stm_circuit_case(case_name, k, quorum, F::from(DEFAULT_TEST_MSG)) + run_stm_circuit_case( + case_name, + k, + quorum, + SignedMessageWithoutPrefix::from(DEFAULT_TEST_MSG), + ) } /// Run a case with a caller-specified message. -pub(crate) fn run_stm_circuit_case(case_name: &str, k: u32, quorum: u32, msg: F) -> StmResult<()> { +pub(crate) fn run_stm_circuit_case( + case_name: &str, + k: u32, + quorum: u32, + msg: SignedMessageWithoutPrefix, +) -> StmResult<()> { let num_lotteries = quorum * LOTTERIES_PER_QUORUM; let env = setup_stm_circuit_env(case_name, k, quorum, num_lotteries)?; let merkle_tree = create_default_merkle_tree(env.num_signers())?; diff --git a/mithril-stm/src/circuits/halo2/golden/mod.rs b/mithril-stm/src/circuits/halo2/tests/golden/mod.rs similarity index 100% rename from mithril-stm/src/circuits/halo2/golden/mod.rs rename to mithril-stm/src/circuits/halo2/tests/golden/mod.rs diff --git a/mithril-stm/src/circuits/halo2/tests/mod.rs b/mithril-stm/src/circuits/halo2/tests/mod.rs new file mode 100644 index 00000000000..c5c4fcfc78f --- /dev/null +++ b/mithril-stm/src/circuits/halo2/tests/mod.rs @@ -0,0 +1,3 @@ +//! Test-only modules for the Halo2 circuit integration. + +pub(crate) mod golden; diff --git a/mithril-stm/src/circuits/halo2/types.rs b/mithril-stm/src/circuits/halo2/types.rs index 32fa8fa96fb..5c6f3874cef 100644 --- a/mithril-stm/src/circuits/halo2/types.rs +++ b/mithril-stm/src/circuits/halo2/types.rs @@ -2,18 +2,108 @@ //! //! This module bridges STM domain concepts (message, lottery index, Merkle proof) //! to circuit-oriented types consumed by the Halo2 relation and gadgets. +//! Wrapper types in this module represent the circuit statement layer. + +use std::ops::{Add, AddAssign, Neg, Sub}; -use crate::signature_scheme::SchnorrVerificationKey; use ff::Field; +use midnight_curves::{Fq as MidnightBaseField, JubjubExtended as MidnightJubjub}; + +use crate::signature_scheme::{BaseFieldElement, SchnorrVerificationKey}; + +/// Shared Midnight field alias used by Halo2 relation/chips. +pub(crate) type CircuitBase = MidnightBaseField; +/// Shared Midnight curve alias used by Halo2 relation/chips. +pub(crate) type CircuitCurve = MidnightJubjub; + +/// Field type boundaries: +/// - `BaseFieldElement`: STM/domain field wrapper. +/// - `CircuitBaseField`: circuit statement/input wrapper. +/// - `CircuitBase`: raw proving-library field. +/// +/// Circuit-local wrapper for base-field values present in public inputs/witness data. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub struct CircuitBaseField(CircuitBase); + +impl CircuitBaseField { + /// Additive identity in the circuit base field. + pub const ZERO: Self = Self(CircuitBase::ZERO); + /// Multiplicative identity in the circuit base field. + pub const ONE: Self = Self(CircuitBase::ONE); +} + +impl From for CircuitBaseField { + fn from(value: u64) -> Self { + Self(CircuitBase::from(value)) + } +} + +impl From for CircuitBaseField { + fn from(value: CircuitBase) -> Self { + Self(value) + } +} + +impl From for CircuitBase { + fn from(value: CircuitBaseField) -> Self { + value.0 + } +} + +impl From for CircuitBaseField { + fn from(value: BaseFieldElement) -> Self { + value.0.into() + } +} + +impl From for BaseFieldElement { + fn from(value: CircuitBaseField) -> Self { + Self(value.into()) + } +} + +impl From for CircuitBase { + fn from(value: BaseFieldElement) -> Self { + CircuitBaseField::from(value).into() + } +} + +impl Add for CircuitBaseField { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl AddAssign for CircuitBaseField { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} -pub use midnight_curves::{Bls12, Fq as JubjubBase, Fr as JubjubScalar, JubjubExtended as Jubjub}; +impl Sub for CircuitBaseField { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl Neg for CircuitBaseField { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(-self.0) + } +} /// Lottery threshold value used by the circuit for signer eligibility checks. -pub type Target = JubjubBase; +pub type Target = CircuitBaseField; /// Signed message value used by the circuit transcript, without any domain prefix. -pub type SignedMessageWithoutPrefix = JubjubBase; +pub type SignedMessageWithoutPrefix = CircuitBaseField; /// Merkle root public input committed by the STM membership commitment tree. -pub type MerkleRoot = JubjubBase; +pub type MerkleRoot = CircuitBaseField; /// Merkle-tree leaf material used by Halo2 witness construction. /// @@ -23,33 +113,39 @@ pub type MerkleRoot = JubjubBase; pub struct MTLeaf(pub SchnorrVerificationKey, pub Target); /// Position of a sibling node relative to the current hash in a Merkle path. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Position { Left, Right, } -impl From for JubjubBase { - fn from(value: Position) -> Self { - match value { - Position::Left => JubjubBase::ZERO, - Position::Right => JubjubBase::ONE, +impl From for CircuitBase { + fn from(position: Position) -> Self { + match position { + Position::Left => Self::ZERO, + Position::Right => Self::ONE, } } } +impl From for CircuitBaseField { + fn from(position: Position) -> Self { + Self(CircuitBase::from(position)) + } +} + /// Merkle authentication path used by the Halo2 circuit witness. /// /// Each entry stores sibling position and sibling hash value for one tree level. #[derive(Clone, Debug)] pub struct MerklePath { /// Ordered list of `(position, sibling_hash)` from leaf level to root level. - pub siblings: Vec<(Position, JubjubBase)>, + pub siblings: Vec<(Position, CircuitBaseField)>, } impl MerklePath { /// Creates a new Merkle path from ordered sibling entries. - pub fn new(siblings: Vec<(Position, JubjubBase)>) -> Self { + pub fn new(siblings: Vec<(Position, CircuitBaseField)>) -> Self { Self { siblings } } } diff --git a/mithril-stm/src/circuits/halo2/utils/mod.rs b/mithril-stm/src/circuits/halo2/utils/mod.rs deleted file mode 100644 index fa5d7d1a12e..00000000000 --- a/mithril-stm/src/circuits/halo2/utils/mod.rs +++ /dev/null @@ -1,93 +0,0 @@ -use ff::PrimeField; -use num_bigint::BigUint; -use num_traits::{Num, One}; -use thiserror::Error; - -/// Errors returned by Halo2 utility conversion helpers. -#[derive(Debug, Error)] -pub(crate) enum Halo2UtilsError { - /// Parsing the field modulus constant failed. - #[error("Failed to parse prime field modulus from hex")] - FieldModulusParse, - /// Converting a reduced integer to a field element failed. - #[error("Failed to convert reduced integer to field element")] - FieldElementConversion, -} - -/// Splits a field element into `(lower, upper)` limbs at `num_bits` using LE encoding. -/// -/// The input is interpreted as a little-endian integer before bit slicing. -pub(crate) fn split_field_element_into_le_limbs( - value: &F, - num_bits: u32, -) -> Result<(F, F), Halo2UtilsError> { - let value_big = BigUint::from_bytes_le(value.to_repr().as_ref()); - let lower_mask = (BigUint::one() << num_bits) - BigUint::one(); - let lower_big = value_big.clone() & &lower_mask; - let upper_big = value_big >> num_bits; - let lower = big_unsigned_integer_to_field_element::(lower_big)?; - let upper = big_unsigned_integer_to_field_element::(upper_big)?; - Ok((lower, upper)) -} - -/// Parses the prime field modulus (`F::MODULUS`) into a `BigUint`. -fn field_modulus_as_biguint() -> Result { - BigUint::from_str_radix(&F::MODULUS[2..], 16).map_err(|_| Halo2UtilsError::FieldModulusParse) -} - -/// Reduces an unsigned integer modulo `p` and converts it into the field element type. -/// -/// Conversion uses `from_str_vartime` on the reduced decimal representation. -fn big_unsigned_integer_to_field_element(e: BigUint) -> Result { - let modulus = field_modulus_as_biguint::()?; - let e = e % modulus; - F::from_str_vartime(&e.to_str_radix(10)[..]).ok_or(Halo2UtilsError::FieldElementConversion) -} - -#[cfg(test)] -mod merkle_path_adapter { - use crate::circuits::halo2::types::{MerklePath as Halo2MerklePath, Position}; - use crate::membership_commitment::MerklePath as StmMerklePath; - use crate::signature_scheme::BaseFieldElement; - use digest::Digest; - use thiserror::Error; - - #[derive(Debug, Error)] - pub enum MerklePathAdapterError { - #[error("Invalid merkle digest length")] - InvalidDigestLength, - #[error("Non-canonical merkle digest")] - NonCanonicalDigest, - } - - impl TryFrom<&StmMerklePath> for Halo2MerklePath { - type Error = MerklePathAdapterError; - - fn try_from(stm_path: &StmMerklePath) -> Result { - let mut siblings = Vec::with_capacity(stm_path.values.len()); - - for (i, value) in stm_path.values.iter().enumerate() { - let bytes: [u8; 32] = value - .as_slice() - .try_into() - .map_err(|_| MerklePathAdapterError::InvalidDigestLength)?; - let node = BaseFieldElement::from_bytes(&bytes) - .ok() - .map(|base| base.0) - .ok_or(MerklePathAdapterError::NonCanonicalDigest)?; - let bit = (stm_path.index >> i) & 1; - // At level `i`, `bit = (index >> i) & 1`: `0` means current is left, `1` means right. - // STM uses `H(current || sibling)` for `bit == 0`, else `H(sibling || current)`. - // Map `0 -> Position::Right` and `1 -> Position::Left` so Halo2 folds identically. - let position = if bit == 0 { - Position::Right - } else { - Position::Left - }; - siblings.push((position, node)); - } - - Ok(Halo2MerklePath::new(siblings)) - } - } -}