From 6763def9e40fbaed3d83aa20c2858791477d537e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 31 Aug 2026 18:19:30 +0200 Subject: [PATCH 01/12] feat(stm): add predicate for certified circuit verification keys Add 'requires_certified_circuit_verification_keys' on AggregateSignatureType, true for the SNARK proof systems and false for concatenation. --- .../protocol/aggregate_signature/signature.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/mithril-stm/src/protocol/aggregate_signature/signature.rs b/mithril-stm/src/protocol/aggregate_signature/signature.rs index 7665bf2d5bd..3e22df4630b 100644 --- a/mithril-stm/src/protocol/aggregate_signature/signature.rs +++ b/mithril-stm/src/protocol/aggregate_signature/signature.rs @@ -80,6 +80,22 @@ impl AggregateSignatureType { AggregateSignatureType::IvcSnark => true, } } + + /// Whether an aggregate signature of this type is verified against circuit verification keys + /// that must be certified by an external authority before verification. + /// + /// Returns `true` for the SNARK proof systems, whose circuit verification keys are carried in + /// the ancillary verifier data, and `false` for the concatenation proof system, which uses no + /// circuit. + pub fn requires_certified_circuit_verification_keys(&self) -> bool { + match self { + AggregateSignatureType::Concatenation => false, + #[cfg(feature = "future_snark")] + AggregateSignatureType::Snark => true, + #[cfg(feature = "future_snark")] + AggregateSignatureType::IvcSnark => true, + } + } } impl From<&AggregateSignature> for AggregateSignatureType { @@ -886,6 +902,23 @@ mod tests { assert_golden_value(AggregateSignatureType::Snark, false); } + #[test] + fn golden_requires_certified_circuit_verification_keys_per_aggregate_signature_type() { + fn assert_golden_value(aggregate_signature_type: AggregateSignatureType, expected: bool) { + assert_eq!( + expected, + aggregate_signature_type.requires_certified_circuit_verification_keys(), + "golden 'requires_certified_circuit_verification_keys' value changed for {aggregate_signature_type}, this alters certificate verification semantics" + ); + } + + assert_golden_value(AggregateSignatureType::Concatenation, false); + #[cfg(feature = "future_snark")] + assert_golden_value(AggregateSignatureType::Snark, true); + #[cfg(feature = "future_snark")] + assert_golden_value(AggregateSignatureType::IvcSnark, true); + } + mod aggregate_signature_golden_concatenation { use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; From 380e73c201178e70f795d40f67e652d7d6885990 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 31 Aug 2026 18:22:37 +0200 Subject: [PATCH 02/12] feat(stm): expose circuit verification key digests from ancillary verifier data Add CircuitVerificationKeyDigest (Poseidon hash of the canonical key bytes, hex-serialized) and a digests accessor covering both SNARK variants. --- mithril-stm/src/circuits/mod.rs | 4 + .../src/circuits/verification_key_digest.rs | 316 ++++++++++++++++++ mithril-stm/src/lib.rs | 3 + .../aggregate_signature/ancillary_data.rs | 54 +++ 4 files changed, 377 insertions(+) create mode 100644 mithril-stm/src/circuits/verification_key_digest.rs diff --git a/mithril-stm/src/circuits/mod.rs b/mithril-stm/src/circuits/mod.rs index 57587f3015f..bbf3726721e 100644 --- a/mithril-stm/src/circuits/mod.rs +++ b/mithril-stm/src/circuits/mod.rs @@ -11,6 +11,7 @@ pub mod halo2_ivc; pub(crate) mod key_generator; pub(crate) mod key_provider; pub mod trusted_setup; +mod verification_key_digest; #[cfg(test)] pub(crate) mod test_utils; @@ -19,6 +20,9 @@ pub(crate) use halo2::types::CircuitCurve; pub(crate) use halo2::witness::{ CircuitInstance, CircuitMerkleTreeLeaf, CircuitWitness, MerklePath, }; +pub use verification_key_digest::{ + CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE, CircuitVerificationKeyDigest, +}; /// Constant holding the current path of the cached values related to the circuits const MITHRIL_CIRCUIT_CACHE_FOLDER: &str = "mithril-circuit"; diff --git a/mithril-stm/src/circuits/verification_key_digest.rs b/mithril-stm/src/circuits/verification_key_digest.rs new file mode 100644 index 00000000000..24607b3e2af --- /dev/null +++ b/mithril-stm/src/circuits/verification_key_digest.rs @@ -0,0 +1,316 @@ +//! Opaque digest identifying a circuit verification key. +//! +//! The digest is computed as a Poseidon hash over the canonical byte serialization of a +//! verifying key. Poseidon is SNARK-friendly and native to the scalar field of the circuits, so +//! the digest computation stays cheap if the registry check is ever proven in-circuit. It lets +//! callers reference a circuit verification key, for example in a signed registry, without +//! carrying the key itself or depending on its internal structure. + +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +use anyhow::{Context, anyhow}; +use digest::Digest; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::circuits::halo2::NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION; +use crate::circuits::halo2_ivc::RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION; +use crate::hash::poseidon::MidnightPoseidonDigest; +use crate::proof_system::{NonDeterministicSnarkProverFactory, SnarkProverFactory}; +use crate::{MithrilMembershipDigest, Parameters, StmError, StmResult, codec::TryToBytes}; + +/// Byte length of a circuit verification key digest. +pub const CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE: usize = 32; + +/// Poseidon digest of the canonical byte serialization of a circuit verification key. +/// +/// Serialized as a lowercase hex string. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct CircuitVerificationKeyDigest([u8; CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE]); + +impl CircuitVerificationKeyDigest { + /// Compute the digest of a verifying key from its canonical byte serialization. + pub(crate) fn try_from_verification_key( + verification_key: &K, + ) -> StmResult { + Ok(Self::from_canonical_key_bytes( + &verification_key.to_bytes_vec()?, + )) + } + + /// Compute the digest of a verifying key already in its canonical byte serialization. + fn from_canonical_key_bytes(canonical_key_bytes: &[u8]) -> Self { + let mut hasher = MidnightPoseidonDigest::new(); + hasher.update(canonical_key_bytes); + Self(hasher.finalize().into()) + } + + /// Digest of the IVC circuit verification key. + /// + /// The IVC circuit does not depend on the protocol parameters, so its verification key is the + /// embedded production constant for every deployment. + pub fn for_ivc_circuit() -> Self { + Self::from_canonical_key_bytes(RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION) + } + + /// Digest of the embedded certificate circuit verification key generated for the production + /// protocol parameters. + /// + /// The certificate circuit depends on the protocol parameters, so this digest only covers + /// deployments running with the production parameters. + pub fn for_production_certificate_circuit() -> Self { + Self::from_canonical_key_bytes(NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION) + } + + /// Compute the digest of the certificate circuit verification key for the given protocol + /// parameters, deriving the key from the trusted setup when it is not cached yet. + /// + /// The key is derived through the same prover the clerk uses to aggregate signatures, so the + /// digest matches the one carried by the certificates produced with these parameters. + pub fn compute_for_certificate_circuit(parameters: &Parameters) -> StmResult { + let prover = + SnarkProverFactory::::snark_aggregate_signature_prover( + &NonDeterministicSnarkProverFactory, + parameters, + )?; + + Self::try_from_verification_key(prover.verifying_key()) + } + + /// Return the digest bytes. + pub fn as_bytes(&self) -> &[u8; CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE] { + &self.0 + } +} + +impl Display for CircuitVerificationKeyDigest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +impl FromStr for CircuitVerificationKeyDigest { + type Err = StmError; + + fn from_str(s: &str) -> StmResult { + let bytes = + hex::decode(s).with_context(|| "CircuitVerificationKeyDigest: invalid hex encoding")?; + let bytes: [u8; CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE] = + bytes.try_into().map_err(|bytes: Vec| { + anyhow!( + "CircuitVerificationKeyDigest: expected {CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE} bytes, got {}", + bytes.len() + ) + })?; + + Ok(Self(bytes)) + } +} + +impl Serialize for CircuitVerificationKeyDigest { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for CircuitVerificationKeyDigest { + fn deserialize>(deserializer: D) -> Result { + let hex_string = String::deserialize(deserializer)?; + hex_string.parse().map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use crate::circuits::halo2_ivc::tests::common::asset_readers::load_embedded_verification_context_asset; + + use super::*; + + #[test] + #[ignore = "helper printing the production circuit verification key digests, run it to author the circuit verification key registry"] + fn print_circuit_verification_key_digests_for_production() { + println!( + "certificate-circuit (production protocol parameters): {}", + CircuitVerificationKeyDigest::for_production_certificate_circuit() + ); + println!( + "ivc-circuit: {}", + CircuitVerificationKeyDigest::for_ivc_circuit() + ); + } + + #[test] + fn digest_is_deterministic_and_separates_different_keys() { + let context = load_embedded_verification_context_asset() + .expect("verification context asset should load"); + + let certificate_key_digest = CircuitVerificationKeyDigest::try_from_verification_key( + &context.certificate_verifying_key, + ) + .unwrap(); + let certificate_key_digest_again = CircuitVerificationKeyDigest::try_from_verification_key( + &context.certificate_verifying_key, + ) + .unwrap(); + let recursive_key_digest = CircuitVerificationKeyDigest::try_from_verification_key( + &context.recursive_verifying_key, + ) + .unwrap(); + + assert_eq!(certificate_key_digest, certificate_key_digest_again); + assert_ne!(certificate_key_digest, recursive_key_digest); + } + + #[test] + fn digest_is_poseidon_hash_of_canonical_key_bytes() { + let context = load_embedded_verification_context_asset() + .expect("verification context asset should load"); + + let digest = CircuitVerificationKeyDigest::try_from_verification_key( + &context.certificate_verifying_key, + ) + .unwrap(); + + let mut hasher = MidnightPoseidonDigest::new(); + hasher.update(context.certificate_verifying_key.to_bytes_vec().unwrap()); + let expected: [u8; CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE] = hasher.finalize().into(); + + assert_eq!(&expected, digest.as_bytes()); + } + + #[test] + fn digest_round_trips_through_hex_string() { + let context = load_embedded_verification_context_asset() + .expect("verification context asset should load"); + let digest = CircuitVerificationKeyDigest::try_from_verification_key( + &context.certificate_verifying_key, + ) + .unwrap(); + + let hex_string = digest.to_string(); + let restored: CircuitVerificationKeyDigest = hex_string.parse().unwrap(); + + assert_eq!(digest, restored); + } + + #[test] + fn digest_round_trips_through_serde_json() { + let context = load_embedded_verification_context_asset() + .expect("verification context asset should load"); + let digest = CircuitVerificationKeyDigest::try_from_verification_key( + &context.certificate_verifying_key, + ) + .unwrap(); + + let json = serde_json::to_string(&digest).unwrap(); + let restored: CircuitVerificationKeyDigest = serde_json::from_str(&json).unwrap(); + + assert_eq!(json, format!("\"{digest}\"")); + assert_eq!(digest, restored); + } + + #[test] + fn from_str_rejects_invalid_hex_and_wrong_length() { + "not-hex" + .parse::() + .expect_err("non-hex input must be rejected"); + "abcd" + .parse::() + .expect_err("input shorter than the digest size must be rejected"); + } + + mod golden { + use crate::circuits::halo2::keys::NonRecursiveCircuitVerifyingKey; + use crate::circuits::halo2_ivc::keys::RecursiveCircuitVerifyingKey; + use crate::codec::TryFromBytes; + + use super::*; + + struct FixedBytesVerificationKey(Vec); + + impl TryToBytes for FixedBytesVerificationKey { + fn to_bytes_vec(&self) -> StmResult> { + Ok(self.0.clone()) + } + } + + #[test] + fn golden_digest_of_fixed_verification_key_bytes() { + let digest = CircuitVerificationKeyDigest::try_from_verification_key( + &FixedBytesVerificationKey(vec![42u8; 64]), + ) + .unwrap(); + + assert_eq!( + "5cfbcf921d5b29e3d449c5ecd707dd78924612790a27a68610c1cf0af3b7cc52", + digest.to_string(), + "golden circuit verification key digest changed for a fixed input, this alters the digest computation and breaks published circuit verification key registries" + ); + } + + #[test] + fn golden_digests_of_production_circuit_keys() { + assert_eq!( + "1264305828d13c48a7b85b0cf472198d5a8014d8c06b50e9f4dd9c586249355c", + CircuitVerificationKeyDigest::for_production_certificate_circuit().to_string(), + "golden production certificate circuit verification key digest changed, either the digest computation or the embedded production key changed, which breaks published circuit verification key registries" + ); + assert_eq!( + "e2077c751852ee5a4e0908b7b963e037bac64f6fd0bf1af2ff1f012e0aa57e57", + CircuitVerificationKeyDigest::for_ivc_circuit().to_string(), + "golden IVC circuit verification key digest changed, either the digest computation or the embedded production key changed, which breaks published circuit verification key registries" + ); + } + + #[test] + fn production_digests_match_the_deserialized_embedded_production_keys() { + let certificate_verifying_key = NonRecursiveCircuitVerifyingKey::try_from_bytes( + NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, + ) + .unwrap(); + let recursive_verifying_key = RecursiveCircuitVerifyingKey::try_from_bytes( + RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, + ) + .unwrap(); + + assert_eq!( + CircuitVerificationKeyDigest::try_from_verification_key(&certificate_verifying_key) + .unwrap(), + CircuitVerificationKeyDigest::for_production_certificate_circuit(), + "the embedded production certificate key constant must stay the canonical key serialization" + ); + assert_eq!( + CircuitVerificationKeyDigest::try_from_verification_key(&recursive_verifying_key) + .unwrap(), + CircuitVerificationKeyDigest::for_ivc_circuit(), + "the embedded IVC key constant must stay the canonical key serialization" + ); + } + + #[test] + fn golden_digests_of_embedded_verification_context_keys() { + let context = load_embedded_verification_context_asset() + .expect("verification context asset should load"); + + let certificate_key_digest = CircuitVerificationKeyDigest::try_from_verification_key( + &context.certificate_verifying_key, + ) + .unwrap(); + let recursive_key_digest = CircuitVerificationKeyDigest::try_from_verification_key( + &context.recursive_verifying_key, + ) + .unwrap(); + + assert_eq!( + "1ecb40d6ba62504520a904ae053f7ad6747ce2ebafd7facef3fb4009d52b6336", + certificate_key_digest.to_string(), + "golden certificate circuit verification key digest changed, either the digest computation or the canonical key serialization changed, which breaks published circuit verification key registries" + ); + assert_eq!( + "08174d68b60d5655d0def90e6d3680f8bd6216a1c714bc3c6111d7c1d8472a28", + recursive_key_digest.to_string(), + "golden IVC circuit verification key digest changed, either the digest computation or the canonical key serialization changed, which breaks published circuit verification key registries" + ); + } + } +} diff --git a/mithril-stm/src/lib.rs b/mithril-stm/src/lib.rs index 6bf6df8c2d0..8b60fa824c2 100644 --- a/mithril-stm/src/lib.rs +++ b/mithril-stm/src/lib.rs @@ -70,6 +70,9 @@ use hash::poseidon::MidnightPoseidonDigest; #[cfg(feature = "benchmark-internals")] pub use hash::poseidon::MidnightPoseidonDigest; +#[cfg(feature = "future_snark")] +pub use circuits::{CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE, CircuitVerificationKeyDigest}; + #[cfg(feature = "future_snark")] pub use proof_system::{ AggregateVerificationKeyForSnark, MERKLE_TREE_DEPTH_FOR_SNARK, SnarkProof, SnarkVerifierData, diff --git a/mithril-stm/src/protocol/aggregate_signature/ancillary_data.rs b/mithril-stm/src/protocol/aggregate_signature/ancillary_data.rs index 1a8a07fd56c..ce613ca68a7 100644 --- a/mithril-stm/src/protocol/aggregate_signature/ancillary_data.rs +++ b/mithril-stm/src/protocol/aggregate_signature/ancillary_data.rs @@ -15,6 +15,7 @@ use crate::{BaseFieldElement, SchnorrSigningKey, circuits::halo2_ivc::PREIMAGE_S #[cfg(feature = "future_snark")] use crate::{ SchnorrVerificationKey, StandardSchnorrSignature, + circuits::CircuitVerificationKeyDigest, proof_system::{IvcRollingState, SnarkVerifierData, halo2_ivc_snark::IvcVerifierData}, protocol::aggregate_signature::GenesisMessagePreimage, }; @@ -127,6 +128,30 @@ impl AncillaryVerifierData { Self::IvcSnark(_) => None, } } + + /// Returns the digests of the circuit verification keys carried by this verifier data, in a + /// stable order (certificate circuit first, then IVC circuit when present). + /// + /// These are the keys the proof is verified against, so certifying them certifies the + /// circuits used to produce the aggregate signature. + #[cfg(feature = "future_snark")] + pub fn circuit_verification_key_digests(&self) -> StmResult> { + match self { + Self::IvcSnark(ivc_verifier_data) => Ok(vec![ + CircuitVerificationKeyDigest::try_from_verification_key( + ivc_verifier_data.certificate_circuit_verification_key(), + )?, + CircuitVerificationKeyDigest::try_from_verification_key( + ivc_verifier_data.ivc_circuit_verification_key(), + )?, + ]), + Self::Snark(snark_verifier_data) => Ok(vec![ + CircuitVerificationKeyDigest::try_from_verification_key( + snark_verifier_data.certificate_circuit_verification_key(), + )?, + ]), + } + } } /// Genesis-related data carried into aggregate signature creation. @@ -435,6 +460,35 @@ mod tests { assert!(reconstructed.as_ivc_verifier_data().is_none()); } + #[cfg(feature = "future_snark")] + #[test] + fn ivc_verifier_data_exposes_certificate_then_ivc_circuit_verification_key_digests() { + let context = load_embedded_verification_context_asset() + .expect("verification context asset should load"); + let ivc_ancillary_verifier_data = AncillaryVerifierData::IvcSnark(IvcVerifierData::new( + MessageHash::ZERO, + context.certificate_verifying_key.clone(), + context.recursive_verifying_key, + )); + let snark_ancillary_verifier_data = + AncillaryVerifierData::Snark(SnarkVerifierData::new(context.certificate_verifying_key)); + + let ivc_digests = ivc_ancillary_verifier_data + .circuit_verification_key_digests() + .unwrap(); + let snark_digests = snark_ancillary_verifier_data + .circuit_verification_key_digests() + .unwrap(); + + assert_eq!(2, ivc_digests.len()); + assert_ne!(ivc_digests[0], ivc_digests[1]); + assert_eq!( + vec![ivc_digests[0]], + snark_digests, + "both variants must expose the same digest for the same certificate circuit verification key, certificate circuit first" + ); + } + /// Byte-locks the CBOR encoding of the pre-existing `IvcSnark` ancillary variant against a /// hardcoded digest, so appending the `Snark` variant to `AncillaryVerifierData` cannot /// silently change the encoding of the existing variant — which would break committed IVC From 95efa99191913fe0e8774c5e8a3cab571af036e6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 31 Aug 2026 18:25:58 +0200 Subject: [PATCH 03/12] feat(common): add circuit verification key registry types Add the genesis-signed registry (whitelist entries with epoch ranges and revocation-wins semantics) under crypto_helper/circuit_key_registry. --- Cargo.toml | 2 +- .../crypto_helper/circuit_key_registry/mod.rs | 11 + .../circuit_key_registry/registry.rs | 503 ++++++++++++++++++ mithril-common/src/crypto_helper/mod.rs | 4 + 4 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 mithril-common/src/crypto_helper/circuit_key_registry/mod.rs create mode 100644 mithril-common/src/crypto_helper/circuit_key_registry/registry.rs diff --git a/Cargo.toml b/Cargo.toml index 3f3c1580227..7abb171305f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ rayon = "1.12.0" reqwest = { version = "0.13.4", default-features = false, features = ["json"] } semver = "1.0.28" serde = { version = "1.0.228", features = ["derive", "rc"] } -serde_json = "1.0.150" +serde_json = { version = "1.0.150", features = ["raw_value"] } # Pin slog to `2.7.0`: `2.8.2` introduces failures in our test suite slog = "=2.7.0" slog-async = "2.8.0" diff --git a/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs b/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs new file mode 100644 index 00000000000..2d32f910ebf --- /dev/null +++ b/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs @@ -0,0 +1,11 @@ +//! Genesis-signed registry of the circuit verification keys trusted for SNARK certificates. +//! +//! The registry whitelists circuit verification key digests over inclusive epoch ranges and +//! supports revoking them retroactively, e.g. after a circuit vulnerability. It is published in +//! the repository per network, retrieved at runtime and verified against the Ed25519 half of +//! the genesis verification key before use. + +mod registry; + +pub use mithril_stm::{CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE, CircuitVerificationKeyDigest}; +pub use registry::*; diff --git a/mithril-common/src/crypto_helper/circuit_key_registry/registry.rs b/mithril-common/src/crypto_helper/circuit_key_registry/registry.rs new file mode 100644 index 00000000000..12b31e9dc26 --- /dev/null +++ b/mithril-common/src/crypto_helper/circuit_key_registry/registry.rs @@ -0,0 +1,503 @@ +//! Genesis-signed registry of the circuit verification keys trusted for SNARK certificates. + +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use thiserror::Error; + +use mithril_stm::CircuitVerificationKeyDigest; + +use crate::StdResult; +use crate::crypto_helper::{GenesisEd25519Signature, GenesisSigner, GenesisVerifier}; +use crate::entities::Epoch; + +/// Errors raised when checking circuit verification key digests against a +/// [CircuitVerificationKeyRegistry]. +#[derive(Error, Debug, PartialEq, Eq)] +pub enum CircuitVerificationKeyRegistryError { + /// The digest is covered by a revoked entry for the checked epoch. + #[error("circuit verification key '{digest}' is revoked for epoch {epoch}")] + Revoked { + /// Digest of the revoked circuit verification key. + digest: CircuitVerificationKeyDigest, + /// Epoch for which the check was performed. + epoch: Epoch, + }, + + /// The digest is not covered by any allowed entry for the checked epoch. + #[error("circuit verification key '{digest}' is not whitelisted for epoch {epoch}")] + NotWhitelisted { + /// Digest of the unknown or out-of-range circuit verification key. + digest: CircuitVerificationKeyDigest, + /// Epoch for which the check was performed. + epoch: Epoch, + }, +} + +/// Status of a circuit verification key entry over its epoch range. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CircuitVerificationKeyStatus { + /// The key may certify certificates whose epoch falls in the entry's range. + Allowed, + + /// Certificates produced with this key in the entry's range must be rejected. + Revoked, +} + +/// One statement about a circuit verification key, valid over an inclusive epoch range. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CircuitVerificationKeyEntry { + /// Digest of the circuit verification key the statement is about. + pub digest: CircuitVerificationKeyDigest, + + /// Human readable label of the circuit, e.g. "certificate-circuit v2". + pub name: String, + + /// Whether the key is allowed or revoked over the entry's range. + pub status: CircuitVerificationKeyStatus, + + /// First epoch (inclusive) covered by the statement. + pub start_epoch: Epoch, + + /// Last epoch (inclusive) covered by the statement, open-ended when absent. + pub end_epoch: Option, + + /// Audit trail, e.g. the reason of a revocation. + pub comment: Option, +} + +impl CircuitVerificationKeyEntry { + /// Whether the entry's epoch range contains the given epoch. + pub fn covers(&self, epoch: Epoch) -> bool { + self.start_epoch <= epoch && self.end_epoch.is_none_or(|end_epoch| epoch <= end_epoch) + } +} + +/// Registry of the circuit verification keys trusted for SNARK certificates. +/// +/// The registry is scoped by the genesis key that signs it: each network publishes its own +/// registry, signed with its own genesis key. A digest absent from the registry is rejected +/// (whitelist semantics); a revoked entry rejects the epochs it covers even when an allowed +/// entry also covers them (revocation wins). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CircuitVerificationKeyRegistry { + /// Monotonically increasing registry version, used for rollback protection. + pub version: u64, + + /// Statements about the circuit verification keys. + pub entries: Vec, +} + +impl CircuitVerificationKeyRegistry { + /// Check that every digest is whitelisted and not revoked for the given epoch. + /// + /// A digest fails with [Revoked](CircuitVerificationKeyRegistryError::Revoked) when any + /// revoked entry covers the epoch, and with + /// [NotWhitelisted](CircuitVerificationKeyRegistryError::NotWhitelisted) when no allowed + /// entry covers it. + pub fn check( + &self, + digests: &[CircuitVerificationKeyDigest], + epoch: Epoch, + ) -> Result<(), CircuitVerificationKeyRegistryError> { + digests.iter().try_for_each(|digest| { + let is_revoked = self.has_covering_entry_with_status( + digest, + epoch, + CircuitVerificationKeyStatus::Revoked, + ); + let is_allowed = self.has_covering_entry_with_status( + digest, + epoch, + CircuitVerificationKeyStatus::Allowed, + ); + + match (is_revoked, is_allowed) { + (true, _) => Err(CircuitVerificationKeyRegistryError::Revoked { + digest: *digest, + epoch, + }), + (false, false) => Err(CircuitVerificationKeyRegistryError::NotWhitelisted { + digest: *digest, + epoch, + }), + (false, true) => Ok(()), + } + }) + } + + /// Whether an entry with the given status covers the digest for the epoch. + fn has_covering_entry_with_status( + &self, + digest: &CircuitVerificationKeyDigest, + epoch: Epoch, + status: CircuitVerificationKeyStatus, + ) -> bool { + self.entries + .iter() + .any(|entry| entry.digest == *digest && entry.covers(epoch) && entry.status == status) + } +} + +/// Domain separation prefix of the registry genesis signature, so registry signatures can never +/// be confused with any other artifact signed by the genesis key. +pub const REGISTRY_SIGNATURE_DOMAIN_SEPARATOR: &[u8] = + b"MITHRIL_CIRCUIT_VERIFICATION_KEY_REGISTRY_V1"; + +/// A [CircuitVerificationKeyRegistry] together with its Ed25519 genesis signature. +/// +/// The registry travels as its exact JSON bytes and the signature covers those bytes (prefixed +/// by [REGISTRY_SIGNATURE_DOMAIN_SEPARATOR]), never a re-serialization: a verifier can then +/// tolerate registry fields added by future schema versions, since unknown fields survive +/// verbatim in the signed bytes and are ignored at parse time. +#[derive(Debug, Serialize, Deserialize)] +pub struct SignedCircuitVerificationKeyRegistry { + /// Exact JSON of the signed registry. + registry: Box, + + /// Ed25519 genesis signature over the domain separator followed by the exact registry JSON + /// bytes. + pub signature: GenesisEd25519Signature, +} + +impl SignedCircuitVerificationKeyRegistry { + /// Sign a registry with the Ed25519 half of the genesis signer. + pub fn try_new( + registry: CircuitVerificationKeyRegistry, + genesis_signer: &GenesisSigner, + ) -> StdResult { + let registry_json = serde_json::to_string_pretty(®istry)?; + let signature = genesis_signer.ed25519.sign(&Self::signable_bytes(®istry_json)); + + Ok(Self { + registry: RawValue::from_string(registry_json)?, + signature, + }) + } + + /// Verify the genesis signature over the exact registry JSON bytes and parse the registry. + pub fn verify( + &self, + genesis_verifier: &GenesisVerifier, + ) -> StdResult { + genesis_verifier + .verify_ed25519(&Self::signable_bytes(self.registry.get()), &self.signature)?; + + Ok(serde_json::from_str(self.registry.get())?) + } + + /// Parse the registry without verifying its signature, for displaying or testing purposes + /// only: never trust the result. + pub fn parse_registry_unverified(&self) -> StdResult { + Ok(serde_json::from_str(self.registry.get())?) + } + + /// Prefix the registry JSON bytes with the domain separator. + fn signable_bytes(registry_json: &str) -> Vec { + [REGISTRY_SIGNATURE_DOMAIN_SEPARATOR, registry_json.as_bytes()].concat() + } +} + +impl Clone for SignedCircuitVerificationKeyRegistry { + fn clone(&self) -> Self { + Self { + registry: self.registry.to_owned(), + signature: self.signature, + } + } +} + +impl PartialEq for SignedCircuitVerificationKeyRegistry { + fn eq(&self, other: &Self) -> bool { + self.registry.get() == other.registry.get() && self.signature == other.signature + } +} + +impl Eq for SignedCircuitVerificationKeyRegistry {} + +#[cfg(test)] +mod tests { + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; + + use crate::crypto_helper::GenesisEd25519Signer; + + use super::*; + + fn digest(seed: u8) -> CircuitVerificationKeyDigest { + hex::encode([seed; 32]).parse().unwrap() + } + + fn entry( + digest: CircuitVerificationKeyDigest, + status: CircuitVerificationKeyStatus, + start_epoch: u64, + end_epoch: Option, + ) -> CircuitVerificationKeyEntry { + CircuitVerificationKeyEntry { + digest, + name: "circuit".to_string(), + status, + start_epoch: Epoch(start_epoch), + end_epoch: end_epoch.map(Epoch), + comment: None, + } + } + + fn registry(entries: Vec) -> CircuitVerificationKeyRegistry { + CircuitVerificationKeyRegistry { + version: 1, + entries, + } + } + + mod entry_coverage { + use super::*; + + #[test] + fn covers_inclusive_bounds_of_a_closed_range() { + let entry = entry( + digest(1), + CircuitVerificationKeyStatus::Allowed, + 10, + Some(20), + ); + + assert!(!entry.covers(Epoch(9))); + assert!(entry.covers(Epoch(10))); + assert!(entry.covers(Epoch(20))); + assert!(!entry.covers(Epoch(21))); + } + + #[test] + fn covers_every_epoch_from_start_when_open_ended() { + let entry = entry(digest(1), CircuitVerificationKeyStatus::Allowed, 10, None); + + assert!(!entry.covers(Epoch(9))); + assert!(entry.covers(Epoch(10))); + assert!(entry.covers(Epoch(u64::MAX))); + } + } + + mod check { + use super::*; + + #[test] + fn accepts_digests_covered_by_an_allowed_entry() { + let registry = registry(vec![ + entry( + digest(1), + CircuitVerificationKeyStatus::Allowed, + 10, + Some(20), + ), + entry(digest(2), CircuitVerificationKeyStatus::Allowed, 10, None), + ]); + + registry.check(&[digest(1), digest(2)], Epoch(15)).unwrap(); + } + + #[test] + fn rejects_an_unknown_digest_as_not_whitelisted() { + let registry = registry(vec![entry( + digest(1), + CircuitVerificationKeyStatus::Allowed, + 10, + None, + )]); + + let error = registry.check(&[digest(9)], Epoch(15)).unwrap_err(); + + assert_eq!( + CircuitVerificationKeyRegistryError::NotWhitelisted { + digest: digest(9), + epoch: Epoch(15), + }, + error + ); + } + + #[test] + fn rejects_an_epoch_outside_the_allowed_range_as_not_whitelisted() { + let registry = registry(vec![entry( + digest(1), + CircuitVerificationKeyStatus::Allowed, + 10, + Some(20), + )]); + + let error = registry.check(&[digest(1)], Epoch(21)).unwrap_err(); + + assert_eq!( + CircuitVerificationKeyRegistryError::NotWhitelisted { + digest: digest(1), + epoch: Epoch(21), + }, + error + ); + } + + #[test] + fn revocation_wins_over_an_allowed_entry_covering_the_same_epoch() { + let registry = registry(vec![ + entry(digest(1), CircuitVerificationKeyStatus::Allowed, 10, None), + entry(digest(1), CircuitVerificationKeyStatus::Revoked, 250, None), + ]); + + registry.check(&[digest(1)], Epoch(249)).unwrap(); + let error = registry.check(&[digest(1)], Epoch(250)).unwrap_err(); + + assert_eq!( + CircuitVerificationKeyRegistryError::Revoked { + digest: digest(1), + epoch: Epoch(250), + }, + error + ); + } + + #[test] + fn rejects_when_any_digest_of_the_list_fails() { + let registry = registry(vec![entry( + digest(1), + CircuitVerificationKeyStatus::Allowed, + 10, + None, + )]); + + registry.check(&[digest(1), digest(2)], Epoch(15)).unwrap_err(); + } + + #[test] + fn accepts_an_empty_digest_list() { + let registry = registry(vec![]); + + registry.check(&[], Epoch(15)).unwrap(); + } + } + + mod signature { + use super::*; + + #[test] + fn signed_registry_round_trips_signature_verification() { + let genesis_signer = + GenesisSigner::from_ed25519(GenesisEd25519Signer::create_deterministic_signer()); + let signed_registry = SignedCircuitVerificationKeyRegistry::try_new( + registry(vec![entry( + digest(1), + CircuitVerificationKeyStatus::Allowed, + 10, + None, + )]), + &genesis_signer, + ) + .unwrap(); + + let verified_registry = + signed_registry.verify(&genesis_signer.create_verifier()).unwrap(); + + assert_eq!( + registry(vec![entry( + digest(1), + CircuitVerificationKeyStatus::Allowed, + 10, + None, + )]), + verified_registry + ); + } + + #[test] + fn tampered_registry_fails_signature_verification() { + let genesis_signer = + GenesisSigner::from_ed25519(GenesisEd25519Signer::create_deterministic_signer()); + let mut signed_registry = SignedCircuitVerificationKeyRegistry::try_new( + registry(vec![entry( + digest(1), + CircuitVerificationKeyStatus::Allowed, + 10, + None, + )]), + &genesis_signer, + ) + .unwrap(); + let mut tampered_registry = signed_registry.parse_registry_unverified().unwrap(); + tampered_registry.version = 2; + signed_registry.registry = + RawValue::from_string(serde_json::to_string_pretty(&tampered_registry).unwrap()) + .unwrap(); + + signed_registry + .verify(&genesis_signer.create_verifier()) + .expect_err("a tampered registry must fail signature verification"); + } + + #[test] + fn registry_with_unknown_fields_still_verifies_and_parses() { + let genesis_signer = + GenesisSigner::from_ed25519(GenesisEd25519Signer::create_deterministic_signer()); + let registry_json_with_unknown_field = + r#"{ "version": 1, "entries": [], "a-future-field": true }"#.to_string(); + let signature = + genesis_signer + .ed25519 + .sign(&SignedCircuitVerificationKeyRegistry::signable_bytes( + ®istry_json_with_unknown_field, + )); + let signed_registry = SignedCircuitVerificationKeyRegistry { + registry: RawValue::from_string(registry_json_with_unknown_field).unwrap(), + signature, + }; + + let verified_registry = + signed_registry.verify(&genesis_signer.create_verifier()).unwrap(); + + assert_eq!(1, verified_registry.version); + } + + #[test] + fn signature_from_another_genesis_key_is_rejected() { + let genesis_signer = + GenesisSigner::from_ed25519(GenesisEd25519Signer::create_deterministic_signer()); + let other_verifier = GenesisSigner::from_ed25519( + GenesisEd25519Signer::create_test_signer(ChaCha20Rng::from_seed([7u8; 32])), + ) + .create_verifier(); + let signed_registry = + SignedCircuitVerificationKeyRegistry::try_new(registry(vec![]), &genesis_signer) + .unwrap(); + + signed_registry + .verify(&other_verifier) + .expect_err("a signature from another genesis key must be rejected"); + } + } + + mod serialization { + use super::*; + + #[test] + fn signed_registry_round_trips_through_json() { + let genesis_signer = + GenesisSigner::from_ed25519(GenesisEd25519Signer::create_deterministic_signer()); + let signed_registry = SignedCircuitVerificationKeyRegistry::try_new( + registry(vec![entry( + digest(1), + CircuitVerificationKeyStatus::Revoked, + 10, + Some(20), + )]), + &genesis_signer, + ) + .unwrap(); + + let json = serde_json::to_string(&signed_registry).unwrap(); + let restored: SignedCircuitVerificationKeyRegistry = + serde_json::from_str(&json).unwrap(); + + assert_eq!(signed_registry, restored); + restored.verify(&genesis_signer.create_verifier()).unwrap(); + } + } +} diff --git a/mithril-common/src/crypto_helper/mod.rs b/mithril-common/src/crypto_helper/mod.rs index a2937d2804f..2cbfe120f8e 100644 --- a/mithril-common/src/crypto_helper/mod.rs +++ b/mithril-common/src/crypto_helper/mod.rs @@ -1,6 +1,8 @@ //! Tools and types to abstract the use of the [Mithril STM library](https://mithril.network/rust-doc/mithril_stm/index.html) mod cardano; +#[cfg(feature = "future_snark")] +mod circuit_key_registry; mod codec; mod conversions; pub mod ed25519; @@ -18,6 +20,8 @@ pub use cardano::{ ProtocolInitializerErrorWrapper, ProtocolRegistrationErrorWrapper, SerDeShelleyFileFormat, SignerRegistrationParameters, Sum6KesBytes, }; +#[cfg(feature = "future_snark")] +pub use circuit_key_registry::*; pub use codec::*; pub use ed25519_alias::{era::*, manifest::*, protocol_configuration::*}; pub use genesis::*; From feca547251b065b001b54ddbf320f644028cd8e8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 31 Aug 2026 18:28:03 +0200 Subject: [PATCH 04/12] feat(common): add circuit verification key registry retriever Add the retriever trait returning the unverified signed registry, a file based implementation and a fake test double. --- .../crypto_helper/circuit_key_registry/mod.rs | 2 + .../circuit_key_registry/retriever.rs | 145 ++++++++++++++++++ .../double/circuit_key_registry_retriever.rs | 44 ++++++ mithril-common/src/test/double/mod.rs | 4 + 4 files changed, 195 insertions(+) create mode 100644 mithril-common/src/crypto_helper/circuit_key_registry/retriever.rs create mode 100644 mithril-common/src/test/double/circuit_key_registry_retriever.rs diff --git a/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs b/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs index 2d32f910ebf..75b7e59500a 100644 --- a/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs +++ b/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs @@ -6,6 +6,8 @@ //! the genesis verification key before use. mod registry; +mod retriever; pub use mithril_stm::{CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE, CircuitVerificationKeyDigest}; pub use registry::*; +pub use retriever::*; diff --git a/mithril-common/src/crypto_helper/circuit_key_registry/retriever.rs b/mithril-common/src/crypto_helper/circuit_key_registry/retriever.rs new file mode 100644 index 00000000000..3e1afddacd8 --- /dev/null +++ b/mithril-common/src/crypto_helper/circuit_key_registry/retriever.rs @@ -0,0 +1,145 @@ +//! Retrieval of the signed circuit verification key registry from its published source. + +#[cfg(not(target_family = "wasm"))] +use std::path::PathBuf; + +#[cfg(not(target_family = "wasm"))] +use anyhow::Context; +use async_trait::async_trait; +use thiserror::Error; + +use crate::StdError; +#[cfg(not(target_family = "wasm"))] +use crate::StdResult; + +use super::SignedCircuitVerificationKeyRegistry; + +/// [CircuitVerificationKeyRegistryRetriever] related errors. +#[derive(Debug, Error)] +#[error("Error when retrieving circuit verification key registry")] +pub struct CircuitVerificationKeyRegistryRetrieverError(#[source] pub StdError); + +/// Retrieves the signed circuit verification key registry published at the root of the repository. +/// +/// Implementations return the signed document unverified: the genesis signature and version +/// checks belong to the caller, so an untrusted transport cannot bypass them. +#[cfg_attr(test, mockall::automock)] +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] +pub trait CircuitVerificationKeyRegistryRetriever: Sync + Send { + /// Retrieve the signed registry from its source. + async fn retrieve_signed_registry( + &self, + ) -> Result; +} + +/// A [CircuitVerificationKeyRegistryRetriever] reading the signed registry JSON from a local file. +#[cfg(not(target_family = "wasm"))] +pub struct FileCircuitVerificationKeyRegistryRetriever { + registry_file_path: PathBuf, +} + +#[cfg(not(target_family = "wasm"))] +impl FileCircuitVerificationKeyRegistryRetriever { + /// Build a retriever reading the given signed registry JSON file. + pub fn new(registry_file_path: PathBuf) -> Self { + Self { registry_file_path } + } + + /// Read the signed registry JSON file and parse it. + fn read_and_parse_registry_file( + registry_file_path: &PathBuf, + ) -> StdResult { + let json = std::fs::read_to_string(registry_file_path).with_context(|| { + format!( + "Failed to read signed registry file at '{}'", + registry_file_path.display() + ) + })?; + serde_json::from_str(&json).with_context(|| { + format!( + "Failed to parse signed registry file at '{}'", + registry_file_path.display() + ) + }) + } +} + +#[cfg(not(target_family = "wasm"))] +#[async_trait] +impl CircuitVerificationKeyRegistryRetriever for FileCircuitVerificationKeyRegistryRetriever { + async fn retrieve_signed_registry( + &self, + ) -> Result + { + let registry_file_path = self.registry_file_path.clone(); + tokio::task::spawn_blocking(move || Self::read_and_parse_registry_file(®istry_file_path)) + .await + .map_err(|e| CircuitVerificationKeyRegistryRetrieverError(e.into()))? + .map_err(CircuitVerificationKeyRegistryRetrieverError) + } +} + +#[cfg(test)] +mod tests { + use crate::crypto_helper::{ + CircuitVerificationKeyRegistry, GenesisEd25519Signer, GenesisSigner, + }; + use crate::temp_dir_create; + + use super::*; + + fn signed_registry() -> SignedCircuitVerificationKeyRegistry { + let genesis_signer = + GenesisSigner::from_ed25519(GenesisEd25519Signer::create_deterministic_signer()); + SignedCircuitVerificationKeyRegistry::try_new( + CircuitVerificationKeyRegistry { + version: 1, + entries: vec![], + }, + &genesis_signer, + ) + .unwrap() + } + + #[tokio::test] + async fn file_retriever_reads_a_signed_registry_json_file() { + let temp_dir = temp_dir_create!(); + let registry_file_path = temp_dir.join("signed-registry.json"); + let signed_registry = signed_registry(); + std::fs::write( + ®istry_file_path, + serde_json::to_string(&signed_registry).unwrap(), + ) + .unwrap(); + + let retrieved = FileCircuitVerificationKeyRegistryRetriever::new(registry_file_path) + .retrieve_signed_registry() + .await + .unwrap(); + + assert_eq!(signed_registry, retrieved); + } + + #[tokio::test] + async fn file_retriever_fails_on_a_missing_file() { + let temp_dir = temp_dir_create!(); + + FileCircuitVerificationKeyRegistryRetriever::new(temp_dir.join("missing.json")) + .retrieve_signed_registry() + .await + .expect_err("a missing registry file must fail retrieval"); + } + + #[tokio::test] + async fn file_retriever_fails_on_an_invalid_json_file() { + let temp_dir = temp_dir_create!(); + let registry_file_path = temp_dir.join("signed-registry.json"); + std::fs::write(®istry_file_path, "not a signed registry").unwrap(); + + FileCircuitVerificationKeyRegistryRetriever::new(registry_file_path) + .retrieve_signed_registry() + .await + .expect_err("an invalid registry file must fail retrieval"); + } +} diff --git a/mithril-common/src/test/double/circuit_key_registry_retriever.rs b/mithril-common/src/test/double/circuit_key_registry_retriever.rs new file mode 100644 index 00000000000..7ff338b6781 --- /dev/null +++ b/mithril-common/src/test/double/circuit_key_registry_retriever.rs @@ -0,0 +1,44 @@ +//! A module used for a fake implementation of a circuit verification key registry retriever +//! + +use anyhow::anyhow; +use async_trait::async_trait; + +use crate::crypto_helper::{ + CircuitVerificationKeyRegistryRetriever, CircuitVerificationKeyRegistryRetrieverError, + SignedCircuitVerificationKeyRegistry, +}; + +/// A fake [CircuitVerificationKeyRegistryRetriever] that returns a configured signed registry. +pub struct FakeCircuitVerificationKeyRegistryRetriever { + signed_registry: Option, +} + +impl FakeCircuitVerificationKeyRegistryRetriever { + /// Create a fake retriever returning the given signed registry. + pub fn from_signed_registry(signed_registry: SignedCircuitVerificationKeyRegistry) -> Self { + Self { + signed_registry: Some(signed_registry), + } + } + + /// Create a fake retriever failing every retrieval. + pub fn that_fails() -> Self { + Self { + signed_registry: None, + } + } +} + +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] +impl CircuitVerificationKeyRegistryRetriever for FakeCircuitVerificationKeyRegistryRetriever { + async fn retrieve_signed_registry( + &self, + ) -> Result + { + self.signed_registry.clone().ok_or_else(|| { + CircuitVerificationKeyRegistryRetrieverError(anyhow!("Signed registry not found")) + }) + } +} diff --git a/mithril-common/src/test/double/mod.rs b/mithril-common/src/test/double/mod.rs index 31fe8acc671..e95f5685b85 100644 --- a/mithril-common/src/test/double/mod.rs +++ b/mithril-common/src/test/double/mod.rs @@ -4,6 +4,8 @@ mod api_version; mod certificate_retriever; +#[cfg(feature = "future_snark")] +mod circuit_key_registry_retriever; mod dummies; pub mod fake_data; pub mod fake_keys; @@ -11,6 +13,8 @@ pub(super) mod precomputed_kes_key; pub use api_version::DummyApiVersionDiscriminantSource; pub use certificate_retriever::FakeCertificaterRetriever; +#[cfg(feature = "future_snark")] +pub use circuit_key_registry_retriever::FakeCircuitVerificationKeyRegistryRetriever; /// A trait for giving a type a dummy value. /// From 57f5cd96682bd4c2776d6af5eead78bfdce330c5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Mon, 31 Aug 2026 18:30:05 +0200 Subject: [PATCH 05/12] feat(common): add circuit verification key certifier Lazily retrieve the signed registry, verify its genesis signature, network and minimum version, cache it, then check digests fail-closed. --- .../circuit_key_registry/certifier.rs | 562 ++++++++++++++++++ .../crypto_helper/circuit_key_registry/mod.rs | 2 + 2 files changed, 564 insertions(+) create mode 100644 mithril-common/src/crypto_helper/circuit_key_registry/certifier.rs diff --git a/mithril-common/src/crypto_helper/circuit_key_registry/certifier.rs b/mithril-common/src/crypto_helper/circuit_key_registry/certifier.rs new file mode 100644 index 00000000000..3bc24e567ef --- /dev/null +++ b/mithril-common/src/crypto_helper/circuit_key_registry/certifier.rs @@ -0,0 +1,562 @@ +//! Certifier of circuit verification key digests against the genesis-signed registry. + +use std::sync::Arc; + +use anyhow::{Context, anyhow}; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use thiserror::Error; +use tokio::sync::RwLock; + +use mithril_stm::CircuitVerificationKeyDigest; + +use crate::crypto_helper::GenesisVerifier; +use crate::entities::Epoch; +use crate::{StdError, StdResult}; + +use super::{CircuitVerificationKeyRegistry, CircuitVerificationKeyRegistryRetriever}; + +/// Minimum accepted registry version. +/// +/// Bumped at release time whenever a revocation ships, it bounds rollback attacks replaying an +/// older, genuinely signed registry that would resurrect a revoked key. +pub const MINIMUM_REGISTRY_VERSION: u64 = 1; + +/// Time to live in seconds of the registry cached by +/// [CachedCircuitVerificationKeyCertifier]. +/// +/// Once elapsed, the registry is retrieved and verified again, so a registry updated while a +/// node is running (e.g. a revocation) is picked up without a restart. +pub const REGISTRY_CACHE_TIME_TO_LIVE_IN_SECONDS: i64 = 3600; + +/// Errors raised by a [CircuitVerificationKeyCertifier] when obtaining a trusted registry. +#[derive(Error, Debug)] +pub enum CircuitVerificationKeyCertifierError { + /// The signed registry could not be retrieved from its source. + #[error("circuit verification key registry retrieval failed")] + RegistryRetrieval(#[source] StdError), + + /// The genesis signature of the retrieved registry is invalid, or its signed payload cannot + /// be parsed. + /// + /// A registry published for another network is also rejected here, as each network signs its + /// own registry with its own genesis key. + #[error("circuit verification key registry has an invalid genesis signature")] + InvalidRegistrySignature(#[source] StdError), + + /// The retrieved registry version is below the compiled minimum. + #[error( + "circuit verification key registry version {version} is below the minimum accepted version {minimum_version}" + )] + RegistryVersionBelowMinimum { + /// Version declared by the retrieved registry. + version: u64, + /// Minimum version accepted by this build. + minimum_version: u64, + }, + + /// The refreshed registry version is below the previously verified one. + #[error( + "circuit verification key registry version {version} is below the previously verified version {cached_version}" + )] + RegistryVersionRollback { + /// Version declared by the refreshed registry. + version: u64, + /// Version of the previously verified registry. + cached_version: u64, + }, +} + +/// Certifies circuit verification key digests against the genesis-signed registry. +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] +pub trait CircuitVerificationKeyCertifier: Sync + Send { + /// Obtain the verified registry the digests are checked against. + async fn get_verified_registry(&self) -> StdResult; + + /// Check that every digest is whitelisted and not revoked for the given epoch. + async fn check(&self, digests: &[CircuitVerificationKeyDigest], epoch: Epoch) -> StdResult<()> { + let registry = self.get_verified_registry().await?; + + registry + .check(digests, epoch) + .map_err(|e| anyhow!(e)) + .with_context(|| "Circuit verification key certification failed") + } +} + +/// A [CircuitVerificationKeyCertifier] retrieving and verifying the registry (genesis signature +/// and minimum version) at every use. +/// +/// Wrap it in a [CachedCircuitVerificationKeyCertifier] to avoid retrieving the registry at +/// every check. Fail-closed: any retrieval or verification failure fails the check. +pub struct MithrilCircuitVerificationKeyCertifier { + registry_retriever: Arc, + genesis_verifier: Arc, +} + +impl MithrilCircuitVerificationKeyCertifier { + /// Build a certifier from a registry retriever and the genesis verifier holding the registry + /// signing key, which scopes the registry to its network. + pub fn new( + registry_retriever: Arc, + genesis_verifier: Arc, + ) -> Self { + Self { + registry_retriever, + genesis_verifier, + } + } +} + +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] +impl CircuitVerificationKeyCertifier for MithrilCircuitVerificationKeyCertifier { + async fn get_verified_registry(&self) -> StdResult { + let signed_registry = self + .registry_retriever + .retrieve_signed_registry() + .await + .map_err(|e| CircuitVerificationKeyCertifierError::RegistryRetrieval(e.into()))?; + + let registry = signed_registry + .verify(&self.genesis_verifier) + .map_err(CircuitVerificationKeyCertifierError::InvalidRegistrySignature)?; + if registry.version < MINIMUM_REGISTRY_VERSION { + return Err( + CircuitVerificationKeyCertifierError::RegistryVersionBelowMinimum { + version: registry.version, + minimum_version: MINIMUM_REGISTRY_VERSION, + } + .into(), + ); + } + + Ok(registry) + } +} + +/// A verified registry together with the time it was last obtained. +struct VerifiedRegistryCache { + /// The verified registry. + registry: CircuitVerificationKeyRegistry, + + /// Time the registry was last obtained and verified. + refreshed_at: DateTime, +} + +/// A [CircuitVerificationKeyCertifier] decorator caching the verified registry for +/// [REGISTRY_CACHE_TIME_TO_LIVE_IN_SECONDS]. +/// +/// Once elapsed, the registry is obtained again from the decorated certifier, so a registry +/// updated while the node runs (e.g. a revocation) is picked up without a restart. Fail-closed: +/// a failed refresh fails the check, and a refresh cannot lower the registry version. +pub struct CachedCircuitVerificationKeyCertifier { + certifier: Arc, + cache_time_to_live_in_seconds: i64, + verified_registry_cache: RwLock>, +} + +impl CachedCircuitVerificationKeyCertifier { + /// Build a caching decorator over the given certifier. + pub fn new(certifier: Arc) -> Self { + Self { + certifier, + cache_time_to_live_in_seconds: REGISTRY_CACHE_TIME_TO_LIVE_IN_SECONDS, + verified_registry_cache: RwLock::new(None), + } + } + + #[cfg(test)] + fn with_cache_time_to_live_in_seconds(mut self, cache_time_to_live_in_seconds: i64) -> Self { + self.cache_time_to_live_in_seconds = cache_time_to_live_in_seconds; + self + } + + /// Whether the cached registry is still within its time to live. + /// + /// A negative age (the clock jumped backwards) is treated as stale, so it forces a refresh + /// instead of keeping the cache fresh until the clock catches up. + fn is_cache_fresh(&self, cache: &VerifiedRegistryCache) -> bool { + let age_in_seconds = (Utc::now() - cache.refreshed_at).num_seconds(); + + (0..self.cache_time_to_live_in_seconds).contains(&age_in_seconds) + } +} + +#[cfg_attr(target_family = "wasm", async_trait(?Send))] +#[cfg_attr(not(target_family = "wasm"), async_trait)] +impl CircuitVerificationKeyCertifier for CachedCircuitVerificationKeyCertifier { + async fn get_verified_registry(&self) -> StdResult { + { + let cache = self.verified_registry_cache.read().await; + if let Some(cache) = cache.as_ref() + && self.is_cache_fresh(cache) + { + return Ok(cache.registry.clone()); + } + } + + let mut cache = self.verified_registry_cache.write().await; + if let Some(cache) = cache.as_ref() + && self.is_cache_fresh(cache) + { + return Ok(cache.registry.clone()); + } + + let registry = self.certifier.get_verified_registry().await?; + if let Some(previous_cache) = cache.as_ref() + && registry.version < previous_cache.registry.version + { + return Err( + CircuitVerificationKeyCertifierError::RegistryVersionRollback { + version: registry.version, + cached_version: previous_cache.registry.version, + } + .into(), + ); + } + *cache = Some(VerifiedRegistryCache { + registry: registry.clone(), + refreshed_at: Utc::now(), + }); + + Ok(registry) + } +} + +#[cfg(test)] +mod tests { + use rand_chacha::ChaCha20Rng; + use rand_core::SeedableRng; + + use crate::crypto_helper::circuit_key_registry::retriever::MockCircuitVerificationKeyRegistryRetriever; + use crate::crypto_helper::{ + CircuitVerificationKeyEntry, CircuitVerificationKeyRegistryError, + CircuitVerificationKeyRegistryRetrieverError, CircuitVerificationKeyStatus, + GenesisEd25519Signer, GenesisSigner, SignedCircuitVerificationKeyRegistry, + }; + use crate::test::double::FakeCircuitVerificationKeyRegistryRetriever; + + use super::*; + + fn digest(seed: u8) -> CircuitVerificationKeyDigest { + hex::encode([seed; 32]).parse().unwrap() + } + + fn genesis_signer() -> GenesisSigner { + GenesisSigner::from_ed25519(GenesisEd25519Signer::create_deterministic_signer()) + } + + fn registry_allowing( + digests: &[CircuitVerificationKeyDigest], + ) -> CircuitVerificationKeyRegistry { + CircuitVerificationKeyRegistry { + version: MINIMUM_REGISTRY_VERSION, + entries: digests + .iter() + .map(|digest| CircuitVerificationKeyEntry { + digest: *digest, + name: "circuit".to_string(), + status: CircuitVerificationKeyStatus::Allowed, + start_epoch: Epoch(0), + end_epoch: None, + comment: None, + }) + .collect(), + } + } + + mod mithril_certifier { + use super::*; + + fn certifier_over( + registry: CircuitVerificationKeyRegistry, + genesis_signer: &GenesisSigner, + ) -> MithrilCircuitVerificationKeyCertifier { + let signed_registry = + SignedCircuitVerificationKeyRegistry::try_new(registry, genesis_signer).unwrap(); + MithrilCircuitVerificationKeyCertifier::new( + Arc::new( + FakeCircuitVerificationKeyRegistryRetriever::from_signed_registry( + signed_registry, + ), + ), + Arc::new(genesis_signer.create_verifier()), + ) + } + + #[tokio::test] + async fn check_succeeds_with_a_whitelisted_digest() { + let genesis_signer = genesis_signer(); + let certifier = certifier_over(registry_allowing(&[digest(1)]), &genesis_signer); + + certifier.check(&[digest(1)], Epoch(10)).await.unwrap(); + } + + #[tokio::test] + async fn check_propagates_registry_check_errors() { + let genesis_signer = genesis_signer(); + let certifier = certifier_over(registry_allowing(&[digest(1)]), &genesis_signer); + + let error = certifier.check(&[digest(9)], Epoch(10)).await.unwrap_err(); + + assert_eq!( + error.downcast_ref::(), + Some(&CircuitVerificationKeyRegistryError::NotWhitelisted { + digest: digest(9), + epoch: Epoch(10), + }), + "the registry check error must be preserved, got: {error}" + ); + } + + #[tokio::test] + async fn check_fails_closed_when_retrieval_fails() { + let genesis_signer = genesis_signer(); + let certifier = MithrilCircuitVerificationKeyCertifier::new( + Arc::new(FakeCircuitVerificationKeyRegistryRetriever::that_fails()), + Arc::new(genesis_signer.create_verifier()), + ); + + let error = certifier.check(&[digest(1)], Epoch(10)).await.unwrap_err(); + + assert!( + matches!( + error.downcast_ref::(), + Some(CircuitVerificationKeyCertifierError::RegistryRetrieval(_)) + ), + "a retrieval failure must fail the check, got: {error}" + ); + } + + #[tokio::test] + async fn check_rejects_a_registry_signed_by_another_genesis_key() { + let genesis_signer = genesis_signer(); + let other_genesis_signer = GenesisSigner::from_ed25519( + GenesisEd25519Signer::create_test_signer(ChaCha20Rng::from_seed([7u8; 32])), + ); + let signed_registry = SignedCircuitVerificationKeyRegistry::try_new( + registry_allowing(&[digest(1)]), + &other_genesis_signer, + ) + .unwrap(); + let certifier = MithrilCircuitVerificationKeyCertifier::new( + Arc::new( + FakeCircuitVerificationKeyRegistryRetriever::from_signed_registry( + signed_registry, + ), + ), + Arc::new(genesis_signer.create_verifier()), + ); + + let error = certifier.check(&[digest(1)], Epoch(10)).await.unwrap_err(); + + assert!( + matches!( + error.downcast_ref::(), + Some(CircuitVerificationKeyCertifierError::InvalidRegistrySignature(_)) + ), + "a registry signed by another genesis key must be rejected, got: {error}" + ); + } + + #[tokio::test] + async fn check_rejects_a_registry_version_below_the_minimum() { + let genesis_signer = genesis_signer(); + let mut registry = registry_allowing(&[digest(1)]); + registry.version = MINIMUM_REGISTRY_VERSION - 1; + let certifier = certifier_over(registry, &genesis_signer); + + let error = certifier.check(&[digest(1)], Epoch(10)).await.unwrap_err(); + + assert!( + matches!( + error.downcast_ref::(), + Some( + CircuitVerificationKeyCertifierError::RegistryVersionBelowMinimum { + version: 0, + minimum_version: MINIMUM_REGISTRY_VERSION, + } + ) + ), + "a registry version below the minimum must be rejected, got: {error}" + ); + } + + #[tokio::test] + async fn check_retrieves_and_verifies_the_registry_at_every_use() { + let genesis_signer = genesis_signer(); + let signed_registry = SignedCircuitVerificationKeyRegistry::try_new( + registry_allowing(&[digest(1)]), + &genesis_signer, + ) + .unwrap(); + let mut registry_retriever = MockCircuitVerificationKeyRegistryRetriever::new(); + registry_retriever + .expect_retrieve_signed_registry() + .times(2) + .returning(move || Ok(signed_registry.clone())); + let certifier = MithrilCircuitVerificationKeyCertifier::new( + Arc::new(registry_retriever), + Arc::new(genesis_signer.create_verifier()), + ); + + certifier.check(&[digest(1)], Epoch(10)).await.unwrap(); + certifier.check(&[digest(1)], Epoch(11)).await.unwrap(); + } + } + + mod cached_certifier { + use super::*; + + fn cached_certifier_over_retriever( + registry_retriever: MockCircuitVerificationKeyRegistryRetriever, + genesis_signer: &GenesisSigner, + ) -> CachedCircuitVerificationKeyCertifier { + CachedCircuitVerificationKeyCertifier::new(Arc::new( + MithrilCircuitVerificationKeyCertifier::new( + Arc::new(registry_retriever), + Arc::new(genesis_signer.create_verifier()), + ), + )) + } + + #[tokio::test] + async fn check_retrieves_and_verifies_the_registry_only_once_within_the_time_to_live() { + let genesis_signer = genesis_signer(); + let signed_registry = SignedCircuitVerificationKeyRegistry::try_new( + registry_allowing(&[digest(1)]), + &genesis_signer, + ) + .unwrap(); + let mut registry_retriever = MockCircuitVerificationKeyRegistryRetriever::new(); + registry_retriever + .expect_retrieve_signed_registry() + .times(1) + .return_once(move || Ok(signed_registry)); + let certifier = cached_certifier_over_retriever(registry_retriever, &genesis_signer); + + certifier.check(&[digest(1)], Epoch(10)).await.unwrap(); + certifier.check(&[digest(1)], Epoch(11)).await.unwrap(); + } + + #[test] + fn a_cache_refreshed_in_the_future_is_stale() { + let genesis_signer = genesis_signer(); + let certifier = CachedCircuitVerificationKeyCertifier::new(Arc::new( + MithrilCircuitVerificationKeyCertifier::new( + Arc::new(FakeCircuitVerificationKeyRegistryRetriever::that_fails()), + Arc::new(genesis_signer.create_verifier()), + ), + )); + let cache = VerifiedRegistryCache { + registry: registry_allowing(&[digest(1)]), + refreshed_at: Utc::now() + chrono::Duration::hours(2), + }; + + assert!( + !certifier.is_cache_fresh(&cache), + "a cache refreshed in the future (backwards clock jump) must be stale" + ); + } + + #[tokio::test] + async fn check_refreshes_the_registry_after_the_cache_time_to_live_expires() { + let genesis_signer = genesis_signer(); + let signed_registry = SignedCircuitVerificationKeyRegistry::try_new( + registry_allowing(&[digest(1)]), + &genesis_signer, + ) + .unwrap(); + let mut registry_retriever = MockCircuitVerificationKeyRegistryRetriever::new(); + registry_retriever + .expect_retrieve_signed_registry() + .times(2) + .returning(move || Ok(signed_registry.clone())); + let certifier = cached_certifier_over_retriever(registry_retriever, &genesis_signer) + .with_cache_time_to_live_in_seconds(-1); + + certifier.check(&[digest(1)], Epoch(10)).await.unwrap(); + certifier.check(&[digest(1)], Epoch(11)).await.unwrap(); + } + + #[tokio::test] + async fn check_rejects_a_refreshed_registry_with_a_lower_version() { + let genesis_signer = genesis_signer(); + let mut newer_registry = registry_allowing(&[digest(1)]); + newer_registry.version = MINIMUM_REGISTRY_VERSION + 1; + let newer_signed_registry = + SignedCircuitVerificationKeyRegistry::try_new(newer_registry, &genesis_signer) + .unwrap(); + let older_signed_registry = SignedCircuitVerificationKeyRegistry::try_new( + registry_allowing(&[digest(1)]), + &genesis_signer, + ) + .unwrap(); + let mut registry_retriever = MockCircuitVerificationKeyRegistryRetriever::new(); + registry_retriever + .expect_retrieve_signed_registry() + .times(1) + .return_once(move || Ok(newer_signed_registry)); + registry_retriever + .expect_retrieve_signed_registry() + .times(1) + .return_once(move || Ok(older_signed_registry)); + let certifier = cached_certifier_over_retriever(registry_retriever, &genesis_signer) + .with_cache_time_to_live_in_seconds(-1); + + certifier.check(&[digest(1)], Epoch(10)).await.unwrap(); + let error = certifier.check(&[digest(1)], Epoch(11)).await.unwrap_err(); + + assert!( + matches!( + error.downcast_ref::(), + Some( + CircuitVerificationKeyCertifierError::RegistryVersionRollback { + version: 1, + cached_version: 2, + } + ) + ), + "a refreshed registry with a lower version must be rejected, got: {error}" + ); + } + + #[tokio::test] + async fn check_fails_closed_when_the_refresh_fails() { + let genesis_signer = genesis_signer(); + let signed_registry = SignedCircuitVerificationKeyRegistry::try_new( + registry_allowing(&[digest(1)]), + &genesis_signer, + ) + .unwrap(); + let mut registry_retriever = MockCircuitVerificationKeyRegistryRetriever::new(); + registry_retriever + .expect_retrieve_signed_registry() + .times(1) + .return_once(move || Ok(signed_registry)); + registry_retriever + .expect_retrieve_signed_registry() + .times(1) + .return_once(|| { + Err(CircuitVerificationKeyRegistryRetrieverError(anyhow!( + "registry source unreachable" + ))) + }); + let certifier = cached_certifier_over_retriever(registry_retriever, &genesis_signer) + .with_cache_time_to_live_in_seconds(-1); + + certifier.check(&[digest(1)], Epoch(10)).await.unwrap(); + let error = certifier.check(&[digest(1)], Epoch(11)).await.unwrap_err(); + + assert!( + matches!( + error.downcast_ref::(), + Some(CircuitVerificationKeyCertifierError::RegistryRetrieval(_)) + ), + "a failed refresh must fail the check, got: {error}" + ); + } + } +} diff --git a/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs b/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs index 75b7e59500a..18d8d7c5c38 100644 --- a/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs +++ b/mithril-common/src/crypto_helper/circuit_key_registry/mod.rs @@ -5,9 +5,11 @@ //! the repository per network, retrieved at runtime and verified against the Ed25519 half of //! the genesis verification key before use. +mod certifier; mod registry; mod retriever; +pub use certifier::*; pub use mithril_stm::{CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE, CircuitVerificationKeyDigest}; pub use registry::*; pub use retriever::*; From 272af8d6e80631bb267e7e93e360d814f140fd83 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 8 Sep 2026 15:42:19 +0200 Subject: [PATCH 06/12] fix(stm): hash the circuit verification key bytes with SHA-256 before Poseidon The Poseidon hasher must absorb field elements, so the canonical key bytes are pre-hashed with SHA-256 as elsewhere in the crate. --- .../src/circuits/verification_key_digest.rs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/mithril-stm/src/circuits/verification_key_digest.rs b/mithril-stm/src/circuits/verification_key_digest.rs index 24607b3e2af..21f8dc264e4 100644 --- a/mithril-stm/src/circuits/verification_key_digest.rs +++ b/mithril-stm/src/circuits/verification_key_digest.rs @@ -1,10 +1,11 @@ //! Opaque digest identifying a circuit verification key. //! -//! The digest is computed as a Poseidon hash over the canonical byte serialization of a -//! verifying key. Poseidon is SNARK-friendly and native to the scalar field of the circuits, so -//! the digest computation stays cheap if the registry check is ever proven in-circuit. It lets -//! callers reference a circuit verification key, for example in a signed registry, without -//! carrying the key itself or depending on its internal structure. +//! The digest is computed as a Poseidon hash over the SHA-256 hash of the canonical byte +//! serialization of a verifying key, so the Poseidon hasher absorbs a single field element, as +//! byte strings are fed to it elsewhere in the crate. Poseidon is SNARK-friendly and native to +//! the scalar field of the circuits, so the digest computation stays cheap if the registry check +//! is ever proven in-circuit. It lets callers reference a circuit verification key, for example +//! in a signed registry, without carrying the key itself or depending on its internal structure. use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -12,6 +13,7 @@ use std::str::FromStr; use anyhow::{Context, anyhow}; use digest::Digest; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use sha2::Sha256; use crate::circuits::halo2::NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION; use crate::circuits::halo2_ivc::RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION; @@ -22,7 +24,8 @@ use crate::{MithrilMembershipDigest, Parameters, StmError, StmResult, codec::Try /// Byte length of a circuit verification key digest. pub const CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE: usize = 32; -/// Poseidon digest of the canonical byte serialization of a circuit verification key. +/// Poseidon digest of the SHA-256 hash of the canonical byte serialization of a circuit +/// verification key. /// /// Serialized as a lowercase hex string. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -38,10 +41,12 @@ impl CircuitVerificationKeyDigest { )) } - /// Compute the digest of a verifying key already in its canonical byte serialization. + /// Compute the digest of a verifying key already in its canonical byte serialization, hashing + /// the bytes with SHA-256 first so the Poseidon hasher absorbs a single field element. fn from_canonical_key_bytes(canonical_key_bytes: &[u8]) -> Self { + let canonical_key_bytes_hash: [u8; 32] = Sha256::digest(canonical_key_bytes).into(); let mut hasher = MidnightPoseidonDigest::new(); - hasher.update(canonical_key_bytes); + hasher.update(canonical_key_bytes_hash); Self(hasher.finalize().into()) } @@ -162,7 +167,7 @@ mod tests { } #[test] - fn digest_is_poseidon_hash_of_canonical_key_bytes() { + fn digest_is_poseidon_hash_of_the_sha256_hash_of_canonical_key_bytes() { let context = load_embedded_verification_context_asset() .expect("verification context asset should load"); @@ -171,8 +176,10 @@ mod tests { ) .unwrap(); + let canonical_key_bytes_hash: [u8; 32] = + Sha256::digest(context.certificate_verifying_key.to_bytes_vec().unwrap()).into(); let mut hasher = MidnightPoseidonDigest::new(); - hasher.update(context.certificate_verifying_key.to_bytes_vec().unwrap()); + hasher.update(canonical_key_bytes_hash); let expected: [u8; CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE] = hasher.finalize().into(); assert_eq!(&expected, digest.as_bytes()); @@ -242,7 +249,7 @@ mod tests { .unwrap(); assert_eq!( - "5cfbcf921d5b29e3d449c5ecd707dd78924612790a27a68610c1cf0af3b7cc52", + "9e68083f22b192e8c0ec6a62c2904ec546128a7bf4bb45138be8fa1a114e1f00", digest.to_string(), "golden circuit verification key digest changed for a fixed input, this alters the digest computation and breaks published circuit verification key registries" ); @@ -251,12 +258,12 @@ mod tests { #[test] fn golden_digests_of_production_circuit_keys() { assert_eq!( - "1264305828d13c48a7b85b0cf472198d5a8014d8c06b50e9f4dd9c586249355c", + "beca1c3e5b14ba8b74bad0177e1d762078473b33aaacb7017dd7425c53f61d25", CircuitVerificationKeyDigest::for_production_certificate_circuit().to_string(), "golden production certificate circuit verification key digest changed, either the digest computation or the embedded production key changed, which breaks published circuit verification key registries" ); assert_eq!( - "e2077c751852ee5a4e0908b7b963e037bac64f6fd0bf1af2ff1f012e0aa57e57", + "cf0e9d63b167d81431b96bdf71bdaa7d0d947f134329cfacaa9d4e31794c3069", CircuitVerificationKeyDigest::for_ivc_circuit().to_string(), "golden IVC circuit verification key digest changed, either the digest computation or the embedded production key changed, which breaks published circuit verification key registries" ); @@ -302,12 +309,12 @@ mod tests { .unwrap(); assert_eq!( - "1ecb40d6ba62504520a904ae053f7ad6747ce2ebafd7facef3fb4009d52b6336", + "653471392ada496934d7752f9b483efd92ae6c3271af636945c4b4ce74ae316c", certificate_key_digest.to_string(), "golden certificate circuit verification key digest changed, either the digest computation or the canonical key serialization changed, which breaks published circuit verification key registries" ); assert_eq!( - "08174d68b60d5655d0def90e6d3680f8bd6216a1c714bc3c6111d7c1d8472a28", + "770a223fac0f319f0a76990b2c71a6faa7e3525c29d37eb80bf9fe5d5406b221", recursive_key_digest.to_string(), "golden IVC circuit verification key digest changed, either the digest computation or the canonical key serialization changed, which breaks published circuit verification key registries" ); From 78bfa08eb486545a36057d99613058aacb482e68 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 8 Sep 2026 16:10:36 +0200 Subject: [PATCH 07/12] fix(stm): digest the transcript representation of circuit verification keys The serialized key bytes omit the recursive circuit constraint system, so the digest covers the transcript representation that binds the gates. --- .../src/circuits/verification_key_digest.rs | 201 ++++++++---------- .../aggregate_signature/ancillary_data.rs | 30 ++- 2 files changed, 97 insertions(+), 134 deletions(-) diff --git a/mithril-stm/src/circuits/verification_key_digest.rs b/mithril-stm/src/circuits/verification_key_digest.rs index 21f8dc264e4..b3689a26596 100644 --- a/mithril-stm/src/circuits/verification_key_digest.rs +++ b/mithril-stm/src/circuits/verification_key_digest.rs @@ -1,11 +1,14 @@ //! Opaque digest identifying a circuit verification key. //! -//! The digest is computed as a Poseidon hash over the SHA-256 hash of the canonical byte -//! serialization of a verifying key, so the Poseidon hasher absorbs a single field element, as -//! byte strings are fed to it elsewhere in the crate. Poseidon is SNARK-friendly and native to -//! the scalar field of the circuits, so the digest computation stays cheap if the registry check -//! is ever proven in-circuit. It lets callers reference a circuit verification key, for example -//! in a signed registry, without carrying the key itself or depending on its internal structure. +//! The digest is computed as a Poseidon hash over the transcript representation of a verifying +//! key: the field element Halo2 derives from the pinned constraint system, the evaluation domain +//! and the fixed and permutation commitments. The serialized key bytes are not enough to identify +//! a circuit, as the recursive key serialization omits the constraint system, reconstructed from +//! the circuit code when the key is read, so two circuits with different gates can share the same +//! bytes. Poseidon is SNARK-friendly and native to the scalar field of the circuits, so the digest +//! computation stays cheap if the registry check is ever proven in-circuit. It lets callers +//! reference a circuit verification key, for example in a signed registry, without carrying the +//! key itself or depending on its internal structure. use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -13,40 +16,42 @@ use std::str::FromStr; use anyhow::{Context, anyhow}; use digest::Digest; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use sha2::Sha256; use crate::circuits::halo2::NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION; -use crate::circuits::halo2_ivc::RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION; +use crate::circuits::halo2::keys::NonRecursiveCircuitVerifyingKey; +use crate::circuits::halo2_ivc::keys::RecursiveCircuitVerifyingKey; +use crate::circuits::halo2_ivc::{ + KZGCommitmentScheme, NativeField, PairingEngine, + RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, VerifyingKey, +}; use crate::hash::poseidon::MidnightPoseidonDigest; use crate::proof_system::{NonDeterministicSnarkProverFactory, SnarkProverFactory}; -use crate::{MithrilMembershipDigest, Parameters, StmError, StmResult, codec::TryToBytes}; +use crate::{MithrilMembershipDigest, Parameters, StmError, StmResult, codec::TryFromBytes}; /// Byte length of a circuit verification key digest. pub const CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE: usize = 32; -/// Poseidon digest of the SHA-256 hash of the canonical byte serialization of a circuit -/// verification key. +/// Poseidon digest of the transcript representation of a circuit verification key. /// /// Serialized as a lowercase hex string. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct CircuitVerificationKeyDigest([u8; CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE]); impl CircuitVerificationKeyDigest { - /// Compute the digest of a verifying key from its canonical byte serialization. - pub(crate) fn try_from_verification_key( + /// Compute the digest of a verifying key from its transcript representation. + pub(crate) fn from_verification_key< + K: AsRef>>, + >( verification_key: &K, - ) -> StmResult { - Ok(Self::from_canonical_key_bytes( - &verification_key.to_bytes_vec()?, - )) + ) -> Self { + Self::from_transcript_representation(verification_key.as_ref().transcript_repr()) } - /// Compute the digest of a verifying key already in its canonical byte serialization, hashing - /// the bytes with SHA-256 first so the Poseidon hasher absorbs a single field element. - fn from_canonical_key_bytes(canonical_key_bytes: &[u8]) -> Self { - let canonical_key_bytes_hash: [u8; 32] = Sha256::digest(canonical_key_bytes).into(); + /// Compute the digest of the transcript representation of a verifying key, a field element + /// the Poseidon hasher absorbs as is. + fn from_transcript_representation(transcript_representation: NativeField) -> Self { let mut hasher = MidnightPoseidonDigest::new(); - hasher.update(canonical_key_bytes_hash); + hasher.update(transcript_representation.to_bytes_le()); Self(hasher.finalize().into()) } @@ -54,8 +59,12 @@ impl CircuitVerificationKeyDigest { /// /// The IVC circuit does not depend on the protocol parameters, so its verification key is the /// embedded production constant for every deployment. - pub fn for_ivc_circuit() -> Self { - Self::from_canonical_key_bytes(RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION) + pub fn for_ivc_circuit() -> StmResult { + Ok(Self::from_verification_key( + &RecursiveCircuitVerifyingKey::try_from_bytes( + RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, + )?, + )) } /// Digest of the embedded certificate circuit verification key generated for the production @@ -63,8 +72,12 @@ impl CircuitVerificationKeyDigest { /// /// The certificate circuit depends on the protocol parameters, so this digest only covers /// deployments running with the production parameters. - pub fn for_production_certificate_circuit() -> Self { - Self::from_canonical_key_bytes(NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION) + pub fn for_production_certificate_circuit() -> StmResult { + Ok(Self::from_verification_key( + &NonRecursiveCircuitVerifyingKey::try_from_bytes( + NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, + )?, + )) } /// Compute the digest of the certificate circuit verification key for the given protocol @@ -79,7 +92,7 @@ impl CircuitVerificationKeyDigest { parameters, )?; - Self::try_from_verification_key(prover.verifying_key()) + Ok(Self::from_verification_key(prover.verifying_key())) } /// Return the digest bytes. @@ -136,11 +149,11 @@ mod tests { fn print_circuit_verification_key_digests_for_production() { println!( "certificate-circuit (production protocol parameters): {}", - CircuitVerificationKeyDigest::for_production_certificate_circuit() + CircuitVerificationKeyDigest::for_production_certificate_circuit().unwrap() ); println!( "ivc-circuit: {}", - CircuitVerificationKeyDigest::for_ivc_circuit() + CircuitVerificationKeyDigest::for_ivc_circuit().unwrap() ); } @@ -149,37 +162,33 @@ mod tests { let context = load_embedded_verification_context_asset() .expect("verification context asset should load"); - let certificate_key_digest = CircuitVerificationKeyDigest::try_from_verification_key( - &context.certificate_verifying_key, - ) - .unwrap(); - let certificate_key_digest_again = CircuitVerificationKeyDigest::try_from_verification_key( - &context.certificate_verifying_key, - ) - .unwrap(); - let recursive_key_digest = CircuitVerificationKeyDigest::try_from_verification_key( - &context.recursive_verifying_key, - ) - .unwrap(); + let certificate_key_digest = + CircuitVerificationKeyDigest::from_verification_key(&context.certificate_verifying_key); + let certificate_key_digest_again = + CircuitVerificationKeyDigest::from_verification_key(&context.certificate_verifying_key); + let recursive_key_digest = + CircuitVerificationKeyDigest::from_verification_key(&context.recursive_verifying_key); assert_eq!(certificate_key_digest, certificate_key_digest_again); assert_ne!(certificate_key_digest, recursive_key_digest); } #[test] - fn digest_is_poseidon_hash_of_the_sha256_hash_of_canonical_key_bytes() { + fn digest_is_poseidon_hash_of_the_verification_key_transcript_representation() { let context = load_embedded_verification_context_asset() .expect("verification context asset should load"); - let digest = CircuitVerificationKeyDigest::try_from_verification_key( - &context.certificate_verifying_key, - ) - .unwrap(); + let digest = + CircuitVerificationKeyDigest::from_verification_key(&context.certificate_verifying_key); - let canonical_key_bytes_hash: [u8; 32] = - Sha256::digest(context.certificate_verifying_key.to_bytes_vec().unwrap()).into(); let mut hasher = MidnightPoseidonDigest::new(); - hasher.update(canonical_key_bytes_hash); + hasher.update( + context + .certificate_verifying_key + .as_ref() + .transcript_repr() + .to_bytes_le(), + ); let expected: [u8; CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE] = hasher.finalize().into(); assert_eq!(&expected, digest.as_bytes()); @@ -189,10 +198,8 @@ mod tests { fn digest_round_trips_through_hex_string() { let context = load_embedded_verification_context_asset() .expect("verification context asset should load"); - let digest = CircuitVerificationKeyDigest::try_from_verification_key( - &context.certificate_verifying_key, - ) - .unwrap(); + let digest = + CircuitVerificationKeyDigest::from_verification_key(&context.certificate_verifying_key); let hex_string = digest.to_string(); let restored: CircuitVerificationKeyDigest = hex_string.parse().unwrap(); @@ -204,10 +211,8 @@ mod tests { fn digest_round_trips_through_serde_json() { let context = load_embedded_verification_context_asset() .expect("verification context asset should load"); - let digest = CircuitVerificationKeyDigest::try_from_verification_key( - &context.certificate_verifying_key, - ) - .unwrap(); + let digest = + CircuitVerificationKeyDigest::from_verification_key(&context.certificate_verifying_key); let json = serde_json::to_string(&digest).unwrap(); let restored: CircuitVerificationKeyDigest = serde_json::from_str(&json).unwrap(); @@ -227,70 +232,34 @@ mod tests { } mod golden { - use crate::circuits::halo2::keys::NonRecursiveCircuitVerifyingKey; - use crate::circuits::halo2_ivc::keys::RecursiveCircuitVerifyingKey; - use crate::codec::TryFromBytes; - use super::*; - struct FixedBytesVerificationKey(Vec); - - impl TryToBytes for FixedBytesVerificationKey { - fn to_bytes_vec(&self) -> StmResult> { - Ok(self.0.clone()) - } - } - #[test] - fn golden_digest_of_fixed_verification_key_bytes() { - let digest = CircuitVerificationKeyDigest::try_from_verification_key( - &FixedBytesVerificationKey(vec![42u8; 64]), - ) - .unwrap(); + fn golden_digest_of_fixed_transcript_representation() { + let digest = CircuitVerificationKeyDigest::from_transcript_representation( + NativeField::from(42u64), + ); assert_eq!( - "9e68083f22b192e8c0ec6a62c2904ec546128a7bf4bb45138be8fa1a114e1f00", + "ea21d013415b00dc1a74ea17cd305efc640a941fb3aa662a059923ea6aaece36", digest.to_string(), - "golden circuit verification key digest changed for a fixed input, this alters the digest computation and breaks published circuit verification key registries" + "golden circuit verification key digest changed for a fixed transcript representation, this alters the digest computation and breaks published circuit verification key registries" ); } #[test] fn golden_digests_of_production_circuit_keys() { assert_eq!( - "beca1c3e5b14ba8b74bad0177e1d762078473b33aaacb7017dd7425c53f61d25", - CircuitVerificationKeyDigest::for_production_certificate_circuit().to_string(), - "golden production certificate circuit verification key digest changed, either the digest computation or the embedded production key changed, which breaks published circuit verification key registries" + "23109dde1bbcc5293159e1299434761221bc3131d4476ddf67155edccf06c219", + CircuitVerificationKeyDigest::for_production_certificate_circuit() + .unwrap() + .to_string(), + "golden production certificate circuit verification key digest changed, either the digest computation, the embedded production key or the certificate circuit changed, which breaks published circuit verification key registries" ); assert_eq!( - "cf0e9d63b167d81431b96bdf71bdaa7d0d947f134329cfacaa9d4e31794c3069", - CircuitVerificationKeyDigest::for_ivc_circuit().to_string(), - "golden IVC circuit verification key digest changed, either the digest computation or the embedded production key changed, which breaks published circuit verification key registries" - ); - } - - #[test] - fn production_digests_match_the_deserialized_embedded_production_keys() { - let certificate_verifying_key = NonRecursiveCircuitVerifyingKey::try_from_bytes( - NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, - ) - .unwrap(); - let recursive_verifying_key = RecursiveCircuitVerifyingKey::try_from_bytes( - RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, - ) - .unwrap(); - - assert_eq!( - CircuitVerificationKeyDigest::try_from_verification_key(&certificate_verifying_key) - .unwrap(), - CircuitVerificationKeyDigest::for_production_certificate_circuit(), - "the embedded production certificate key constant must stay the canonical key serialization" - ); - assert_eq!( - CircuitVerificationKeyDigest::try_from_verification_key(&recursive_verifying_key) - .unwrap(), - CircuitVerificationKeyDigest::for_ivc_circuit(), - "the embedded IVC key constant must stay the canonical key serialization" + "91e3fa784a720632b294f0becc2cbc1262274c6b36e2d2da1b716031751ec369", + CircuitVerificationKeyDigest::for_ivc_circuit().unwrap().to_string(), + "golden IVC circuit verification key digest changed, either the digest computation, the embedded production key or the IVC circuit changed, which breaks published circuit verification key registries" ); } @@ -299,24 +268,22 @@ mod tests { let context = load_embedded_verification_context_asset() .expect("verification context asset should load"); - let certificate_key_digest = CircuitVerificationKeyDigest::try_from_verification_key( + let certificate_key_digest = CircuitVerificationKeyDigest::from_verification_key( &context.certificate_verifying_key, - ) - .unwrap(); - let recursive_key_digest = CircuitVerificationKeyDigest::try_from_verification_key( + ); + let recursive_key_digest = CircuitVerificationKeyDigest::from_verification_key( &context.recursive_verifying_key, - ) - .unwrap(); + ); assert_eq!( - "653471392ada496934d7752f9b483efd92ae6c3271af636945c4b4ce74ae316c", + "ddbcb7ac2fc177e397166cc49314c73f9638787db6db90f287d3490451378159", certificate_key_digest.to_string(), - "golden certificate circuit verification key digest changed, either the digest computation or the canonical key serialization changed, which breaks published circuit verification key registries" + "golden certificate circuit verification key digest changed, either the digest computation or the circuit changed, which breaks published circuit verification key registries" ); assert_eq!( - "770a223fac0f319f0a76990b2c71a6faa7e3525c29d37eb80bf9fe5d5406b221", + "af6087f9a37517c1024d67685b34f052bb534830a772f81117a85b75ae59b20a", recursive_key_digest.to_string(), - "golden IVC circuit verification key digest changed, either the digest computation or the canonical key serialization changed, which breaks published circuit verification key registries" + "golden IVC circuit verification key digest changed, either the digest computation or the circuit changed, which breaks published circuit verification key registries" ); } } diff --git a/mithril-stm/src/protocol/aggregate_signature/ancillary_data.rs b/mithril-stm/src/protocol/aggregate_signature/ancillary_data.rs index ce613ca68a7..3e48fe01f0c 100644 --- a/mithril-stm/src/protocol/aggregate_signature/ancillary_data.rs +++ b/mithril-stm/src/protocol/aggregate_signature/ancillary_data.rs @@ -135,21 +135,21 @@ impl AncillaryVerifierData { /// These are the keys the proof is verified against, so certifying them certifies the /// circuits used to produce the aggregate signature. #[cfg(feature = "future_snark")] - pub fn circuit_verification_key_digests(&self) -> StmResult> { + pub fn circuit_verification_key_digests(&self) -> Vec { match self { - Self::IvcSnark(ivc_verifier_data) => Ok(vec![ - CircuitVerificationKeyDigest::try_from_verification_key( + Self::IvcSnark(ivc_verifier_data) => vec![ + CircuitVerificationKeyDigest::from_verification_key( ivc_verifier_data.certificate_circuit_verification_key(), - )?, - CircuitVerificationKeyDigest::try_from_verification_key( + ), + CircuitVerificationKeyDigest::from_verification_key( ivc_verifier_data.ivc_circuit_verification_key(), - )?, - ]), - Self::Snark(snark_verifier_data) => Ok(vec![ - CircuitVerificationKeyDigest::try_from_verification_key( + ), + ], + Self::Snark(snark_verifier_data) => { + vec![CircuitVerificationKeyDigest::from_verification_key( snark_verifier_data.certificate_circuit_verification_key(), - )?, - ]), + )] + } } } } @@ -473,12 +473,8 @@ mod tests { let snark_ancillary_verifier_data = AncillaryVerifierData::Snark(SnarkVerifierData::new(context.certificate_verifying_key)); - let ivc_digests = ivc_ancillary_verifier_data - .circuit_verification_key_digests() - .unwrap(); - let snark_digests = snark_ancillary_verifier_data - .circuit_verification_key_digests() - .unwrap(); + let ivc_digests = ivc_ancillary_verifier_data.circuit_verification_key_digests(); + let snark_digests = snark_ancillary_verifier_data.circuit_verification_key_digests(); assert_eq!(2, ivc_digests.len()); assert_ne!(ivc_digests[0], ivc_digests[1]); From ab585f35c2b23f478cc70f5ab99332d465d608af Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 8 Sep 2026 16:17:01 +0200 Subject: [PATCH 08/12] fix(stm): domain separate the circuit verification key digest Prepend a dedicated Poseidon domain separation tag (CVKD_DST) to the transcript representation, following the existing tag convention. --- .../src/circuits/verification_key_digest.rs | 54 ++++++++++--------- mithril-stm/src/signature_scheme/mod.rs | 2 + .../schnorr_signature/jubjub/mod.rs | 5 +- .../jubjub/poseidon_digest.rs | 36 ++++++++----- 4 files changed, 55 insertions(+), 42 deletions(-) diff --git a/mithril-stm/src/circuits/verification_key_digest.rs b/mithril-stm/src/circuits/verification_key_digest.rs index b3689a26596..86544034eeb 100644 --- a/mithril-stm/src/circuits/verification_key_digest.rs +++ b/mithril-stm/src/circuits/verification_key_digest.rs @@ -1,8 +1,8 @@ //! Opaque digest identifying a circuit verification key. //! -//! The digest is computed as a Poseidon hash over the transcript representation of a verifying -//! key: the field element Halo2 derives from the pinned constraint system, the evaluation domain -//! and the fixed and permutation commitments. The serialized key bytes are not enough to identify +//! The digest is computed as a Poseidon hash, under a dedicated domain separation tag, over the +//! transcript representation of a verifying key: the field element Halo2 derives from the pinned +//! constraint system, the evaluation domain and the fixed and permutation commitments. The serialized key bytes are not enough to identify //! a circuit, as the recursive key serialization omits the constraint system, reconstructed from //! the circuit code when the key is read, so two circuits with different gates can share the same //! bytes. Poseidon is SNARK-friendly and native to the scalar field of the circuits, so the digest @@ -14,7 +14,6 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; use anyhow::{Context, anyhow}; -use digest::Digest; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::circuits::halo2::NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION; @@ -24,14 +23,18 @@ use crate::circuits::halo2_ivc::{ KZGCommitmentScheme, NativeField, PairingEngine, RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, VerifyingKey, }; -use crate::hash::poseidon::MidnightPoseidonDigest; use crate::proof_system::{NonDeterministicSnarkProverFactory, SnarkProverFactory}; +use crate::signature_scheme::{ + BaseFieldElement, DOMAIN_SEPARATION_TAG_CIRCUIT_VERIFICATION_KEY_DIGEST, + compute_poseidon_digest, +}; use crate::{MithrilMembershipDigest, Parameters, StmError, StmResult, codec::TryFromBytes}; /// Byte length of a circuit verification key digest. pub const CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE: usize = 32; -/// Poseidon digest of the transcript representation of a circuit verification key. +/// Domain separated Poseidon digest of the transcript representation of a circuit verification +/// key. /// /// Serialized as a lowercase hex string. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -48,11 +51,14 @@ impl CircuitVerificationKeyDigest { } /// Compute the digest of the transcript representation of a verifying key, a field element - /// the Poseidon hasher absorbs as is. + /// the Poseidon hash absorbs as is after the domain separation tag. fn from_transcript_representation(transcript_representation: NativeField) -> Self { - let mut hasher = MidnightPoseidonDigest::new(); - hasher.update(transcript_representation.to_bytes_le()); - Self(hasher.finalize().into()) + let digest = compute_poseidon_digest(&[ + DOMAIN_SEPARATION_TAG_CIRCUIT_VERIFICATION_KEY_DIGEST, + BaseFieldElement(transcript_representation), + ]); + + Self(digest.to_bytes()) } /// Digest of the IVC circuit verification key. @@ -174,24 +180,20 @@ mod tests { } #[test] - fn digest_is_poseidon_hash_of_the_verification_key_transcript_representation() { + fn digest_is_domain_separated_poseidon_hash_of_the_verification_key_transcript_representation() + { let context = load_embedded_verification_context_asset() .expect("verification context asset should load"); let digest = CircuitVerificationKeyDigest::from_verification_key(&context.certificate_verifying_key); - let mut hasher = MidnightPoseidonDigest::new(); - hasher.update( - context - .certificate_verifying_key - .as_ref() - .transcript_repr() - .to_bytes_le(), - ); - let expected: [u8; CIRCUIT_VERIFICATION_KEY_DIGEST_SIZE] = hasher.finalize().into(); + let expected = compute_poseidon_digest(&[ + DOMAIN_SEPARATION_TAG_CIRCUIT_VERIFICATION_KEY_DIGEST, + BaseFieldElement(context.certificate_verifying_key.as_ref().transcript_repr()), + ]); - assert_eq!(&expected, digest.as_bytes()); + assert_eq!(&expected.to_bytes(), digest.as_bytes()); } #[test] @@ -241,7 +243,7 @@ mod tests { ); assert_eq!( - "ea21d013415b00dc1a74ea17cd305efc640a941fb3aa662a059923ea6aaece36", + "f1d28fe496eacf78e36379ddd2ce01b8705dfd6ece4894a8db25aba6cb38312d", digest.to_string(), "golden circuit verification key digest changed for a fixed transcript representation, this alters the digest computation and breaks published circuit verification key registries" ); @@ -250,14 +252,14 @@ mod tests { #[test] fn golden_digests_of_production_circuit_keys() { assert_eq!( - "23109dde1bbcc5293159e1299434761221bc3131d4476ddf67155edccf06c219", + "b4f2e431d9b6f016d6c551a3b6ae9f2b6b494941181eabdafd1313fc7c240f25", CircuitVerificationKeyDigest::for_production_certificate_circuit() .unwrap() .to_string(), "golden production certificate circuit verification key digest changed, either the digest computation, the embedded production key or the certificate circuit changed, which breaks published circuit verification key registries" ); assert_eq!( - "91e3fa784a720632b294f0becc2cbc1262274c6b36e2d2da1b716031751ec369", + "d4c87805251f4bb7e68dde64c18d0da544cc9c2385983074fada3e753dbf6423", CircuitVerificationKeyDigest::for_ivc_circuit().unwrap().to_string(), "golden IVC circuit verification key digest changed, either the digest computation, the embedded production key or the IVC circuit changed, which breaks published circuit verification key registries" ); @@ -276,12 +278,12 @@ mod tests { ); assert_eq!( - "ddbcb7ac2fc177e397166cc49314c73f9638787db6db90f287d3490451378159", + "61026abdb434dacb969e2b0dd6ff3c49de3f2eba759d967fce011bd519372714", certificate_key_digest.to_string(), "golden certificate circuit verification key digest changed, either the digest computation or the circuit changed, which breaks published circuit verification key registries" ); assert_eq!( - "af6087f9a37517c1024d67685b34f052bb534830a772f81117a85b75ae59b20a", + "d3688e5681a2a35a218006bd07f2be2ff8991a35249ac5646f9d6ba042d4ac0c", recursive_key_digest.to_string(), "golden IVC circuit verification key digest changed, either the digest computation or the circuit changed, which breaks published circuit verification key registries" ); diff --git a/mithril-stm/src/signature_scheme/mod.rs b/mithril-stm/src/signature_scheme/mod.rs index 294192af881..50243c16d01 100644 --- a/mithril-stm/src/signature_scheme/mod.rs +++ b/mithril-stm/src/signature_scheme/mod.rs @@ -4,6 +4,8 @@ mod schnorr_signature; pub use bls_multi_signature::*; +#[cfg(feature = "future_snark")] +pub(crate) use schnorr_signature::DOMAIN_SEPARATION_TAG_CIRCUIT_VERIFICATION_KEY_DIGEST; #[cfg(feature = "future_snark")] pub(crate) use schnorr_signature::DOMAIN_SEPARATION_TAG_LOTTERY; #[cfg(feature = "future_snark")] diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/mod.rs b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/mod.rs index a6aea521bcf..7405402c5a3 100644 --- a/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/mod.rs +++ b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/mod.rs @@ -6,8 +6,9 @@ pub(crate) use curve_points::*; pub use field_elements::BaseFieldElement; pub(crate) use field_elements::ScalarFieldElement; pub(crate) use poseidon_digest::{ - DOMAIN_SEPARATION_TAG_LOTTERY, DOMAIN_SEPARATION_TAG_STANDARD_SIGNATURE, - DOMAIN_SEPARATION_TAG_UNIQUE_SIGNATURE, compute_poseidon_digest, + DOMAIN_SEPARATION_TAG_CIRCUIT_VERIFICATION_KEY_DIGEST, DOMAIN_SEPARATION_TAG_LOTTERY, + DOMAIN_SEPARATION_TAG_STANDARD_SIGNATURE, DOMAIN_SEPARATION_TAG_UNIQUE_SIGNATURE, + compute_poseidon_digest, }; use serde::{ diff --git a/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/poseidon_digest.rs b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/poseidon_digest.rs index 550346b7bb1..ac77861af41 100644 --- a/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/poseidon_digest.rs +++ b/mithril-stm/src/signature_scheme/schnorr_signature/jubjub/poseidon_digest.rs @@ -23,8 +23,6 @@ pub(crate) const DOMAIN_SEPARATION_TAG_STANDARD_SIGNATURE: BaseFieldElement = /// Domain Separation Tag (DST) for the lottery check. It is used as a prefix when computing /// the eligibility value of a signature. -// TODO: remove this allow dead_code directive when function is called or future_snark is activated -#[allow(dead_code)] pub const DOMAIN_SEPARATION_TAG_LOTTERY: BaseFieldElement = BaseFieldElement(JubjubBase::from_raw([ 0x4C4F_5454_5F44_5354, // "LOTT_DST" (ASCII), little-endian u64 @@ -33,6 +31,16 @@ pub const DOMAIN_SEPARATION_TAG_LOTTERY: BaseFieldElement = 0, ])); +/// Domain Separation Tag (DST) for the Poseidon hash computing the digest of a circuit +/// verification key. +pub(crate) const DOMAIN_SEPARATION_TAG_CIRCUIT_VERIFICATION_KEY_DIGEST: BaseFieldElement = + BaseFieldElement(JubjubBase::from_raw([ + 0x4356_4B44_5F44_5354, // "CVKD_DST" (ASCII), little-endian u64 + 0, + 0, + 0, + ])); + /// Computes a Poseidon digest over the provided base field elements. /// Returns a base field element as the digest. pub(crate) fn compute_poseidon_digest(input: &[BaseFieldElement]) -> BaseFieldElement { @@ -44,24 +52,24 @@ pub(crate) fn compute_poseidon_digest(input: &[BaseFieldElement]) -> BaseFieldEl #[cfg(test)] mod test { use super::{ - DOMAIN_SEPARATION_TAG_LOTTERY, DOMAIN_SEPARATION_TAG_STANDARD_SIGNATURE, - DOMAIN_SEPARATION_TAG_UNIQUE_SIGNATURE, + DOMAIN_SEPARATION_TAG_CIRCUIT_VERIFICATION_KEY_DIGEST, DOMAIN_SEPARATION_TAG_LOTTERY, + DOMAIN_SEPARATION_TAG_STANDARD_SIGNATURE, DOMAIN_SEPARATION_TAG_UNIQUE_SIGNATURE, }; #[test] fn domain_separation_tags_are_pairwise_distinct() { - assert_ne!( - DOMAIN_SEPARATION_TAG_UNIQUE_SIGNATURE, - DOMAIN_SEPARATION_TAG_STANDARD_SIGNATURE - ); - assert_ne!( + let tags = [ DOMAIN_SEPARATION_TAG_UNIQUE_SIGNATURE, - DOMAIN_SEPARATION_TAG_LOTTERY - ); - assert_ne!( DOMAIN_SEPARATION_TAG_STANDARD_SIGNATURE, - DOMAIN_SEPARATION_TAG_LOTTERY - ); + DOMAIN_SEPARATION_TAG_LOTTERY, + DOMAIN_SEPARATION_TAG_CIRCUIT_VERIFICATION_KEY_DIGEST, + ]; + + for (index, tag) in tags.iter().enumerate() { + for other_tag in &tags[index + 1..] { + assert_ne!(tag, other_tag); + } + } } mod golden { From 29906fc8fb3ff449b28d529c81e2fc8f1ff35ff8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 8 Sep 2026 16:36:38 +0200 Subject: [PATCH 09/12] fix(stm): remove infallible destructuring matches in concatenation proof tests Use the aggregate signature accessor instead, which also compiles cleanly without the future_snark feature. --- .../src/proof_system/concatenation/proof.rs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/mithril-stm/src/proof_system/concatenation/proof.rs b/mithril-stm/src/proof_system/concatenation/proof.rs index 98ae4be1afd..0de9be3e857 100644 --- a/mithril-stm/src/proof_system/concatenation/proof.rs +++ b/mithril-stm/src/proof_system/concatenation/proof.rs @@ -594,11 +594,9 @@ mod tests { #[test] fn cbor_encoding_is_stable() { let aggregate = golden_aggregate_signature(); - let proof = match aggregate { - AggregateSignature::Concatenation(proof) => proof, - #[cfg(feature = "future_snark")] - _ => panic!("Expected Concatenation variant"), - }; + let proof = aggregate + .to_concatenation_proof() + .expect("Expected Concatenation variant"); let bytes = proof .to_bytes() .expect("ConcatenationProof serialization should not fail"); @@ -608,21 +606,17 @@ mod tests { #[test] fn cbor_encoding_is_deterministic() { let aggregate_1 = golden_aggregate_signature(); - let proof_1 = match aggregate_1 { - AggregateSignature::Concatenation(proof) => proof, - #[cfg(feature = "future_snark")] - _ => panic!("Expected Concatenation variant"), - }; + let proof_1 = aggregate_1 + .to_concatenation_proof() + .expect("Expected Concatenation variant"); let bytes_1 = proof_1 .to_bytes() .expect("ConcatenationProof serialization should not fail"); let aggregate_2 = golden_aggregate_signature(); - let proof_2 = match aggregate_2 { - AggregateSignature::Concatenation(proof) => proof, - #[cfg(feature = "future_snark")] - _ => panic!("Expected Concatenation variant"), - }; + let proof_2 = aggregate_2 + .to_concatenation_proof() + .expect("Expected Concatenation variant"); let bytes_2 = proof_2 .to_bytes() .expect("ConcatenationProof serialization should not fail"); From ff16cd1eaa646c943283125173634eadfe46cb22 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Tue, 8 Sep 2026 17:42:00 +0200 Subject: [PATCH 10/12] fix(common): report every rejected circuit verification key digest The registry check collects all rejections with their reason in a single error variant instead of stopping at the first rejected digest. --- .../circuit_key_registry/certifier.rs | 12 +- .../circuit_key_registry/registry.rs | 197 +++++++++++++----- 2 files changed, 152 insertions(+), 57 deletions(-) diff --git a/mithril-common/src/crypto_helper/circuit_key_registry/certifier.rs b/mithril-common/src/crypto_helper/circuit_key_registry/certifier.rs index 3bc24e567ef..e13e725377d 100644 --- a/mithril-common/src/crypto_helper/circuit_key_registry/certifier.rs +++ b/mithril-common/src/crypto_helper/circuit_key_registry/certifier.rs @@ -233,8 +233,9 @@ mod tests { use crate::crypto_helper::circuit_key_registry::retriever::MockCircuitVerificationKeyRegistryRetriever; use crate::crypto_helper::{ CircuitVerificationKeyEntry, CircuitVerificationKeyRegistryError, - CircuitVerificationKeyRegistryRetrieverError, CircuitVerificationKeyStatus, - GenesisEd25519Signer, GenesisSigner, SignedCircuitVerificationKeyRegistry, + CircuitVerificationKeyRegistryRetrieverError, CircuitVerificationKeyRejection, + CircuitVerificationKeyRejectionReason, CircuitVerificationKeyStatus, GenesisEd25519Signer, + GenesisSigner, SignedCircuitVerificationKeyRegistry, }; use crate::test::double::FakeCircuitVerificationKeyRegistryRetriever; @@ -303,9 +304,12 @@ mod tests { assert_eq!( error.downcast_ref::(), - Some(&CircuitVerificationKeyRegistryError::NotWhitelisted { - digest: digest(9), + Some(&CircuitVerificationKeyRegistryError::Rejected { epoch: Epoch(10), + rejections: vec![CircuitVerificationKeyRejection { + digest: digest(9), + reason: CircuitVerificationKeyRejectionReason::NotWhitelisted, + }], }), "the registry check error must be preserved, got: {error}" ); diff --git a/mithril-common/src/crypto_helper/circuit_key_registry/registry.rs b/mithril-common/src/crypto_helper/circuit_key_registry/registry.rs index 12b31e9dc26..e575ba7132f 100644 --- a/mithril-common/src/crypto_helper/circuit_key_registry/registry.rs +++ b/mithril-common/src/crypto_helper/circuit_key_registry/registry.rs @@ -1,5 +1,7 @@ //! Genesis-signed registry of the circuit verification keys trusted for SNARK certificates. +use std::fmt::{Display, Formatter}; + use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; use thiserror::Error; @@ -14,23 +16,58 @@ use crate::entities::Epoch; /// [CircuitVerificationKeyRegistry]. #[derive(Error, Debug, PartialEq, Eq)] pub enum CircuitVerificationKeyRegistryError { - /// The digest is covered by a revoked entry for the checked epoch. - #[error("circuit verification key '{digest}' is revoked for epoch {epoch}")] - Revoked { - /// Digest of the revoked circuit verification key. - digest: CircuitVerificationKeyDigest, + /// Some digests are revoked or not whitelisted for the checked epoch. + #[error("circuit verification keys rejected for epoch {epoch}: {}", CircuitVerificationKeyRejection::join(.rejections))] + Rejected { /// Epoch for which the check was performed. epoch: Epoch, + /// Rejected digests with their reason, in the checked order. + rejections: Vec, }, +} - /// The digest is not covered by any allowed entry for the checked epoch. - #[error("circuit verification key '{digest}' is not whitelisted for epoch {epoch}")] - NotWhitelisted { - /// Digest of the unknown or out-of-range circuit verification key. - digest: CircuitVerificationKeyDigest, - /// Epoch for which the check was performed. - epoch: Epoch, - }, +/// Reason for which a circuit verification key digest is rejected for an epoch. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CircuitVerificationKeyRejectionReason { + /// The digest is covered by a revoked entry. + Revoked, + + /// The digest is not covered by any allowed entry. + NotWhitelisted, +} + +/// A circuit verification key digest rejected by the registry, with the reason. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CircuitVerificationKeyRejection { + /// Rejected digest. + pub digest: CircuitVerificationKeyDigest, + + /// Reason of the rejection. + pub reason: CircuitVerificationKeyRejectionReason, +} + +impl CircuitVerificationKeyRejection { + /// Join the rejections in a single comma separated line. + fn join(rejections: &[Self]) -> String { + rejections + .iter() + .map(ToString::to_string) + .collect::>() + .join(", ") + } +} + +impl Display for CircuitVerificationKeyRejection { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.reason { + CircuitVerificationKeyRejectionReason::Revoked => { + write!(f, "'{}' is revoked", self.digest) + } + CircuitVerificationKeyRejectionReason::NotWhitelisted => { + write!(f, "'{}' is not whitelisted", self.digest) + } + } + } } /// Status of a circuit verification key entry over its epoch range. @@ -89,40 +126,56 @@ pub struct CircuitVerificationKeyRegistry { } impl CircuitVerificationKeyRegistry { - /// Check that every digest is whitelisted and not revoked for the given epoch. + /// Check that every digest is whitelisted and not revoked for the given epoch, reporting + /// every rejected digest at once. /// - /// A digest fails with [Revoked](CircuitVerificationKeyRegistryError::Revoked) when any - /// revoked entry covers the epoch, and with - /// [NotWhitelisted](CircuitVerificationKeyRegistryError::NotWhitelisted) when no allowed + /// A digest is rejected as [Revoked](CircuitVerificationKeyRejectionReason::Revoked) when + /// any revoked entry covers the epoch, and as + /// [NotWhitelisted](CircuitVerificationKeyRejectionReason::NotWhitelisted) when no allowed /// entry covers it. pub fn check( &self, digests: &[CircuitVerificationKeyDigest], epoch: Epoch, ) -> Result<(), CircuitVerificationKeyRegistryError> { - digests.iter().try_for_each(|digest| { - let is_revoked = self.has_covering_entry_with_status( - digest, - epoch, - CircuitVerificationKeyStatus::Revoked, - ); - let is_allowed = self.has_covering_entry_with_status( - digest, - epoch, - CircuitVerificationKeyStatus::Allowed, - ); + let rejections: Vec = digests + .iter() + .filter_map(|digest| self.find_digest_rejection(digest, epoch)) + .collect(); - match (is_revoked, is_allowed) { - (true, _) => Err(CircuitVerificationKeyRegistryError::Revoked { - digest: *digest, - epoch, - }), - (false, false) => Err(CircuitVerificationKeyRegistryError::NotWhitelisted { - digest: *digest, - epoch, - }), - (false, true) => Ok(()), - } + if rejections.is_empty() { + Ok(()) + } else { + Err(CircuitVerificationKeyRegistryError::Rejected { epoch, rejections }) + } + } + + /// Find why the digest is rejected for the epoch, if it is: revocation wins over an allowed + /// entry covering the same epoch. + fn find_digest_rejection( + &self, + digest: &CircuitVerificationKeyDigest, + epoch: Epoch, + ) -> Option { + let is_revoked = self.has_covering_entry_with_status( + digest, + epoch, + CircuitVerificationKeyStatus::Revoked, + ); + let is_allowed = self.has_covering_entry_with_status( + digest, + epoch, + CircuitVerificationKeyStatus::Allowed, + ); + let reason = match (is_revoked, is_allowed) { + (true, _) => Some(CircuitVerificationKeyRejectionReason::Revoked), + (false, false) => Some(CircuitVerificationKeyRejectionReason::NotWhitelisted), + (false, true) => None, + }; + + reason.map(|reason| CircuitVerificationKeyRejection { + digest: *digest, + reason, }) } @@ -282,6 +335,13 @@ mod tests { mod check { use super::*; + fn rejection( + digest: CircuitVerificationKeyDigest, + reason: CircuitVerificationKeyRejectionReason, + ) -> CircuitVerificationKeyRejection { + CircuitVerificationKeyRejection { digest, reason } + } + #[test] fn accepts_digests_covered_by_an_allowed_entry() { let registry = registry(vec![ @@ -309,9 +369,12 @@ mod tests { let error = registry.check(&[digest(9)], Epoch(15)).unwrap_err(); assert_eq!( - CircuitVerificationKeyRegistryError::NotWhitelisted { - digest: digest(9), + CircuitVerificationKeyRegistryError::Rejected { epoch: Epoch(15), + rejections: vec![rejection( + digest(9), + CircuitVerificationKeyRejectionReason::NotWhitelisted + )], }, error ); @@ -329,9 +392,12 @@ mod tests { let error = registry.check(&[digest(1)], Epoch(21)).unwrap_err(); assert_eq!( - CircuitVerificationKeyRegistryError::NotWhitelisted { - digest: digest(1), + CircuitVerificationKeyRegistryError::Rejected { epoch: Epoch(21), + rejections: vec![rejection( + digest(1), + CircuitVerificationKeyRejectionReason::NotWhitelisted + )], }, error ); @@ -348,24 +414,49 @@ mod tests { let error = registry.check(&[digest(1)], Epoch(250)).unwrap_err(); assert_eq!( - CircuitVerificationKeyRegistryError::Revoked { - digest: digest(1), + CircuitVerificationKeyRegistryError::Rejected { epoch: Epoch(250), + rejections: vec![rejection( + digest(1), + CircuitVerificationKeyRejectionReason::Revoked + )], }, error ); } #[test] - fn rejects_when_any_digest_of_the_list_fails() { - let registry = registry(vec![entry( - digest(1), - CircuitVerificationKeyStatus::Allowed, - 10, - None, - )]); + fn reports_every_rejected_digest_of_the_list_with_its_reason() { + let registry = registry(vec![ + entry(digest(1), CircuitVerificationKeyStatus::Allowed, 10, None), + entry(digest(2), CircuitVerificationKeyStatus::Revoked, 10, None), + ]); + + let error = registry + .check(&[digest(1), digest(2), digest(3)], Epoch(15)) + .unwrap_err(); - registry.check(&[digest(1), digest(2)], Epoch(15)).unwrap_err(); + assert_eq!( + CircuitVerificationKeyRegistryError::Rejected { + epoch: Epoch(15), + rejections: vec![ + rejection(digest(2), CircuitVerificationKeyRejectionReason::Revoked), + rejection( + digest(3), + CircuitVerificationKeyRejectionReason::NotWhitelisted + ), + ], + }, + error + ); + assert_eq!( + format!( + "circuit verification keys rejected for epoch 15: '{}' is revoked, '{}' is not whitelisted", + digest(2), + digest(3) + ), + error.to_string() + ); } #[test] From f73959557cf740e56576585b9ebce66ddcb4298f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Wed, 9 Sep 2026 17:38:47 +0200 Subject: [PATCH 11/12] chore: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3e761dbc9..c26dc8cf1aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ As a minor extension, we have adopted a slightly different versioning convention - Reworked the Mithril aggregator's file archiver to produce byte-stable archives across systems. - Existing archives must be regenerated by the Mithril aggregator to ensure byte stability. - Preliminary support for uploading immutable files to IPFS with the Mithril aggregator and downloading them from IPFS with the Mithril client library and CLI. + - Preliminary support for the circuit verification key registry, a genesis-signed whitelist (with revocations) of the circuit verification keys trusted for SNARK certificates. - **REMOVED** support for `Gzip` compression/decompression in the Mithril aggregator and client: - The aggregator no longer produces or supports `Gzip` compression for snapshot-related artifacts: immutable files and ancillaries. From 4330db7907fa49fbb8f0521256ecc60307034653 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Raynaud Date: Wed, 9 Sep 2026 17:40:20 +0200 Subject: [PATCH 12/12] chore: upgrade crate versions * mithril-common from `0.7.20` to `0.7.21` * mithril-stm from `0.12.15` to `0.12.16` --- Cargo.lock | 4 ++-- .../mithril-cardano-node-internal-database/Cargo.toml | 2 +- internal/mithril-aggregator-client/Cargo.toml | 2 +- internal/mithril-aggregator-discovery/Cargo.toml | 2 +- mithril-client/Cargo.toml | 2 +- mithril-common/Cargo.toml | 4 ++-- mithril-stm/Cargo.toml | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2fa95b4ab8f..3cecdd4b637 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4513,7 +4513,7 @@ dependencies = [ [[package]] name = "mithril-common" -version = "0.7.20" +version = "0.7.21" dependencies = [ "anyhow", "async-trait", @@ -4834,7 +4834,7 @@ dependencies = [ [[package]] name = "mithril-stm" -version = "0.12.15" +version = "0.12.16" dependencies = [ "anyhow", "blake2 0.10.6", diff --git a/internal/cardano-node/mithril-cardano-node-internal-database/Cargo.toml b/internal/cardano-node/mithril-cardano-node-internal-database/Cargo.toml index 244d1882754..436e7cfab32 100644 --- a/internal/cardano-node/mithril-cardano-node-internal-database/Cargo.toml +++ b/internal/cardano-node/mithril-cardano-node-internal-database/Cargo.toml @@ -15,7 +15,7 @@ anyhow = { workspace = true } async-trait = { workspace = true } digest = { workspace = true } hex = { workspace = true } -mithril-common = { path = "../../../mithril-common", version = "0.7.20" } +mithril-common = { path = "../../../mithril-common", version = "0.7.21" } serde = { workspace = true } serde_json = { workspace = true } sha2 = "0.10.9" diff --git a/internal/mithril-aggregator-client/Cargo.toml b/internal/mithril-aggregator-client/Cargo.toml index 1d769319524..7c3000ee94e 100644 --- a/internal/mithril-aggregator-client/Cargo.toml +++ b/internal/mithril-aggregator-client/Cargo.toml @@ -13,7 +13,7 @@ include = ["**/*.rs", "Cargo.toml", "README.md"] [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } -mithril-common = { path = "../../mithril-common", version = "0.7.20" } +mithril-common = { path = "../../mithril-common", version = "0.7.21" } reqwest = { workspace = true } semver = { workspace = true } serde = { workspace = true } diff --git a/internal/mithril-aggregator-discovery/Cargo.toml b/internal/mithril-aggregator-discovery/Cargo.toml index 85817f10fcf..0c693a32b85 100644 --- a/internal/mithril-aggregator-discovery/Cargo.toml +++ b/internal/mithril-aggregator-discovery/Cargo.toml @@ -14,7 +14,7 @@ include = ["**/*.rs", "Cargo.toml", "README.md", ".gitignore"] anyhow = { workspace = true } async-trait = { workspace = true } mithril-aggregator-client = { path = "../mithril-aggregator-client", version = "0.2.4" } -mithril-common = { path = "../../mithril-common", version = "0.7.20" } +mithril-common = { path = "../../mithril-common", version = "0.7.21" } rand = { version = "0.10.2" } reqwest = { workspace = true } serde = { workspace = true } diff --git a/mithril-client/Cargo.toml b/mithril-client/Cargo.toml index 82315d274d6..53c90a344f4 100644 --- a/mithril-client/Cargo.toml +++ b/mithril-client/Cargo.toml @@ -63,7 +63,7 @@ chrono = { workspace = true } flume = { version = "0.12.0", optional = true } futures = "0.3.32" mithril-aggregator-client = { path = "../internal/mithril-aggregator-client", version = "0.2.4" } -mithril-common = { path = "../mithril-common", version = "0.7.20", default-features = false } +mithril-common = { path = "../mithril-common", version = "0.7.21", default-features = false } reqwest = { workspace = true, default-features = false, features = ["charset", "http2", "query", "stream", "system-proxy"] } serde = { workspace = true } serde_json = { workspace = true } diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index 4db2a0399bf..2e61e4d1c81 100644 --- a/mithril-common/Cargo.toml +++ b/mithril-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-common" -version = "0.7.20" +version = "0.7.21" description = "Common types, interfaces, and utilities for Mithril nodes." authors = { workspace = true } edition = { workspace = true } @@ -51,7 +51,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.12.15", default-features = false } +mithril-stm = { path = "../mithril-stm", version = "0.12.16", default-features = false } nom = "8.0.0" rand_chacha = { workspace = true } rand_core = { workspace = true } diff --git a/mithril-stm/Cargo.toml b/mithril-stm/Cargo.toml index 7657ef5abae..44e22994c4b 100644 --- a/mithril-stm/Cargo.toml +++ b/mithril-stm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-stm" -version = "0.12.15" +version = "0.12.16" edition = { workspace = true } authors = { workspace = true } homepage = { workspace = true }