Skip to content
8 changes: 8 additions & 0 deletions mithril-stm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.11.5 (07-27-2026)

### Changed

- Updated the `from_bytes_legacy` functions for `MerkleTreeBatchCommitment`, `MerkleTree`, `ConcatenationProof` and `SingleSignature`
- Updated the validation of the `phi_f` value in the lottery/eligibility computations
- Updated the visibility of `ClosedKeyRegistration`, `RegistrationEntry` and `ClosedRegistrationEntry` to ensure proper verification of the proof of possession

## 0.11.4 (07-27-2026)

### Added
Expand Down
7 changes: 3 additions & 4 deletions mithril-stm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ use rayon::prelude::*;

use mithril_stm::{
AggregateSignatureType, AggregationError, AncillaryGenesisData, AncillaryProofInput, Clerk,
Initializer, KeyRegistration, Parameters, RegistrationEntry, Signer, SingleSignature,
Initializer, KeyRegistration, Parameters, Signer, SingleSignature,
MithrilMembershipDigest, AggregateVerificationKey,
};

Expand Down Expand Up @@ -100,13 +100,12 @@ let mut key_reg = KeyRegistration::initialize();
let mut ps: Vec<Initializer> = Vec::with_capacity(nparties as usize);
for stake in parties {
let p = Initializer::new(params, stake, &mut rng);
let entry = RegistrationEntry::new(
p.get_verification_key_proof_of_possession_for_concatenation(),
key_reg.register(
p.stake,
&p.get_verification_key_proof_of_possession_for_concatenation(),
#[cfg(feature = "future_snark")] p.schnorr_verification_key,
)
.unwrap();
key_reg.register_by_entry(&entry).unwrap();
ps.push(p);
}

Expand Down
9 changes: 8 additions & 1 deletion mithril-stm/benches/size_benches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ where
let mut key_reg = KeyRegistration::initialize();
for stake in parties {
let p = Initializer::new(params, stake, &mut rng);
key_reg.register_by_entry(&p.clone().try_into().unwrap()).unwrap();
key_reg
.register(
stake,
&p.get_verification_key_proof_of_possession_for_concatenation(),
#[cfg(feature = "future_snark")]
p.schnorr_verification_key,
)
.unwrap();
ps.push(p);
}

Expand Down
18 changes: 16 additions & 2 deletions mithril-stm/benches/stm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ fn stm_benches<D: MembershipDigest>(
// We need to initialise the key_reg at each iteration
key_reg = KeyRegistration::initialize();
for p in initializers.iter() {
key_reg.register_by_entry(&p.clone().try_into().unwrap()).unwrap();
key_reg
.register(
p.stake,
&p.get_verification_key_proof_of_possession_for_concatenation(),
#[cfg(feature = "future_snark")]
p.schnorr_verification_key,
)
.unwrap();
}
})
});
Expand Down Expand Up @@ -136,7 +143,14 @@ fn batch_benches<D>(
}
let mut key_reg = KeyRegistration::initialize();
for p in initializers.iter() {
key_reg.register_by_entry(&p.clone().try_into().unwrap()).unwrap();
key_reg
.register(
p.stake,
&p.get_verification_key_proof_of_possession_for_concatenation(),
#[cfg(feature = "future_snark")]
p.schnorr_verification_key,
)
.unwrap();
}

let closed_reg = key_reg.close_registration(&params).unwrap();
Expand Down
18 changes: 9 additions & 9 deletions mithril-stm/examples/key_registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use rand_core::{RngCore, SeedableRng};
use mithril_stm::{
AggregateSignature, AggregateSignatureType, AncillaryGenesisData, AncillaryProofInput, Clerk,
ClosedKeyRegistration, Initializer, KeyRegistration, MithrilMembershipDigest, Parameters,
RegistrationEntry, Stake, VerificationKeyProofOfPossessionForConcatenation,
Stake, VerificationKeyProofOfPossessionForConcatenation,
};

