Skip to content

fix: resolve bug in modulo operations during hash_public_keys, add unit test with real data from mainnet - #3

Merged
PastaPastaPasta merged 3 commits into
mainfrom
fix-modulo-ops
Aug 12, 2025
Merged

fix: resolve bug in modulo operations during hash_public_keys, add unit test with real data from mainnet#3
PastaPastaPasta merged 3 commits into
mainfrom
fix-modulo-ops

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 12, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • None
  • Bug Fixes
    • Corrected byte-order handling in secure aggregation, improving reliability and cross-platform correctness for aggregate signature generation and verification, including legacy mode.
  • Documentation
    • Updated examples and cleaned up comments; removed outdated snippets and clarified serialization format notes. No API changes.
  • Tests
    • Added a large-scale secure aggregation verification test with 57 signers to validate correctness under heavier loads.

@coderabbitai

coderabbitai Bot commented Aug 12, 2025

Copy link
Copy Markdown

Walkthrough

The PR updates documentation imports and examples, fixes endianness when constructing scalars in secure aggregation, and adds a large-scale verification test. It also removes an example from serialization docs and applies minor formatting changes. No public API behavior changes except the corrected byte order in scalar hashing.

Changes

Cohort / File(s) Summary of changes
Secure aggregation core
src/secure_aggregation.rs
Corrected endianness: reverse 32-byte hash before Scalar::from_repr in both modern and legacy/mode paths; removed outdated endianness comment; reformatted generic bounds and internal closures without behavioral changes.
Docs and examples
src/aggregate_signature.rs, src/signature.rs, src/serialization.rs
Updated example import paths to blsful::Bls12381G1Impl; removed a large example block and minor whitespace; no functional or API changes; SerializationFormat docs trimmed (Example section removed).
Tests
tests/secure_aggregation_test.rs
Added test_large_scale_aggregate_signature_verification validating secure aggregation verification with 57 public keys and a precomputed aggregate signature (Modern format).

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Signature
  participant SecureAggregation
  participant Pairing

  Caller->>Signature: verify_secure(message, pubkeys)
  Signature->>SecureAggregation: hash_public_keys_with_sorted(...)
  SecureAggregation->>SecureAggregation: hash -> 32 bytes -> reverse -> Scalar(s)
  SecureAggregation-->>Signature: coefficients, sorted pubkeys
  Signature->>Pairing: aggregate verify with coefficients
  Pairing-->>Signature: result
  Signature-->>Caller: verification outcome
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15–20 minutes

Possibly related PRs

Suggested reviewers

  • QuantumExplorer

Poem

I flipped the bytes, a tidy hop,
From tail to head—no more swap!
Fifty-seven keys in chorus sing,
Aggregates align—what joy they bring.
Docs trimmed neat, tests now glow,
A bunny bows—onward we go! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-modulo-ops

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
src/secure_aggregation.rs (1)

61-96: Ensure deterministic modular reduction using from_bytes_wide instead of from_repr

from_repr requires a canonical (reduced) input and will return None if the 32-byte hash is ≥ the field modulus (≈50% of the time). To match C++’s bn_mod_basic (which always reduces), switch to an explicit reduction via from_bytes_wide.

Locations to update:

  • src/secure_aggregation.rs (lines 61–96)
  • src/secret_key_share.rs (around lines 88–90)
  • src/helpers.rs (around lines 96–113)

Suggested diff in secure_aggregation.rs:

- // Convert hash → big-endian repr → reverse → from_repr (no reduction)
- let mut repr = <…>::Repr::default();
- // copy `hash` into `repr_bytes` and reverse
- let scalar = <<C as Pairing>::PublicKey as Group>::Scalar
-     ::from_repr(repr)
-     .into_option()?
+ // Deterministically reduce hash mod p using from_bytes_wide
+ let mut wide = [0u8; 64];
+ // Place big-endian hash into the high bytes of a little-endian 64-byte array
+ for (i, &b) in hash.iter().enumerate() {
+     wide[31 - i] = b;
+ }
+ let scalar = <<C as Pairing>::PublicKey as Group>::Scalar
+     ::from_bytes_wide(&wide);

Apply the same pattern in the other files to ensure every from_repr path becomes an explicit reduction. This guarantees parity with C++’s bn_mod_basic.

🧹 Nitpick comments (1)
tests/secure_aggregation_test.rs (1)

