Skip to content

Conversation

@samkim-crypto
Copy link
Contributor

@samkim-crypto samkim-crypto commented Feb 2, 2026

Summary of Changes

Cleaning up the solana-bls-cert-verify crate.

766cde8: Benches for the certificate verification logic exists in solana-core, but I thought it made sense to add benches here too to identify any specific bottle necks related to certificate verification.

735274f: Updated the public key aggregation logic to take advantage of the mixed point additions.

The solana-bls-signatures v3.0 crate added support for mixed point additions (anza-xyz/solana-sdk#506). Previously, we had to convert all points to projective before summing these together to aggregate public keys, but we can now add affine or even unserialized points directly to project points using a simpler group add formula.

Using mixed point additions, the bench results seem to improve quite drastically:

BLS Cert Verify/Base2_Notarize/1000
                        time:   [2.8434 ms 2.8479 ms 2.8525 ms]
                        change: [-92.215% -92.192% -92.171%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild
BLS Cert Verify/Base3_NotarizeFallback/1000
                        time:   [3.5388 ms 3.5573 ms 3.5776 ms]
                        change: [-90.838% -90.788% -90.732%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 10 outliers among 100 measurements (10.00%)
  8 (8.00%) high mild
  2 (2.00%) high severe

I am glad that this gives quite a significant performance boost, but maybe we should have used mixed-point addition from the start.. 🙏

01c9687: For the base3 certificate verification, we have to aggregate two sets of public keys (pertaining to primary votes and fallback votes). Currently, these two sets are aggregated sequentially. If we aggregate these two sets in parallel, then we get decent performance improvements, so I applied this change.

BLS Cert Verify/Base3_NotarizeFallback/1000
                        time:   [3.1656 ms 3.1710 ms 3.1765 ms]
                        change: [-11.390% -10.858% -10.366%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 2 outliers among 100 measurements (2.00%)

The rest of the commits db2b01e, 315a77c, 8b3615f, f99187d are general refactoring and code quality improvements. In particular, I renamed the function verify_cert_get_total_stake to just verify_certificate because that seemed cleaner and follows a more standard rust naming convention, but I can revert this if others are opposed.

Finally, I added more precise bench logic in 6721329 to include:

  • A single BLS signature verification. This serves as a good reference since we can't possibly hope to beat a single sig verification
  • Collect public key
  • Aggregate public key

Here are the results:

| Validators | Collect Keys | Aggregate Keys | Verify Sig (Constant) | Total (Actual) | Overhead |
| **500** | 0.003 ms     | 0.85 ms        | 1.26 ms               | **2.26 ms** | ~0.15 ms |
| **1000** | 0.007 ms     | 1.46 ms        | 1.26 ms               | **2.88 ms** | ~0.15 ms |
| **1500** | 0.010 ms     | 1.92 ms        | 1.26 ms               | **3.39 ms** | ~0.20 ms |
| **2000** | 0.013 ms     | 2.51 ms        | 1.26 ms               | **3.97 ms** | ~0.19 ms |

Distribution:

| Validators | Aggregate % | Verify % | Collect % |
| **500**   | 38%         | 56%     | < 0.2%    |
| **1000** | 51%         | 44%      | < 0.3%    |
| **1500** | 57%         | 37%      | < 0.3%    |
| **2000** | 63%         | 32%      | < 0.3%    |

Roughly for 1000~1500 validators, the public key aggregation logic takes a little over 50% of the cost of verifying a certificate.

I created in issue #708 to cut down public key aggregation even further. If we take approach 2, then I think public key aggregation can almost be an order of magnitude faster. I'll tackle this issue on a follow-up.

@samkim-crypto samkim-crypto force-pushed the refactor-bls-cert-verify branch from 813ae83 to 6721329 Compare February 3, 2026 08:06
@samkim-crypto samkim-crypto marked this pull request as ready for review February 3, 2026 09:24
akhi3030
akhi3030 previously approved these changes Feb 3, 2026
Copy link
Contributor

@akhi3030 akhi3030 left a comment

Choose a reason for hiding this comment

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

Thanks, looks great!

fn bench_verify_cert(c: &mut Criterion) {
let mut group = c.benchmark_group("BLS Cert Verify");
let validator_sizes = [500, 1000, 1500, 2000];
const TEST_STAKE: u64 = 30; // assume each validator has stake 30 (arbitrary number)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const TEST_STAKE: u64 = 30; // assume each validator has stake 30 (arbitrary number)
// assume each validator has stake 30 (arbitrary number)
const TEST_STAKE: u64 = 30;

nit: this seems like the more rusty way to do comments.

}

/// Verifies the [`signature`] of the [`payload`] which is signed by at most [`max_validators`] validators in the base2 encoded [`ranks`] using the [`rank_map`] to lookup the [`BlsPubkey`].
fn get_vote_payloads(cert_type: &CertificateType) -> (Vote, Option<Vote>) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
fn get_vote_payloads(cert_type: &CertificateType) -> (Vote, Option<Vote>) {
fn get_vote_payloads(cert_type: CertificateType) -> (Vote, Option<Vote>) {

nit: if a function takes something as a reference and then clones or copy it, it is better to expose that to the caller.

/// # Returns
///
/// On success, returns the total stake.
pub fn verify_certificate(
Copy link
Contributor

Choose a reason for hiding this comment

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

I had originally picked the name verify_cert_get_total_stake and I am happy with this renaming.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the original name was fine as well! Just thought to keep it simple.


/// Verifies the [`signature`] of the [`payload`] which is signed by at most [`max_validators`] validators in the base2 encoded [`ranks`] using the [`rank_map`] to lookup the [`BlsPubkey`].
fn get_vote_payloads(cert_type: &CertificateType) -> (Vote, Option<Vote>) {
match *cert_type {
Copy link
Contributor

Choose a reason for hiding this comment

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

Factoring out this logic from the function does make it easier to read that function. Just a note that now we are having to create an additional Option and we have two branches instead of one. I doubt this is causing any perf hits but something to keep in mind if we feel like we need to squeeze more perf.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call. Fixed!

// `expect` is safe because the `Vote` struct is composed entirely of primitive
// types (u64, Hash, enums) that are inherently serializable and it is constructed
// locally within this module.
bincode::serialize(vote).expect("Vote serialization should never fail for valid Vote structs")
Copy link
Contributor

Choose a reason for hiding this comment

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

I plan on introducing wincode serialization in bls sigverifier. I think I will refactor this at the same time when I do that.

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