type D = MithrilMembershipDigest;
Expand Down Expand Up @@ -236,14 +236,14 @@ fn local_reg(
};
// data, such as the public key, stake and id.
for (pk, _) in pks.iter().zip(ids.iter()) {
let entry = RegistrationEntry::new(
*pk,
1,
#[cfg(feature = "future_snark")]
None,
)
.unwrap();
local_keyreg.register_by_entry(&entry).unwrap();
local_keyreg
.register(
1,
pk,
#[cfg(feature = "future_snark")]
None,
)
.unwrap();
}
local_keyreg.close_registration(&params).unwrap()
}
14 changes: 7 additions & 7 deletions mithril-stm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
//!
//! use mithril_stm::{
//! AggregateSignatureType, AggregationError, AncillaryGenesisData, AncillaryProofInput, Clerk,
//! Initializer, KeyRegistration, Parameters, RegistrationEntry, Signer, SingleSignature,
//! Initializer, KeyRegistration, Parameters, Signer, SingleSignature,
//! MithrilMembershipDigest,
//! };
//!
Expand Down Expand Up @@ -59,13 +59,12 @@
//! // Create keys for this party
//! let p = Initializer::new(params, stake, &mut rng);
//! // Register keys with the KeyRegistration service
//! let entry = RegistrationEntry::new(
//! p.get_verification_key_proof_of_possession_for_concatenation(),
//! key_reg.register(
//! p.stake,
//! &p.get_verification_key_proof_of_possession_for_concatenation(),
//! #[cfg(feature = "future_snark")] p.schnorr_verification_key,
//! )
//! .unwrap();
//! key_reg.register_by_entry(&entry).unwrap();
//! ps.push(p);
//! }
//!
Expand Down Expand Up @@ -148,14 +147,15 @@ mod protocol;
mod signature_scheme;

pub use proof_system::AggregateVerificationKeyForConcatenation;
pub(crate) use protocol::RegistrationEntry;
pub use protocol::{
AggregateSignature, AggregateSignatureError, AggregateSignatureType, AggregateVerificationKey,
AggregationError, AncillaryGenesisData, AncillaryProofInput, AncillaryProofOutput,
AncillaryProverData, AncillaryVerifierData, Clerk, ClosedKeyRegistration,
ClosedRegistrationEntry, GenesisVerificationKeyBundle, Initializer, KeyRegistration,
Parameters, RegisterError, RegistrationEntry, RegistrationEntryForConcatenation,
SignatureError, Signer, SingleSignature, SingleSignatureWithRegisteredParty,
VerificationKeyForConcatenation, VerificationKeyProofOfPossessionForConcatenation,
Parameters, RegisterError, RegistrationEntryForConcatenation, SignatureError, Signer,
SingleSignature, SingleSignatureWithRegisteredParty, VerificationKeyForConcatenation,
VerificationKeyProofOfPossessionForConcatenation,
};
pub use signature_scheme::BlsSignatureError;

Expand Down
22 changes: 18 additions & 4 deletions mithril-stm/src/membership_commitment/merkle_tree/commitment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,26 @@ impl<D: Digest + FixedOutput, L: MerkleTreeLeaf> MerkleTreeBatchCommitment<D, L>
)));
}

let nr_nodes = self.nr_leaves + self.nr_leaves.next_power_of_two() - 1;

let nr_nodes = self
.nr_leaves
.checked_next_power_of_two()
.and_then(|nr_nodes| nr_nodes.checked_add(self.nr_leaves))
.and_then(|nr_nodes| nr_nodes.checked_sub(1))
.ok_or(MerkleTreeError::SerializationError)?;

let nr_leaves_next_pow_2 = self
.nr_leaves
.checked_next_power_of_two()
.ok_or(MerkleTreeError::SerializationError)?;
ordered_indices = ordered_indices
.into_iter()
.map(|i| i + self.nr_leaves.next_power_of_two() - 1)
.collect();
.map(|i| {
nr_leaves_next_pow_2
.checked_add(i)
.and_then(|idx| idx.checked_sub(1))
.ok_or(MerkleTreeError::SerializationError)
})
.collect::<Result<Vec<_>, _>>()?;

let mut idx = ordered_indices[0];
// First we need to hash the leave values
Expand Down
41 changes: 24 additions & 17 deletions mithril-stm/src/membership_commitment/merkle_tree/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ use std::marker::PhantomData;
use digest::{Digest, FixedOutput};
use serde::{Deserialize, Serialize};

use crate::StmResult;
use crate::codec;
use crate::{StmResult, codec};