143-235: Great real-world coverage; a couple of nits and a small allocation improvement

  • Pre-allocate public_keys with capacity to avoid reallocations.
  • The comment about choosing Bls12381G2Impl could be confusing (it mentions G1 pubkeys and G2 signatures). Consider clarifying that you are using the variant where pubkeys are 48 bytes and signatures are 96 bytes, and that this corresponds to the selected Impl in this crate.

Apply this diff inside the test:

-    // Test data from production system with 57 signers
-    // Using Bls12381G2Impl because public keys are 48 bytes (G1) and signature is 96 bytes (G2)
+    // Test data from production system with 57 signers.
+    // Using the variant where public keys are 48 bytes (G1) and the signature is 96 bytes (G2).
+    // This corresponds to Bls12381G2Impl in this crate.
@@
-    // Parse public keys
-    let mut public_keys = Vec::new();
+    // Parse public keys
+    let mut public_keys = Vec::with_capacity(keys_hex.len());
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f017aa and c0d12bc.

📒 Files selected for processing (5)
  • src/aggregate_signature.rs (1 hunks)
  • src/secure_aggregation.rs (5 hunks)
  • src/serialization.rs (1 hunks)
  • src/signature.rs (2 hunks)
  • tests/secure_aggregation_test.rs (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tests/secure_aggregation_test.rs (4)
src/secure_aggregation.rs (1)
  • PublicKey (517-517)
examples/verify_secure_example.rs (1)
  • PublicKey (35-35)
src/aggregate_signature.rs (2)
  • try_from (126-147)
  • try_from (163-165)
src/signature.rs (5)
  • try_from (123-125)
  • try_from (287-319)
  • from_bytes_with_mode (231-253)
  • to_bytes_with_mode (209-214)
  • verify_secure (177-197)
🔇 Additional comments (10)
src/serialization.rs (1)

24-24: Minor whitespace cleanup looks good

No functional changes; enum semantics and helpers remain intact.

src/secure_aggregation.rs (3)

118-123: Signature bound reformatting is fine

Formatting-only change for H’s bounds. No functional impact.


181-186: Signature bound reformatting is fine

Same as above; no behavior changes.


365-367: Closure style change is fine

Purely stylistic; behavior unchanged.

src/aggregate_signature.rs (1)

15-16: Doc import path update is correct

The example now uses blsful::Bls12381G1Impl, consistent with the other doc updates. No functional changes.

src/signature.rs (4)

13-14: Doc example import path updated correctly

Matches the new canonical path used across examples.


16-16: Doc cleanup

Removing the stray doc spacer is fine.


19-19: Doc cleanup

Another harmless doc whitespace tweak.


88-89: Minor formatting tweak after debug_assert

No behavior change; fine to keep.

tests/secure_aggregation_test.rs (1)

10-10: Importing Modern directly is fine

Brings the enum variant into scope cleanly; improves readability in the test.

Comment thread src/secure_aggregation.rs
Comment on lines +81 to +82
// Reverse bytes since from_repr expects little-endian but hash is big-endian
repr_bytes.reverse();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Endianness fix is correct; consider extracting into a helper to avoid duplication

Reversing the bytes to feed little-endian into from_repr addresses the BE/LE mismatch. The same conversion logic appears below in the legacy-aware path; extract to a single helper to reduce duplication and future drift.

Apply this diff to use a helper here:

-        // Create a field element representation
-        let mut repr =
-            <<<C as Pairing>::PublicKey as Group>::Scalar as PrimeField>::Repr::default();
-        let repr_bytes = repr.as_mut();
-
-        // Copy the hash into the representation
-        // For BLS12-381, the scalar field is ~255 bits, stored in 32 bytes
-        // The hash is 32 bytes, so it fits directly
-        if repr_bytes.len() >= 32 {
-            // Copy hash to the least significant bytes (big-endian interpretation)
-            let offset = repr_bytes.len() - 32;
-            repr_bytes[offset..].copy_from_slice(&hash);
-            // Zero out any higher bytes
-            for byte in &mut repr_bytes[..offset] {
-                *byte = 0;
-            }
-            // Reverse bytes since from_repr expects little-endian but hash is big-endian
-            repr_bytes.reverse();
-        } else {
-            // This shouldn't happen for BLS12-381, but handle it gracefully
-            return Err(BlsError::InvalidInputs(
-                "Field representation too small".to_string(),
-            ));
-        }
-
-        // Create scalar from representation - this automatically reduces modulo field order
-        let scalar = <<C as Pairing>::PublicKey as Group>::Scalar::from_repr(repr)
-            .into_option()
-            .ok_or_else(|| {
-                BlsError::InvalidInputs("Failed to create scalar from hash".to_string())
-            })?;
+        // Convert hash -> scalar (handles endianness and reduction semantics)
+        let scalar = hash32_to_scalar::<C>(hash)?;

Add this helper near the top of the module (outside of the changed range):

#[inline]
fn hash32_to_scalar<C: BlsSignatureImpl>(
    hash: [u8; 32],
) -> BlsResult<<<C as Pairing>::PublicKey as Group>::Scalar> {
    // Build little-endian repr from big-endian hash
    let mut repr =
        <<<C as Pairing>::PublicKey as Group>::Scalar as PrimeField>::Repr::default();
    let repr_bytes = repr.as_mut();

    if repr_bytes.len() < 32 {
        return Err(BlsError::InvalidInputs(
            "Field representation too small".to_string(),
        ));
    }

    let offset = repr_bytes.len() - 32;
    repr_bytes[offset..].copy_from_slice(&hash);
    for byte in &mut repr_bytes[..offset] {
        *byte = 0;
    }
    repr_bytes.reverse();

    // Note: from_repr must accept canonical encodings. If it rejects >r values, we must reduce.
    let ct = <<C as Pairing>::PublicKey as Group>::Scalar::from_repr(repr);
    ct.into_option()
        .ok_or_else(|| BlsError::InvalidInputs("Failed to create scalar from hash".to_string()))
}

Note: If the underlying field’s from_repr rejects values ≥ modulus (common in ff-based fields), this helper may still need an explicit reduction path to fully match “bn_mod_basic”. If that’s the case, we can switch to a reduction-based approach (e.g., using from_bytes_wide with proper LE placement) in a follow-up.

🤖 Prompt for AI Agents
In src/secure_aggregation.rs around lines 81-82, the byte-reversal logic for
converting a big-endian 32-byte hash into the scalar representation is
duplicated; extract it into a single helper function placed near the top of the
module (outside the changed range) named hash32_to_scalar that is generic over
C: BlsSignatureImpl and returns BlsResult<<<C as Pairing>::PublicKey as
Group>::Scalar>. Implement the helper to allocate the scalar Repr, ensure
repr.as_mut() is large enough, copy the 32 hash bytes into the last 32 bytes,
zero the prefix bytes, reverse the whole repr to little-endian, call from_repr
and convert the Option into an Err with a descriptive BlsError on failure; then
replace the inline reversal-and-from_repr code at this location (and the
legacy-aware path) with a call to hash32_to_scalar::<C>(hash).

Comment thread src/secure_aggregation.rs
Comment on lines +311 to +312
// Reverse bytes since from_repr expects little-endian but hash is big-endian
repr_bytes.reverse();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Duplicate endianness fix; reuse a common helper

This mirrors the BE→LE correction above. Recommend calling a shared helper to prevent subtle drift between code paths.

Apply this diff to use the same helper here:

-        if repr_bytes.len() >= 32 {
-            let offset = repr_bytes.len() - 32;
-            repr_bytes[offset..].copy_from_slice(&hash);
-            for byte in &mut repr_bytes[..offset] {
-                *byte = 0;
-            }
-            // Reverse bytes since from_repr expects little-endian but hash is big-endian
-            repr_bytes.reverse();
-        } else {
-            return Err(BlsError::InvalidInputs(
-                "Field representation too small".to_string(),
-            ));
-        }
-
-        let scalar = <<C as Pairing>::PublicKey as Group>::Scalar::from_repr(repr)
-            .into_option()
-            .ok_or_else(|| {
-                BlsError::InvalidInputs("Failed to create scalar from hash".to_string())
-            })?;
+        let scalar = hash32_to_scalar::<C>(hash)?;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/secure_aggregation.rs around lines 311-312, replace the manual
repr_bytes.reverse() endianness flip with the shared BE→LE helper used elsewhere
to avoid duplication and drift; call the same helper function (the one
previously used in this file — e.g., convert_be_to_le_bytes or be_to_le_bytes)
instead of reversing in-place, adjust the call/assignment to match the helper's
signature (mutate or return a new Vec<[u8]> as appropriate), and remove the
manual reverse so both code paths use the identical conversion routine.

@PastaPastaPasta
PastaPastaPasta merged commit be108b2 into main Aug 12, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants