Skip to content

Conversation

@samkim-crypto
Copy link
Contributor

@samkim-crypto samkim-crypto commented Jul 22, 2025

Problem

The existing BLS vote certificate logic had an inefficiency related to costly type conversions during signature aggregation. The VoteCertificate type, a wrapper around CertificateMessage, stored its signature in the compact "affine" form. However, because signature aggregation is performed in "projective" form, each call to aggregate required the existing signature to be converted from affine to projective, incurring a non-trivial performance cost on a critical path.

Summary of Changes

To address this, I added a VoteCertificateBuilder type which holds the certificate signature in its projective form. This results in much cleaner and more performance aggregation function. Once all votes are aggregated, VoteCertificateBuilder::build can be called to produce the final CertificateMessage, converting the signature back to the compact affine form only once at the end.

With the new builder, I removed the redundant VoteCertificate, as its role is now cleanly handled by VoteCertificateBuilder for aggregation and CertificateMessage for the final product.

Additionally, I added both checked (aggregate) and unchecked (aggregate_unchecked) versions of the aggregation function. Since signatures are pre-verified during the sigverify phase, the consensus logic can use the more performant unchecked version.

Fixes #

*signature = Signature::from(current_signature);
Ok(())
let signature_iter = messages.iter().map(|vote_message| &vote_message.signature);
Ok(self.signature.aggregate_with(signature_iter)?)
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry I forgot to ask, do you think this function should really go into CertificateMessage?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry are you stating that the aggregate function should go into CertificateMessage instead of VoteCertificate (now VoteCertificateBuilder) or are you stating that maybe we should remove aggregate from votor entirely?

Copy link
Contributor

Choose a reason for hiding this comment

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

The former, now all we need from VoteCertificate is the aggregate function, I wonder whether we should just move that into BLSMessage::CertificateMessage in the vote program and remove VoteCertificate entirely. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh yeah this is actually what I just addressed in this PR.

I removed VoteCertificate and replaced it with CertificateMessage in most places. For example see here.

But I did add VoteCertificateBuilder (maybe I can rename this to CertificateMessageBuilder instead) that is primarily used for signature aggregation. The issue is that CertificateMessage stores the signature in a more compact Signature form while signature aggregation can only be done in SignatureProjective form. So the previous aggregation function took the Signature type, converted it to SignatureProjective, then convert it back to Signature type after aggregation.

The VoteCertificateBuilder now just holds the SignatureProjective type from the start. Once the signatures are all aggregated, then it can use the build function to create CertificateMessage. It should be more efficient this way since it removes unnecessary costs related to type conversions.

I was planning on writing a detailed PR description, but I just wanted to check if the CI check passed first.

@samkim-crypto samkim-crypto marked this pull request as ready for review July 22, 2025 13:41
#[cfg(test)]
pub fn slot_notarized_fallback(&self, slot: Slot) -> Option<usize> {
// TODO: remove this line once `CertificateMessage` is moved into the repo`
use crate::certificate_pool::vote_certificate::get_vote_count;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need get_vote_count any more, it was only used in tests and those tests have since been removed, I'm removing it in #300

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay great! I removed this during the rebase.

.find_map(|(cert_id, cert)| {
matches!(cert_id, CertificateId::NotarizeFallback(s,_,_) if *s == slot)
.then_some(cert.vote_count())
.then_some(get_vote_count(cert))
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need to return vote count here as I mentioned above, that test only checks if the certificate exists.

/// A builder for creating a `CertificateMessage` by efficiently aggregating BLS signatures.
#[derive(Clone)]
pub struct VoteCertificate(CertificateMessage);
pub struct VoteCertificateBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice, let's change the file name as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

// aggregate the votes
let bitmap = &mut self.0.bitmap;
/// Aggregates a slice of `VoteMessage`s into the builder.
#[allow(dead_code)]
Copy link
Contributor

Choose a reason for hiding this comment

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

If we will only use aggregate_unchecked in the future, we can remove this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Related to your comment below, I just removed the unchecked version and kept the safe one. A potential panic will now happen in add_to_certificate.

output.aggregate(self.vote_entry.transactions.iter())?;
Ok(())
pub fn add_to_certificate(&self, output: &mut VoteCertificateBuilder) {
output.aggregate_unchecked(&self.vote_entry.transactions)
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of calling a function which panics inside, can we use output.aggregate which returns an error and panic here? A bit easier for constructing tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, done!

pub fn add_to_certificate(&self, output: &mut VoteCertificateBuilder) {
output
.aggregate(&self.vote_entry.transactions)
.expect("Incoming vote message signatures are assumed to be valid")
Copy link
Contributor

Choose a reason for hiding this comment

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

just double checking, we've made all the appropriate checks in the vote pool such that the two errors in aggregate can't happen right?

return Err(CertificateError::ValidatorDoesNotExist(

and

Ok(self.signature.aggregate_with(signature_iter)?)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, they should not happen unless our check was screwed up, in that case we can panic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah both of these checks should be guaranteed by the sigverify stage.

  • If the sigverify stage cannot find a valid public key to verify the signature, then it will discard the vote or cert packet
  • The only way the aggregate_with function will fail is if the signature is invalid. The sigverify stage will obviously guarantee this as well

@samkim-crypto samkim-crypto merged commit 70e4858 into anza-xyz:master Jul 28, 2025
7 checks passed
wen-coding pushed a commit to wen-coding/alpenglow that referenced this pull request Jul 29, 2025
bw-solana pushed a commit to bw-solana/alpenglow that referenced this pull request Aug 1, 2025
bw-solana pushed a commit to bw-solana/alpenglow that referenced this pull request Aug 1, 2025
bw-solana pushed a commit to bw-solana/alpenglow that referenced this pull request Aug 1, 2025
bw-solana pushed a commit to bw-solana/alpenglow that referenced this pull request Aug 1, 2025
bw-solana pushed a commit to bw-solana/alpenglow that referenced this pull request Aug 1, 2025
bw-solana pushed a commit to bw-solana/alpenglow that referenced this pull request Aug 1, 2025
bw-solana pushed a commit to bw-solana/alpenglow that referenced this pull request Aug 2, 2025
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.

3 participants