use super::MerkleTreeError;
use super::{
MerkleBatchPath, MerkleTreeBatchCommitment, MerkleTreeError, MerkleTreeLeaf, left_child,
parent, right_child, sibling,
MerkleBatchPath, MerkleTreeBatchCommitment, MerkleTreeLeaf, left_child, parent, right_child,
sibling,
};
#[cfg(feature = "future_snark")]
// TODO: remove this allow dead_code directive when function is called or future_snark is activated
#[allow(dead_code)]
use super::{MerklePath, MerkleTreeCommitment};

/// Tree of hashes, providing a commitment of data and its ordering.
Expand Down Expand Up @@ -147,13 +145,15 @@ impl<D: Digest + FixedOutput, L: MerkleTreeLeaf> MerkleTree<D, L> {
}

/// Convert a `MerkleTree` into a byte string.
#[allow(dead_code)]
pub fn to_bytes(&self) -> StmResult<Vec<u8>> {
Comment on lines 147 to 149
codec::to_cbor_bytes(self)
}

/// Try to convert a byte string into a `MerkleTree`.
/// # Error
/// It returns error if conversion fails.
#[allow(dead_code)]
pub fn from_bytes(bytes: &[u8]) -> StmResult<Self> {
codec::from_versioned_bytes(bytes, Self::from_bytes_legacy)
}
Expand All @@ -162,44 +162,51 @@ impl<D: Digest + FixedOutput, L: MerkleTreeLeaf> MerkleTree<D, L> {
/// # Layout
/// * Number of leaves committed in the Merkle Tree (as u64)
/// * All nodes of the merkle tree (starting with the root)
#[allow(dead_code)]
fn from_bytes_legacy(bytes: &[u8]) -> StmResult<Self> {
let mut u64_bytes = [0u8; 8];
u64_bytes.copy_from_slice(bytes.get(..8).ok_or(MerkleTreeError::SerializationError)?);
let n = usize::try_from(u64::from_be_bytes(u64_bytes))
.map_err(|_| MerkleTreeError::SerializationError)?;
let num_nodes = n + n.next_power_of_two() - 1;
let mut nodes = Vec::with_capacity(num_nodes);
let num_nodes = n
.checked_next_power_of_two()
.and_then(|num_nodes| num_nodes.checked_add(n))
.and_then(|num_nodes| num_nodes.checked_sub(1))
.ok_or(MerkleTreeError::SerializationError)?;
let mut nodes = Vec::new();
for i in 0..num_nodes {
let range_low = i
Comment thread
damrobi marked this conversation as resolved.
.checked_mul(<D as Digest>::output_size())
.and_then(|rl| rl.checked_add(8))
.ok_or(MerkleTreeError::SerializationError)?;
let range_high = i
.checked_add(1)
.and_then(|rh| rh.checked_mul(<D as Digest>::output_size()))
.and_then(|rh| rh.checked_add(8))
.ok_or(MerkleTreeError::SerializationError)?;
nodes.push(
bytes
.get(
8 + i * <D as Digest>::output_size()
..8 + (i + 1) * <D as Digest>::output_size(),
)
.get(range_low..range_high)
.ok_or(MerkleTreeError::SerializationError)?
.to_vec(),
);
}
Ok(Self {
nodes,
leaf_off: num_nodes - n,
leaf_off: num_nodes.checked_sub(n).ok_or(MerkleTreeError::SerializationError)?,
n,
hasher: PhantomData,
leaves: PhantomData,
})
}

#[cfg(feature = "future_snark")]
// TODO: remove this allow dead_code directive when function is called or future_snark is activated
#[allow(dead_code)]
/// Convert merkle tree to a commitment. This function simply returns the root.
pub(crate) fn to_merkle_tree_commitment(&self) -> MerkleTreeCommitment<D, L> {
MerkleTreeCommitment::new(self.nodes[0].clone()) // Use private constructor
}

#[cfg(feature = "future_snark")]
// TODO: remove this allow dead_code directive when function is called or future_snark is activated
#[allow(dead_code)]
/// Get a path (hashes of siblings of the path to the root node)
/// for the `i`th value stored in the tree.
/// Requires `i < self.n`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl<D: MembershipDigest> From<&ClosedKeyRegistration>
mt_commitment: reg
.to_merkle_tree::<D::ConcatenationHash, RegistrationEntryForConcatenation>()
.to_merkle_tree_batch_commitment(),
total_stake: reg.total_stake,
total_stake: reg.get_total_stake(),
}
}
}
Expand Down
38 changes: 29 additions & 9 deletions mithril-stm/src/proof_system/concatenation/eligibility.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::{PhiFValue, Stake};
use anyhow::anyhow;

