Skip to content

feat(consensus,blockchain): Y4b — consumer fork-branching for BLS producer identity#4858

Draft
envestcc wants to merge 19 commits into
iotexproject:masterfrom
envestcc:bls-producer-identity-y4b
Draft

feat(consensus,blockchain): Y4b — consumer fork-branching for BLS producer identity#4858
envestcc wants to merge 19 commits into
iotexproject:masterfrom
envestcc:bls-producer-identity-y4b

Conversation

@envestcc

Copy link
Copy Markdown
Member

Summary

Y4b of the BLS Producer Identity follow-up to IIP-52. Builds on Y4a
(#4857) which added BlockCtx.ProducerPubKey + the
candCenter.GetByBLSPubKey read-only seam. This PR is the consumer
migration: under UseBLSProducerIdentity, post-fork BLS-signed blocks
no longer panic through blk.PublicKey().Address() and downstream
consumers 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:

feat(protocol):     Y4b foundation — UseBLSProducerIdentity flag + FeeRecipient + ResolveBLSProducer
feat(blockchain):   resolve BLS producer via injected resolver in ValidateBlock + commitBlock
feat(evm):          route Coinbase to FeeRecipient post-fork (Eth2 fee_recipient semantics)
feat(rewarding):    short-circuit block-reward attribution via FeeRecipient post-fork
feat(consensus):    ValidateBlockFooter accepts BLS-signed producer via IsProducer
feat(api,state):    clean remaining blk.PublicKey().Address() nil-panic sites for BLS blocks
feat(chainservice): wire stakingBLSProducerResolver into blockchain construction

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 Operator address, resolved from the header's BLS
    pubkey. Stays the canonical key for slasher productivity,
    poll/util.go caller checks, log lines.
  • BlockCtx.FeeRecipient (new in this PR) — the address that
    receives block-level credits. Pre-fork: equal to Producer. Post-fork:
    the candidate's Reward address. Consumed by EVM Coinbase and
    rewarding attribution.
  • FeatureCtx.UseBLSProducerIdentity — height-gated via
    Genesis.ToBeEnabled, shares the height with EnableBLSAggregation
    but stays a semantically distinct switch.

The blockchain package gets a BLSProducerResolver interface and a
BLSProducerResolverOption so it can do the BLS-pubkey → candidate
lookup without importing staking (which would invert the dependency).
The implementation lives in chainservice/bls_producer_resolver.go as
a 4-line shim over staking.ResolveBLSProducer.

Deferred TODOs (in-code, scoped)

  • MintNewBlock post-fork BlockCtx (Y5/Y6) — needs the node's own
    BLS 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.go catch-up loop (Y8) — threading a resolver
    through BlockIndexerChecker is too invasive for this PR. Startup-only
    resync, unreachable until activation.

Test plan

  • go build ./...
  • go test ./action/protocol/rewarding/... ./action/protocol/execution/evm/... ./consensus/scheme/rolldpos/... — 126 passed
  • e2e coverage lands with Y8 once activation gate + minter wiring are in
  • Reviewer: confirm the Y4b scope split (consumer paths now,
    mint-time + startup catch-up deferred) is the right boundary

🤖 Generated with Claude Code

envestcc and others added 19 commits May 27, 2026 10:53
…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>
@envestcc envestcc requested a review from a team as a code owner June 22, 2026 04:35
@sonarqubecloud

Copy link
Copy Markdown

@envestcc envestcc marked this pull request as draft June 23, 2026 01:42
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.

1 participant