feat(consensus): BlockFooter BLS aggregation (IIP-52)#4847
Draft
envestcc wants to merge 8 commits into
Draft
Conversation
…te (IIP-52) Scaffolding for the BLS signature aggregation work tracked in IIP-52. No behavior change yet: EnableBLSAggregation is gated on IsToBeEnabled, and the BLS keys plumbed into rollDPoSCtx are not yet used to sign or verify endorsements. - blockchain/config.go: add Chain.BLSProducerPrivKey (comma-separated hex) and BLSProducerPrivateKeys(). Empty value falls back to deriving each BLS key from the corresponding ECDSA producer key via crypto.GenerateBLS12381PrivateKey. - consensus/scheme/rolldpos: Builder.SetBLSPriKey; NewRollDPoSCtx accepts []*crypto.BLS12381PrivateKey aligned 1:1 with producer ECDSA keys; rollDPoSCtx stores them on blsPriKeys for the upcoming signing path. - consensus/consensus.go: wire SetBLSPriKey(cfg.Chain.BLSProducerPrivateKeys()). - action/protocol/context.go: FeatureCtx.EnableBLSAggregation gated on g.IsToBeEnabled(height); flips to a named hardfork height later. - go.mod: bump iotex-proto to envestcc/iotex-proto bls-aggregate (52e72a6) for the BlockFooter aggregated_signature / signer_bitmap fields and the BLSEndorsement message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches consensus vote signing to BLS12-381 post-fork, reusing the existing Endorsement type and dispatching on signature length (65B secp256k1 vs 96B BLS). Receiver verification and endorsement-manager quorum integration land in a follow-up PR; with the feature gate parked at IsToBeEnabled this commit is dead code in production. - endorsement.EndorseBLS / VerifyBLSEndorsement: thin helpers that produce / verify a regular *Endorsement whose signature field carries a BLS sig. Endorser remains the delegate's secp256k1 producer key so the existing Endorser().Address() path still resolves the iotex address; receivers look up the BLS verifying key from candidate state by that address. - ConsensusConfig.BLSAggregationEnabled(height): feature gate wired off Genesis.ToBeEnabledBlockHeight. Will be re-pointed at a named hardfork height once the full Phase-2 stack lands. - rollDPoSCtx.newEndorsement / endorseBlockProposal: post-fork, sign PROPOSAL, LOCK and COMMIT votes plus the proposer's wrapping endorsement with BLS (skipping delegates without a configured BLS key). Block header signing remains on the ECDSA producer key — that signature ties the block to chain identity and is unrelated to the consensus vote layer. - The proposer's producerKey (ECDSA + BLS + address) is threaded through Proposal / mintNewBlock / endorseBlockProposal so the branch can pick the right key without a separate lookup. - iotex-proto bump to envestcc/iotex-proto@e4439ef (PR iotexproject#169): clarifies Endorsement.signature semantics (pre-fork 65B secp256k1, post-fork 96B BLS, distinguished by length). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stacks on top of the BLS sender PR. Wires up the receiver side so BLS- signed consensus endorsements are accepted, verified against pubkeys resolved from candidate state, and counted toward quorum. With the feature gate parked at IsToBeEnabled this is dead code in production; intended for local iteration until the sender PRs land. - BLSPubKeysByEpochFunc callback type + Builder.SetBLSPubKeysByEpochFunc wired through to NewRollDPoSCtx. - consensus.go provides the implementation: reads the epoch's delegate list from candidate state and extracts each candidate's registered BLSPubKey, returning a map keyed by operator iotex address. - roundCalculator caches the BLS pubkey index per round, decoded as *crypto.BLS12381PublicKey. UpdateRound carries it across height transitions inside an epoch and re-fetches on epoch boundaries. - roundCtx.BLSPubKey(addr) accessor; roundCtx.verifyEndorsement(doc, en) dispatches on signature length (65B secp256k1 vs 96B BLS). - rollDPoSCtx.VerifyEndorsement(height, doc, en) is the public entry point; same length-aware dispatch plus pre/post-fork gating — pre-fork rejects 96B sigs, post-fork rejects 65B sigs. - HandleConsensusMsg replaces the unconditional ECDSA verify with the new VerifyEndorsement; CheckBlockProposer's proofOfLock replay path flows through round.AddVoteEndorsement which now dispatches on signature length internally, so BLS endorsements in proof-of-lock are verified transparently.
- endorsement/bls_endorsement_test.go: round-trip EndorseBLS / VerifyBLSEndorsement, plus negative cases (wrong pubkey, tampered document, tampered signature, nil inputs). Uses deterministic in-test keys (no identityset dep from this package). - consensus/scheme/rolldpos/bls_verify_test.go: covers the two-layer dispatch — roundCtx.verifyEndorsement (signature-length branch, BLS pubkey lookup miss, mismatched pubkey) and rollDPoSCtx.VerifyEndorsement (length gating with the BLS aggregation feature flag in both directions). Plus sender tests that newEndorsement emits the expected signature scheme based on the feature gate. 11 new tests; everything in the affected packages still passes (51 tests total across endorsement/ and consensus/scheme/rolldpos/).
- Bundle delegate address with BLS pubkey into a single 'delegate' struct; roundCtx.delegates becomes []delegate, dropping the parallel blsPubKeys map. roundCalc.delegatesAt replaces blsPubKeysFor and merges both callbacks into one slice — single source of truth for the address/pubkey alignment. - rollDPoSCtx.VerifyEndorsement takes a single *EndorsedConsensusMessage instead of (height, doc, en); the message already carries all three. - Shrink VerifyEndorsement lock scope: snapshot round + feature flag under RLock, release before doing the (potentially slow) signature verification. Safe because *roundCtx is replaced, never mutated in place. Test helpers + the two consumers (HandleConsensusMsg, roundCtx_test) updated. All targeted tests pass.
Per follow-up review on PR iotexproject#4843: remove the separate BLSPubKeysByEpochFunc callback. NodesSelectionByEpochFunc now returns []*Delegate (exported), where each Delegate pairs the operator address with its decoded BLS12-381 public key. consensus.go builds these from candidate state in one pass; roundCalculator stores them directly and no longer needs a parallel lookup/merge step. - Export delegate -> Delegate{Address, BLSPubKey}. - NodesSelectionByEpochFunc: ([]string) -> ([]*Delegate). - Drop BLSPubKeysByEpochFunc type, Builder.SetBLSPubKeysByEpochFunc, the NewRollDPoSCtx param, and roundCalculator.delegatesAt / blsPubKeysFor. - roundCalculator.Delegates returns []*Delegate; Proposers extracts addresses; IsDelegate scans by address. - consensus.go decodes each candidate's BLS pubkey once per epoch in delegatesByEpochFunc; proposersByEpochFunc reuses it. Net -68 lines. Build + vet clean; targeted tests pass.
Per PR iotexproject#4843 review: UpdateRound was reaching into round.delegates directly because Delegates() returned []string while the field is []*Delegate. Make the accessor return []*Delegate so UpdateRound (and any future caller) can use round.Delegates() consistently. - roundCtx.Delegates() now returns []*Delegate. - endorsementManager.Log's (unused) delegates param retyped to []*Delegate. - The one genuine []string consumer (ConsensusMetrics.LatestDelegates, an external metrics field) extracts addresses at the call site.
envestcc
commented
Jun 3, 2026
| return err | ||
| } | ||
| blkHash := blk.HashBlock() | ||
| if blk.Footer.IsAggregated() { |
Member
Author
There was a problem hiding this comment.
When verifying whether there are enough endorsements, for the two handling approaches before and after the hardfork, I think it would be best to use the same piece of verification code for cases where votes exceed 2/3. Currently, it is implemented separately in two places, which is not good for maintainability.
Phase 2 deliverable for IIP-52: proposer aggregates the per-block COMMIT BLS signatures into a single 96-byte sig + a signer bitmap, stored in BlockFooter.aggregated_signature and BlockFooter.signer_bitmap. Verifiers reconstruct the signer set from the bitmap, look up each BLS pubkey from the round's delegate index, and FastAggregateVerify the aggregate against the shared COMMIT-vote hash. - blockchain/block/footer.go: new fields aggregatedSignature, signerBitmap; proto round-trip; IsAggregated / AggregatedSignature / SignerBitmap accessors. - blockchain/block/block.go: new Block.FinalizeWithAggregate; the one-shot contract is preserved via a commitTime witness so either path errors on second call. - consensus/scheme/rolldpos/aggregate.go: aggregateCommitEndorsements builds the aggregate sig + bitmap from a slice of BLS COMMIT endorsements indexed against the round's delegates; bitmapSigners is the inverse for the verifier. - consensus/scheme/rolldpos/rolldposctx.go: at commit time, branch on BLSAggregationEnabled and call FinalizeWithAggregate post-fork. - consensus/scheme/rolldpos/rolldpos.go: ValidateBlockFooter routes aggregated footers through validateAggregatedFooter — bitmap → delegates → BLS pubkeys → BLSAggregateSignature.Verify, with a separate 2/3 majority check. - action/protocol/staking/protocol.go: ActiveCandidates filters out candidates without a registered BLS pubkey once aggregation is enabled, so the aggregate signer set is well-defined. - endorsement/endorsement.go: expose SigningHash so the verifier can reconstruct the COMMIT-vote hash from blk.CommitTime() outside an Endorsement struct. All signers sign the same hash (deterministic ts from round start + TTL sum), which is what FastAggregateVerify requires. 8 new unit tests cover the aggregate round-trip, partial signer sets, rejection of non-BLS endorsements / unknown endorsers, and bitmap edge cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
854e6ad to
ff589bf
Compare
Member
Author
|
Done in
Quorum-rule changes (if we ever revisit 2/3) stay in one place. |
|
This was referenced Jun 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Draft — depends on #4843 → #4842 → #4841. Stacks on the BLS endorsement receiver PR. Once those merge in order, this PR's diff will collapse to just the footer-aggregation changes.
Phase 2 deliverable for IIP-52. Proposer aggregates the per-block COMMIT BLS signatures into a single 96-byte signature plus a signer bitmap, stored in
BlockFooter.aggregated_signature/BlockFooter.signer_bitmap. Verifiers reconstruct the signer set from the bitmap, look up each BLS pubkey from the round's delegate index, andFastAggregateVerifyagainst the shared COMMIT-vote hash.With the feature gate parked at
IsToBeEnabled, this PR is dead code in production until the gate flips.Wire layout (per IIP-52, unchanged from PR #169)
What's in
blockchain/block/footer.go— new fields, proto round-trip,IsAggregated/AggregatedSignature/SignerBitmapaccessors. Pre-fork blocks serialize empty bytes in fields 3/4 — append-only safe.blockchain/block/block.go— newBlock.FinalizeWithAggregate(aggSig, bitmap, ts)mirroring the existingFinalizecontract. One-shot enforcement moved offlen(endorsements) != 0onto acommitTimewitness so both paths error consistently on a second call.consensus/scheme/rolldpos/aggregate.go—aggregateCommitEndorsementsbuilds the aggregate sig + bitmap from a slice of BLS COMMIT endorsements, indexed against the round's[]*Delegate.bitmapSignersis the inverse for the verifier (with an out-of-range check).consensus/scheme/rolldpos/rolldposctx.go— at commit time, branch onBLSAggregationEnabled(height)and callFinalizeWithAggregatepost-fork.consensus/scheme/rolldpos/rolldpos.go—ValidateBlockFooterroutes aggregated footers throughvalidateAggregatedFooter: bitmap → delegates → BLS pubkeys →BLSAggregateSignature.Verify, with a separate 2/3 majority check.action/protocol/staking/protocol.go—ActiveCandidatesfilters out candidates without a registered BLS pubkey post-fork so the aggregate signer set is well-defined.endorsement/endorsement.go— exposeSigningHashso the verifier can reconstruct the COMMIT-vote hash fromblk.CommitTime()outside anEndorsementstruct.Why the common signing hash works
FastAggregateVerifyrequires every signer to have signed the same message. The COMMIT endorsement timestamp is a deterministic function ofround.StartTime() + AcceptBlockTTL + AcceptProposalEndorsementTTL + AcceptLockEndorsementTTL— every delegate computes it identically. The proposer stores that timestamp onBlock.commitTimeat finalize, and the verifier feeds it back intoendorsement.SigningHash(vote, blk.CommitTime())to recompute the hash. No per-signer message variation.Tests
TestAggregateCommitEndorsements_RoundTrip— 4-of-4 set, aggregate verifies viaBLSAggregateSignature.VerifyTestAggregateCommitEndorsements_PartialSet— 3-of-5 set, bitmap correctly identifies signersTestAggregateCommitEndorsements_RejectsNonBLSSig— secp256k1 endorsement in the input rejectedTestAggregateCommitEndorsements_RejectsUnknownEndorser— endorser not in delegate set rejectedTestBitmapSigners_BitBeyondDelegateCount— bitmap claiming non-existent delegate rejectedTestBitmapSigners_EmptyBitmap— empty input yields empty signer listTestFooter_AggregateSerDes— footer proto round-trip preserves agg sig + bitmap + IsAggregatedAll 25 BLS-related tests across
endorsement/,blockchain/block/,consensus/scheme/rolldpos/pass.Not yet in (future work)
IsToBeEnabled. Will be flipped to a named fork once the full stack is reviewed.bc blockdecode of the new footer fields; explorer / API updates. Separate small PR.Test plan
go build ./...go vet ./...🤖 Generated with Claude Code