Fast Verification of Decryption in TLE#3315
Fast Verification of Decryption in TLE#3315guruvamsi-policharla wants to merge 2 commits intomainfrom
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
commonware-mcp | 0fa8d5e | Mar 04 2026, 11:34 PM |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Debug timing and println left in production code
- Removed all timing instrumentation and the stdout
println!fromverify_decryptionso it performs only verification logic.
- Removed all timing instrumentation and the stdout
- ✅ Fixed: Unused public method
GT::scalar_mulhas no callers- Deleted the unused
GT::scalar_mulmethod and its now-unused BLST import to eliminate dead public code.
- Deleted the unused
Or push these changes by commenting:
@cursor push 13291663df
Preview (13291663df)
diff --git a/cryptography/src/bls12381/batch_tle.rs b/cryptography/src/bls12381/batch_tle.rs
--- a/cryptography/src/bls12381/batch_tle.rs
+++ b/cryptography/src/bls12381/batch_tle.rs
@@ -144,25 +144,17 @@
let n = ciphertexts.len();
- // Sample random challenge scalars
- let t0 = std::time::Instant::now();
let challenges: Vec<Scalar> = (0..n).map(|_| Scalar::random(&mut *rng)).collect();
- let t_challenges = t0.elapsed();
// V::Signature MSM: Σ c_i * r_i
- let t0 = std::time::Instant::now();
let c_points: Vec<V::Signature> = ciphertexts.iter().map(|ct| ct.c).collect();
let c_agg = V::Signature::msm(&c_points, &challenges, &commonware_parallel::Sequential);
- let t_sig_msm = t0.elapsed();
// V::Public MSM: Σ u_i * r_i
- let t0 = std::time::Instant::now();
let u_points: Vec<V::Public> = ciphertexts.iter().map(|ct| ct.u).collect();
let u_agg = V::Public::msm(&u_points, &challenges, &commonware_parallel::Sequential);
- let t_pub_msm = t0.elapsed();
// Scalar sum: s = Σ r_i * m_i
- let t0 = std::time::Instant::now();
let mut msg_scalar = Scalar::zero();
for (r, &m) in challenges.iter().zip(messages.iter()) {
let m_scalar = Scalar::from_u64(m);
@@ -173,20 +165,11 @@
let mut msg_term = *h_id_gamma;
msg_term *= &(-msg_scalar);
let sig_term = c_agg + &msg_term;
- let t_scalar = t0.elapsed();
// Check: e(pk^gamma, sig_term) * e(-u_agg, sig_id) == 1
- let t0 = std::time::Instant::now();
let lhs = V::pairing(pk_gamma, &sig_term);
let rhs = V::pairing(&u_agg.neg(), signature);
- let result = lhs.mul(&rhs).is_one();
- let t_pairing = t0.elapsed();
-
- println!(
- " verify_decryption(n={n}): challenges={t_challenges:.2?}, sig_msm={t_sig_msm:.2?}, pub_msm={t_pub_msm:.2?}, scalar={t_scalar:.2?}, pairing={t_pairing:.2?}",
- );
-
- result
+ lhs.mul(&rhs).is_one()
}
#[cfg(test)]
diff --git a/cryptography/src/bls12381/primitives/group.rs b/cryptography/src/bls12381/primitives/group.rs
--- a/cryptography/src/bls12381/primitives/group.rs
+++ b/cryptography/src/bls12381/primitives/group.rs
@@ -16,15 +16,15 @@
use alloc::{vec, vec::Vec};
use blst::{
blst_bendian_from_fp12, blst_bendian_from_scalar, blst_expand_message_xmd, blst_fp12,
- blst_fp12_cyclotomic_sqr, blst_fp12_is_one, blst_fp12_mul, blst_fp12_one, blst_fr,
- blst_fr_add, blst_fr_cneg, blst_fr_from_scalar, blst_fr_from_uint64, blst_fr_inverse,
- blst_fr_mul, blst_fr_rshift, blst_fr_sub, blst_hash_to_g1, blst_hash_to_g2, blst_keygen,
- blst_p1, blst_p1_add_or_double, blst_p1_affine, blst_p1_cneg, blst_p1_compress, blst_p1_double,
- blst_p1_from_affine, blst_p1_in_g1, blst_p1_is_inf, blst_p1_mult, blst_p1_to_affine,
- blst_p1_uncompress, blst_p1s_mult_pippenger, blst_p1s_mult_pippenger_scratch_sizeof,
- blst_p1s_tile_pippenger, blst_p1s_to_affine, blst_p2, blst_p2_add_or_double, blst_p2_affine,
- blst_p2_cneg, blst_p2_compress, blst_p2_double, blst_p2_from_affine, blst_p2_in_g2,
- blst_p2_is_inf, blst_p2_mult, blst_p2_to_affine, blst_p2_uncompress, blst_p2s_mult_pippenger,
+ blst_fp12_is_one, blst_fp12_mul, blst_fp12_one, blst_fr, blst_fr_add, blst_fr_cneg,
+ blst_fr_from_scalar, blst_fr_from_uint64, blst_fr_inverse, blst_fr_mul, blst_fr_rshift,
+ blst_fr_sub, blst_hash_to_g1, blst_hash_to_g2, blst_keygen, blst_p1, blst_p1_add_or_double,
+ blst_p1_affine, blst_p1_cneg, blst_p1_compress, blst_p1_double, blst_p1_from_affine,
+ blst_p1_in_g1, blst_p1_is_inf, blst_p1_mult, blst_p1_to_affine, blst_p1_uncompress,
+ blst_p1s_mult_pippenger, blst_p1s_mult_pippenger_scratch_sizeof, blst_p1s_tile_pippenger,
+ blst_p1s_to_affine, blst_p2, blst_p2_add_or_double, blst_p2_affine, blst_p2_cneg,
+ blst_p2_compress, blst_p2_double, blst_p2_from_affine, blst_p2_in_g2, blst_p2_is_inf,
+ blst_p2_mult, blst_p2_to_affine, blst_p2_uncompress, blst_p2s_mult_pippenger,
blst_p2s_mult_pippenger_scratch_sizeof, blst_p2s_tile_pippenger, blst_p2s_to_affine,
blst_scalar, blst_scalar_from_be_bytes, blst_scalar_from_bendian, blst_scalar_from_fr,
blst_sk_check, Pairing, BLS12_381_G1, BLS12_381_G2, BLST_ERROR,
@@ -456,51 +456,6 @@
unsafe { blst_fp12_is_one(&self.0) }
}
- /// Computes self^scalar using square-and-multiply.
- ///
- /// Uses cyclotomic squaring, which is faster than general fp12
- /// squaring for elements in the cyclotomic subgroup (i.e. pairing outputs).
- pub fn scalar_mul(&self, scalar: &Scalar) -> GT {
- let s = scalar.as_blst_scalar();
- // blst_scalar.b is little-endian
- let bytes = &s.b;
-
- // Find the highest set bit
- let mut top_byte = SCALAR_LENGTH - 1;
- while top_byte > 0 && bytes[top_byte] == 0 {
- top_byte -= 1;
- }
- if bytes[top_byte] == 0 {
- return GT::one();
- }
- let top_bit = 7 - bytes[top_byte].leading_zeros() as usize;
-
- // Square-and-multiply (MSB to LSB)
- let mut acc = self.0;
- let base = &self.0;
- let mut started = false;
- for byte_idx in (0..=top_byte).rev() {
- let b = bytes[byte_idx];
- let bit_start = if byte_idx == top_byte { top_bit } else { 7 };
- for bit in (0..=bit_start).rev() {
- if started {
- // SAFETY: blst_fp12_cyclotomic_sqr supports in-place operation.
- unsafe { blst_fp12_cyclotomic_sqr(&mut acc, &acc) };
- }
- if (b >> bit) & 1 == 1 {
- if started {
- // SAFETY: blst_fp12_mul supports in-place when ret aliases a.
- unsafe { blst_fp12_mul(&mut acc, &acc, base) };
- } else {
- started = true;
- }
- }
- }
- }
-
- if started { GT(acc) } else { GT::one() }
- }
-
/// Converts the GT element to its canonical big-endian byte representation.
pub fn as_slice(&self) -> [u8; GT_ELEMENT_BYTE_LENGTH] {
let mut slice = [0u8; GT_ELEMENT_BYTE_LENGTH];|
|
||
| println!( | ||
| " verify_decryption(n={n}): challenges={t_challenges:.2?}, sig_msm={t_sig_msm:.2?}, pub_msm={t_pub_msm:.2?}, scalar={t_scalar:.2?}, pairing={t_pairing:.2?}", | ||
| ); |
There was a problem hiding this comment.
Debug timing and println left in production code
High Severity
The public verify_decryption function contains std::time::Instant::now() timing instrumentation (five separate timing blocks) and a println! statement that prints detailed timing breakdowns to stdout on every invocation. This is benchmarking/debug code that has no place in a library function — every caller will see unwanted output and pay the cost of timing overhead.
| } | ||
|
|
||
| if started { GT(acc) } else { GT::one() } | ||
| } |
There was a problem hiding this comment.
Unused public method GT::scalar_mul has no callers
Low Severity
GT::scalar_mul is a new pub method with a nontrivial implementation (square-and-multiply with cyclotomic squaring), but a codebase-wide grep reveals it has zero callers — the only match is the definition itself. This is dead code that adds maintenance burden without being exercised by any code path or test.
| // V::Signature MSM: Σ c_i * r_i | ||
| let t0 = std::time::Instant::now(); | ||
| let c_points: Vec<V::Signature> = ciphertexts.iter().map(|ct| ct.c).collect(); | ||
| let c_agg = V::Signature::msm(&c_points, &challenges, &commonware_parallel::Sequential); |
There was a problem hiding this comment.
I suspect we can make this a good deal faster by accepting parallel here and concurrently running the MSMs
|
|
||
| // Sample random challenge scalars | ||
| let t0 = std::time::Instant::now(); | ||
| let challenges: Vec<Scalar> = (0..n).map(|_| Scalar::random(&mut *rng)).collect(); |
There was a problem hiding this comment.
As discussed, I think this could also be SmallScalar
|
|
||
| // Check: e(pk^gamma, sig_term) * e(-u_agg, sig_id) == 1 | ||
| let t0 = std::time::Instant::now(); | ||
| let lhs = V::pairing(pk_gamma, &sig_term); |
There was a problem hiding this comment.
Can do pair() on these pairings
|
|
||
| /// Encrypted message from a small message space {0, ..., 2^k - 1}. | ||
| /// | ||
| /// The VRF committee samples a random gamma and publishes `pk^gamma` and |
There was a problem hiding this comment.
the very unique property here is that the VRF committee can decide when to publish the target and the solution (creating bounded auctions and auctions with reveal keys only published after some height)
| /// Discrete log lookup table mapping hashed GT elements to messages. | ||
| /// | ||
| /// Stores `H(base^m) -> m` for `m in 0..2^k` where | ||
| /// `base = e(pk^gamma, H(id)^{gamma^{-1}})`. |
There was a problem hiding this comment.
Would be great to elaborate practically on how this shared gamma is derived.
|
We can afford CPA here because of an assumed ambient context where some other form of integrity is applied, e.g. signing a transaction as a whole, including this ciphertext. What would that look like in general? Could we integrate that directly here? e.g. using batch verified signatures or something |
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #3315 +/- ##
==========================================
+ Coverage 93.00% 93.01% +0.01%
==========================================
Files 418 419 +1
Lines 142546 143157 +611
Branches 3400 3424 +24
==========================================
+ Hits 132568 133163 +595
- Misses 8883 8904 +21
+ Partials 1095 1090 -5
... and 15 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
| for bit in (0..=bit_start).rev() { | ||
| if started { | ||
| // SAFETY: blst_fp12_cyclotomic_sqr supports in-place operation. | ||
| unsafe { blst_fp12_cyclotomic_sqr(&mut acc, &acc) }; |
There was a problem hiding this comment.
I think the using both the mutable and the immutable reference here and in L493 violates rust's aliasing rules since there can only be no other live references when there is a mutable one: https://doc.rust-lang.org/reference/behavior-considered-undefined.html#r-undefined.alias
The other areas in the codebase correctly handle this by using a raw pointer instead which is fine because raw pointers carry no guarantees and LLVM won't emit noalias for them



Scheme Description
Implements the CPA secure version where the committee additionally holds shares of a random value$\gamma$ and $\gamma^{-1}$ and publish:
We can avoid this at the cost of moving base group MSMs to the target group.
Summary
Adds a batch timelock encryption (TLE) module for BLS12-381 that encrypts messages from a small message space${0, \ldots, 2^k - 1}$ and supports efficient batch verification of decryptions.
HashMap<[u8; 32], u64>) that maps hashedmul,one,is_one, andscalar_mul(square-and-multiply with cyclotomic squaring) to theGTtype ingroup.rs.build_tableprecomputesThe scheme assumes a VRF committee publishes$\text{pk}^\gamma$ and $H(\text{id})^{\gamma^{-1}}$ for each target identity. All public functions accept these pre-processed values as inputs.
Encryption
Given a message$m \in {0, \ldots, 2^k - 1}$ and the committee-provided $H(\text{id})^{\gamma^{-1}}$ :
Decryption
Given the BLS signature$\sigma_{\text{id}}$ over the target identity:
For small message spaces, the discrete log is recovered via a precomputed lookup table mapping$\text{SHA256}(\text{base}^m) \to m$ .
Batch Verification
To verify$B$ claimed decryptions ${m_i}$ for ciphertexts encrypted to the same target, sample random challenges $r_i$ and check:
This requires one$\mathbb{G}_2$ -MSM of size $B$ , one $\mathbb{G}_1$ -MSM of size $B$ , and two pairings (constant). Following [FGHP09], the challenge scalars can be sampled from a smaller range $[0, 2^\ell)$ to further reduce MSM cost, with soundness error at most $2^{-\ell}$ .
Benchmark results (single-threaded)
Ran on a 2019 Intel MacBook Pro