feat(consensus,blockchain): Y4b — consumer fork-branching for BLS producer identity#4858
Draft
envestcc wants to merge 19 commits into
Draft
feat(consensus,blockchain): Y4b — consumer fork-branching for BLS producer identity#4858envestcc wants to merge 19 commits into
envestcc wants to merge 19 commits into
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.
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>
Foundational PR (Y1) for the BLS Producer Identity follow-up to IIP-52. Decouples Header from the secp256k1-only public key type so that the existing methods can transparently handle BLS-signed headers post-fork. No behavior change for pre-fork blocks. - Header.pubkey (crypto.PublicKey) -> Header.producerPubkey ([]byte) raw storage. The signature scheme is implied by len(blockSig): 65 bytes secp256k1 (pre-fork), 96 bytes BLS12-381 (post-fork). - Header.PublicKey() returns nil for non-secp256k1 producer pubkeys; new Header.ProducerPubKey() []byte exposes the raw bytes regardless of scheme. Callers that need a typed PublicKey for an address derivation should switch to ProducerAddress() or do a state lookup post-fork. - Header.VerifySignature() dispatches on len(blockSig); BLS path uses crypto.BLS12381PublicKeyFromBytes + Verify against HashHeaderCore. - Header.ProducerAddress() dispatches on len(blockSig). Pre-fork returns the io1... iotex address (existing behavior). Post-fork returns the hex encoding of the 48-byte BLS pubkey, which is the canonical post-fork operator identifier (per the IIP draft). Return type stays string; the format flips across the fork boundary. Builder/testing setters write producerPubkey directly. blockindexer.go is left as-is: its blk.PublicKey().Address() call returns nil for BLS-signed headers and errors with "failed to get pubkey", which is the safe fail-loud behavior until the per-block state lookup path (populating BlockCtx.Producer with the candidate's Operator address) lands in a later PR. 5 new unit tests cover the BLS dispatch paths: positive round-trip, proto round-trip, wrong-scheme rejection, and empty-pubkey rejection. 27 existing block-package tests continue to pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR iotexproject#4851 review feedback (envestcc): storing the producer pubkey as []byte sacrifices type information. Pre-Y1 it was crypto.PublicKey, which BLS can't satisfy because the interface bundles ECDSA-shaped identity methods (Address, Hash, EcdsaPublicKey) that have no meaningful BLS analogue — forcing BLS through Address() would invite silent truncation in hash.BytesToHash160 and common.BytesToAddress consumers. Compromise: a new narrow interface block.Verifier {Bytes; Verify} that both crypto.PublicKey (secp256k1) and *crypto.BLS12381PublicKey satisfy today without modification. Header.pubkey moves from []byte to Verifier. - VerifySignature: h.pubkey.Verify(digest, sig) — typed, no length switch - ProducerAddress: type-switch on the stored Verifier instead of dispatch on len(blockSig) — intent is explicit ("I'm a BLS key, hex-encode me") - LoadFromBlockHeaderProto: length-based dispatch lives only here, at the wire→typed boundary; downstream code sees the typed Verifier - PublicKey() accessor: type-assert to crypto.PublicKey; returns nil for BLS-signed headers (existing behavior, but no longer re-parses the bytes on every call) - ProducerPubKey(): pubkey.Bytes() with defensive copy - Identity-derivation (Address/Hash) is deliberately absent from Verifier — BLS has no iotex address; the rationale is captured in verifier.go and the BLS Producer Identity IIP draft Identity-shaped accessors that were length-dispatching are now type-dispatching; tests updated to write the typed key into the field rather than raw bytes. Added a decode-side rejection test for malformed pubkey lengths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation for the Y4 consumer migration. Adds two pieces of plumbing that Y4b will consume: 1. BlockCtx.ProducerPubKey []byte — raw producer pubkey bytes populated at BlockCtx assembly. Pre-fork this is the secp256k1 pubkey (33 / 65 B); post-fork it's the BLS12-381 pubkey (48 B). Consumers that match against state.Candidate.BLSPubKey use this rather than Producer.String(), since iotex-address derivation is undefined for a BLS pubkey. 2. CandidateCenter.GetByBLSPubKey + CandidateStateManager method — linear scan over candidates returning the one whose registered BLSPubKey matches. Mirrors the existing GetByName / GetByOwner / GetByOperator pattern. Used by Y4b consumers (reward attribution, EVM fee recipient, productivity tracking) to resolve a candidate from a BLS-signed header's ProducerPubKey. blockchain.go's three BlockCtx-assembly sites (Validate, contextWithBlock used by MintNewBlock + commitBlock) now populate ProducerPubKey: - Validate: blk.Header.ProducerPubKey() — the bytes carried on the header by Y1's length-dispatch. - MintNewBlock: producerPrivateKey.PublicKey().Bytes() — the producer signs with their own key, no header to consult yet. - commitBlock: blk.Header.ProducerPubKey() — same as Validate. No behaviour change for any existing consumer: Producer is still the iotex-address-shaped field they read today, ProducerPubKey is a NEW field that no one consumes yet. Y4b migrates EVM Coinbase to use a state-looked-up Reward address, reward.go to match by ProducerPubKey, ValidateBlockFooter to use roundCtx.IsProducer, etc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Block-validation needs to confirm the block's producer is in the round's delegate set. Pre-fork that works via IsDelegate(addr string) over the secp256k1-derived iotex address parsed from Header.ProducerAddress. Post-fork (BLS Producer Identity follow-up to IIP-52), the same accessor returns hex(blsPubKey), so the string compare in IsDelegate no longer hits anything. IsProducer takes the raw 48-byte BLS pubkey from Header.ProducerPubKey and walks round.delegates checking BLSPubKey.Bytes() with bytes.Equal — the canonical identifier post-fork. It rejects empty / nil inputs and delegates that have no registered BLS pubkey, so a header that drops the field or one that carries an unregistered key is not accepted. Pre-fork code paths continue to use IsDelegate; ValidateBlockFooter fork-branches between the two (lands in PR-Y4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ipient + ResolveBLSProducer Plumbing for the Y4b consumer migration. No behaviour change yet — all of the new fields / helpers are unused. Subsequent commits wire them into the EVM Coinbase, rewarding attribution, ValidateBlockFooter, and slasher productivity paths. - BlockCtx.FeeRecipient (address.Address): decouples "block producer identity" from "where fees go". Pre-fork the two coincide (a delegate's secp256k1 producer identity also receives fees); post-fork they split per the Eth2 fee_recipient model — Producer keeps the candidate's iotex identity for slasher / display, FeeRecipient is the looked-up Candidate.Reward. - FeatureCtx.UseBLSProducerIdentity: gates the Y4b migration. Shares IsToBeEnabled height with EnableBLSAggregation today but stays a semantically distinct switch so call sites read self-descriptively and the two can diverge if a hotfix requires it. - staking.ResolveBLSProducer(ctx, sr, blsPubKey) → (Operator, Reward, error): one linear-scan lookup over the candidate center. Returns errors for empty pubkey, missing candidate, or missing operator / reward — blockchain.go uses these to fail block validation early rather than install a half-populated BlockCtx. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ateBlock + commitBlock Post-fork, blk.PublicKey() returns nil for a BLS-signed header (48-byte BLS pubkey isn't a crypto.PublicKey), so the long-standing blk.PublicKey().Address() pattern panics. Introduce an injection point so blockchain can populate BlockCtx.Producer / FeeRecipient from candidate state without importing the staking package (staking → blockchain dependency is fine; the reverse would be a cycle). - New BLSProducerResolver interface — single method ResolveBLSProducer(ctx, blsPubKey) → (operator, reward, err). Wired in chainservice/builder.go in a follow-up commit using staking.ResolveBLSProducer. - New BLSProducerResolverOption to plumb a resolver into the blockchain construction option chain alongside BlockValidatorOption. - New resolveBlockProducer(ctx, blk) helper — pre-fork: legacy blk.PublicKey().Address() for both Producer + FeeRecipient. Post-fork: defers to the injected resolver. Returns an error (treated as block-validation failure by callers) rather than panicking if no resolver is wired or no candidate matches. - New contextWithBlockAndRecipient — full-arity twin of contextWithBlock that distinguishes Producer (iotex-shaped identity used by slasher / display) from FeeRecipient (EVM Coinbase / reward attribution). contextWithBlock now forwards to it with Producer == FeeRecipient (the pre-fork coincidence). - ValidateBlock + commitBlock switched over to resolveBlockProducer. MintNewBlock is intentionally NOT switched in this commit — it needs the node's own BLS pubkey from config, which is Y5/Y6 work. A TODO at the mint site documents the divergence-on-mint-post-fork hazard for that follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt semantics) Pre-fork the EVM Coinbase reads BlockCtx.Producer because the delegate's secp256k1 identity is both the producer-of-record AND the address that receives fees. Post-fork (UseBLSProducerIdentity) these split: Producer keeps the iotex-shaped identity (operator address) for slasher / display / log lines, while FeeRecipient is the candidate's configured Reward address — the same separation Ethereum's consensus client makes between proposer pubkey and fee_recipient. Both EVM entry points (the main interpreter in protocol/execution/evm and the contract-backend interpreter used for system-contract calls in state/factory/erigonstore) are migrated symmetrically. An unset FeeRecipient (legacy callers / synthetic BlockCtx values) falls back to Producer so existing fixtures keep working pre-fork even without the new field populated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ent post-fork Pre-fork, GrantBlockReward scans the poll-active-set candidate list to find candidate.Address == blkCtx.Producer, then reads candidate.Reward. Post-fork (UseBLSProducerIdentity) the candidate's Reward address has already been resolved at BlockCtx assembly time via the BLSProducerResolver (commit "resolve BLS producer via injected resolver in ValidateBlock + commitBlock") and lives on blkCtx.FeeRecipient — that's the EXACT same lookup the loop is re-doing, just one ns earlier and against the full candidate center rather than the epoch's active subset. Use FeeRecipient directly post-fork. Besides being more efficient, this stays correct in the corner where a delegate is dropped from the active set between BlockCtx assembly and reward attribution — the legacy scan would silently skip the reward there, while the direct lookup honours the resolved address. Pre-fork path is unchanged. FeeRecipient nil-guard preserves backward compat for any caller that builds a synthetic BlockCtx without populating the new field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…IsProducer Pre-fork blk.ProducerAddress() returns an iotex address derived from the header's secp256k1 pubkey, which IsDelegate compares against delegate.Address (also an iotex address). Post-fork the header carries a 48-byte BLS pubkey; ProducerAddress falls back to hex(pubkey) and IsDelegate's address-compare can never match — every BLS-signed block would be rejected as "not a valid delegate". Branch on header pubkey length (the same wire-boundary dispatch used by block/header.go): 48 bytes → BLS, so look the producer up by raw pubkey via roundCtx.IsProducer (added in Y3); other lengths keep the legacy address-based IsDelegate path. Length is a stable property of the header itself, so no FeatureCtx threading is needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sites for BLS blocks Three more sites read producer identity from blk.PublicKey().Address() unconditionally, each panicking on a post-fork BLS-signed header (blk.PublicKey() returns nil for non-secp256k1 keys per the Y1 length-dispatch contract): - api/coreservice.go (simulateAction + traceBlock): route both BlockCtx assemblies through the newly-exported Blockchain.ResolveBlockProducer so the fork branching lives in exactly one place. These are read-only simulate / trace paths so they take a returned error rather than panicking. mock_blockchain regenerated for the interface change. - state/factory/statedb.go (PutBlock): the producer variable was computed and nil-checked but never read. Remove the dead block — it serves no purpose pre-fork and panics post-fork. - blockchain/blockdao/blockindexer.go (CheckIndexer catch-up loop): this is the startup-only resync path. Wiring a BLSProducerResolver through BlockIndexerChecker is invasive (new constructor arg, threads through wherever NewBlockIndexerChecker is called) and would balloon the Y4b diff. Add a TODO scoped to Y8 (e2e + activation gate wiring) — the path is unreachable until a node has to catch up past the fork height, which can't happen before activation anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onstruction Provide the BLSProducerResolver instance that blockchain.NewBlockchain needs. The adapter is a 4-line shim over staking.ResolveBLSProducer backed by builder.cs.factory (which already satisfies protocol.StateReader). Lives here rather than in the blockchain package because blockchain must stay free of staking imports — the reverse import direction (staking → blockchain) is fine. chainservice already sits above both packages, so it's the natural place for the seam. This closes the Y4b consumer-migration loop: ValidateBlock, commitBlock, and the API simulate / trace paths can now construct a correct post-fork BlockCtx with Producer = candidate.Operator and FeeRecipient = candidate.Reward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
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.



Summary
Y4b of the BLS Producer Identity follow-up to IIP-52. Builds on Y4a
(#4857) which added
BlockCtx.ProducerPubKey+ thecandCenter.GetByBLSPubKeyread-only seam. This PR is the consumermigration: under
UseBLSProducerIdentity, post-fork BLS-signed blocksno longer panic through
blk.PublicKey().Address()and downstreamconsumers branch correctly on the new producer identity model.
Stacked on #4857 — please review / merge that first. Until it
merges, the diff here includes Y4a's commits at the top of the list;
those will fall out once Y4a lands on master. The Y4b-only commits
are the seven at the bottom of the commit list:
Design
Eth2-style split of "block producer identity" from "fee recipient":
BlockCtx.Producer— keeps the iotex-shaped identity. Pre-fork:blk.PublicKey().Address()(delegate's secp256k1 address). Post-fork:the candidate's
Operatoraddress, resolved from the header's BLSpubkey. Stays the canonical key for slasher productivity,
poll/util.gocaller checks, log lines.BlockCtx.FeeRecipient(new in this PR) — the address thatreceives block-level credits. Pre-fork: equal to
Producer. Post-fork:the candidate's
Rewardaddress. Consumed by EVMCoinbaseandrewarding attribution.
FeatureCtx.UseBLSProducerIdentity— height-gated viaGenesis.ToBeEnabled, shares the height withEnableBLSAggregationbut stays a semantically distinct switch.
The blockchain package gets a
BLSProducerResolverinterface and aBLSProducerResolverOptionso it can do the BLS-pubkey → candidatelookup without importing
staking(which would invert the dependency).The implementation lives in
chainservice/bls_producer_resolver.goasa 4-line shim over
staking.ResolveBLSProducer.Deferred TODOs (in-code, scoped)
MintNewBlockpost-fork BlockCtx (Y5/Y6) — needs the node's ownBLS pubkey from config. Today's mint path is consistent with pre-fork
behaviour; post-fork it would produce a BlockCtx that disagrees with
the validator's view (state-root divergence on minted blocks). TODO
comment at the mint site documents the hazard.
blockindexer.gocatch-up loop (Y8) — threading a resolverthrough
BlockIndexerCheckeris too invasive for this PR. Startup-onlyresync, unreachable until activation.
Test plan
go build ./...go test ./action/protocol/rewarding/... ./action/protocol/execution/evm/... ./consensus/scheme/rolldpos/...— 126 passedmint-time + startup catch-up deferred) is the right boundary
🤖 Generated with Claude Code