use crate::{PhiFValue, Stake, StmResult};

cfg_num_integer! {
use num_bigint::{BigInt, Sign};
Expand Down Expand Up @@ -29,23 +31,25 @@ cfg_num_integer! {
/// 1 - p 1 - (ev / evMax) (evMax - ev)
///
/// Used to determine winning lottery tickets.
pub(crate) fn is_lottery_won(phi_f: PhiFValue, ev: [u8; 64], stake: Stake, total_stake: Stake) -> bool {
pub(crate) fn is_lottery_won(phi_f: PhiFValue, ev: [u8; 64], stake: Stake, total_stake: Stake) -> StmResult<bool> {
// If phi_f = 1, then we automatically break with true
if (phi_f - 1.0).abs() < PhiFValue::EPSILON {
return true;
return Ok(true);
} else if !(phi_f > 0.0 && phi_f <= 1.0) {
return Err(anyhow!("phi_f must be in the range (0, 1], got {phi_f}"));
}

let ev_max = BigInt::from(2u8).pow(512);
let ev = BigInt::from_bytes_le(Sign::Plus, &ev);
let q = Ratio::new_raw(ev_max.clone(), ev_max - ev);

let c =
Ratio::from_float((1.0 - phi_f).ln()).expect("Only fails if the float is infinite or NaN.");
Ratio::from_float((1.0 - phi_f).ln()).ok_or_else(|| anyhow!("phi_f must not be infinite or NaN, got {phi_f}"))?;
let w = Ratio::new_raw(BigInt::from(stake), BigInt::from(total_stake));
let x = (w * c).neg();

// Now we compute a taylor function that breaks when the result is known.
taylor_comparison(1000, q, x)
Ok(taylor_comparison(1000, q, x))
}

/// Checks if cmp < exp(x). Uses error approximation for an early stop. Whenever the value being
Expand Down Expand Up @@ -92,10 +96,12 @@ cfg_rug! {
/// order to keep the error in the 1e-17 range, we need to carry out the computations with 34
/// decimal digits (in order to represent the 4.5e16 ada without any rounding errors, we need
/// double that precision).
pub(crate) fn is_lottery_won(phi_f: PhiFValue, ev: [u8; 64], stake: Stake, total_stake: Stake) -> bool {
pub(crate) fn is_lottery_won(phi_f: PhiFValue, ev: [u8; 64], stake: Stake, total_stake: Stake) -> StmResult<bool> {
// If phi_f = 1, then we automatically break with true
if (phi_f - 1.0).abs() < PhiFValue::EPSILON {
return true;
return Ok(true);
} else if !(phi_f > 0.0 && phi_f <= 1.0) {
return Err(anyhow!("phi_f must be in the range (0, 1], got {phi_f}"));
}
let ev = rug::Integer::from_digits(&ev, Order::LsfLe);
let ev_max: Float = Float::with_val(117, 2).pow(512);
Expand All @@ -104,7 +110,7 @@ cfg_rug! {
let w = Float::with_val(117, stake) / Float::with_val(117, total_stake);
let phi = Float::with_val(117, 1.0) - Float::with_val(117, 1.0 - phi_f).pow(w);

q < phi
Ok(q < phi)
}
}

Expand Down Expand Up @@ -142,7 +148,7 @@ mod tests {
ev.copy_from_slice(&[&ev_1[..], &ev_2[..]].concat());

let quick_result = trivial_is_lottery_won(phi_f, ev, stake, total_stake);
let result = is_lottery_won(phi_f, ev, stake, total_stake);
let result = is_lottery_won(phi_f, ev, stake, total_stake).unwrap();
assert_eq!(quick_result, result);
}

Expand All @@ -159,4 +165,18 @@ mod tests {
assert!(!taylor_comparison(1000, cmp_p, Ratio::from_float(x).unwrap()));
}
}

#[test]
fn is_lottery_won_rejects_out_of_range_phi_f() {
let ev = [0u8; 64];
let stake = 100;
let total_stake = 1000;

for invalid_phi_f in [-0.5, 0.0, 1.5, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(
is_lottery_won(invalid_phi_f, ev, stake, total_stake).is_err(),
"phi_f = {invalid_phi_f} should be rejected"
);
}
}
}
Loading
Loading