Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions action/protocol/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ type (
GasLimit uint64
// Producer is the address of whom composes the block containing this action
Producer address.Address
// ProducerPubKey is the raw bytes of the block producer's public key
// (secp256k1 33/65B pre-fork, BLS12-381 48B once BLS-signed headers
// activate as part of the BLS Producer Identity follow-up to IIP-52).
// Post-fork callers that need to match against state.Candidate.BLSPubKey
// use this rather than Producer.String(), since iotex-address derivation
// is undefined for a BLS pubkey. May be empty for genesis / synthetic
// BlockCtx values that have no underlying Header.
ProducerPubKey []byte
// AccumTips is the accumulated tips of the block
AccumulatedTips big.Int
// BaseFee is the base fee of the block
Expand Down Expand Up @@ -172,6 +180,7 @@ type (
// contracts are committed and written back
AlwaysWriteCachedContract bool
NoCandidateExitQueue bool
EnableBLSAggregation bool
}

// FeatureWithHeightCtx provides feature check functions.
Expand Down Expand Up @@ -346,6 +355,7 @@ func WithFeatureCtx(ctx context.Context) context.Context {
PrePectraEVM: !g.IsYap(height),
AlwaysWriteCachedContract: !g.IsYap(height),
NoCandidateExitQueue: !g.IsYap(height),
EnableBLSAggregation: g.IsToBeEnabled(height),
},
)
}
Expand Down
21 changes: 21 additions & 0 deletions action/protocol/staking/candidate_center.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,27 @@ func (m *CandidateCenter) ContainsOwner(owner address.Address) bool {
return false
}

// GetByBLSPubKey returns the candidate whose registered BLS pubkey
// matches the given bytes, or nil if none does. Linear scan over All()
// — registration / update are rare and the active candidate set is
// bounded, trading O(N) lookup for not having to maintain another
// index map across the change / base commit flow. Used by Y4b
// consumers (reward attribution, EVM fee recipient, productivity
// tracking) to resolve a candidate from a BLS-signed header's
// ProducerPubKey without going through iotex-address derivation,
// which is undefined for BLS keys.
func (m *CandidateCenter) GetByBLSPubKey(blsPubKey []byte) *Candidate {
if len(blsPubKey) == 0 {
return nil
}
for _, d := range m.All() {
if bytes.Equal(d.BLSPubKey, blsPubKey) {
return d
}
}
return nil
}

// ContainsOperator returns true if the map contains the candidate by operator
func (m *CandidateCenter) ContainsOperator(operator address.Address) bool {
if operator == nil {
Expand Down
34 changes: 34 additions & 0 deletions action/protocol/staking/candidate_center_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,3 +645,37 @@ func TestCandidateUpsert(t *testing.T) {
r.Equal(cand, m.GetByIdentifier(cand.GetIdentifier()))
})
}

// TestCandidateCenter_GetByBLSPubKey covers the lookup used by Y4b
// consumers (reward attribution, EVM fee recipient) to resolve a
// candidate from a BLS-signed header's ProducerPubKey.
func TestCandidateCenter_GetByBLSPubKey(t *testing.T) {
r := require.New(t)
c, err := NewCandidateCenter(nil)
r.NoError(err)

pkA := []byte("dummy-bls-pubkey-A-48-bytes-pad-________________")[:48]
pkB := []byte("dummy-bls-pubkey-B-48-bytes-pad-________________")[:48]

candA := &Candidate{
Owner: identityset.Address(1),
Operator: identityset.Address(7),
Reward: identityset.Address(1),
Name: "cand-a",
Votes: big.NewInt(0),
SelfStake: big.NewInt(0),
SelfStakeBucketIdx: 0,
BLSPubKey: pkA,
}
r.NoError(c.Upsert(candA))
r.NoError(c.commit())

r.Nil(c.GetByBLSPubKey(nil), "nil pubkey returns nil")
r.Nil(c.GetByBLSPubKey([]byte{}), "empty pubkey returns nil")
r.Nil(c.GetByBLSPubKey(pkB), "unregistered pubkey returns nil")

got := c.GetByBLSPubKey(pkA)
r.NotNil(got, "registered pubkey returns the candidate")
r.Equal(candA.Name, got.Name)
r.Equal(candA.Owner.String(), got.Owner.String())
}
11 changes: 11 additions & 0 deletions action/protocol/staking/candidate_statemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ type (
GetByOwner(address.Address) *Candidate
GetByIdentifier(address.Address) *Candidate
GetByOperator(address.Address) *Candidate
// GetByBLSPubKey returns the candidate whose registered BLS pubkey
// matches the given bytes, or nil if no such candidate exists.
// Used by Y4b consumers (reward attribution, EVM fee recipient,
// productivity tracking) to resolve a candidate from a BLS-signed
// block's Header.ProducerPubKey without going through the iotex
// address derivation, which is undefined for BLS keys.
GetByBLSPubKey([]byte) *Candidate
Upsert(*Candidate) error
CreditBucketPool(*big.Int, bool) error
DebitBucketPool(*big.Int, bool) error
Expand Down Expand Up @@ -139,6 +146,10 @@ func (csm *candSM) ContainsOperator(addr address.Address) bool {
return csm.candCenter.ContainsOperator(addr)
}

func (csm *candSM) GetByBLSPubKey(blsPubKey []byte) *Candidate {
return csm.candCenter.GetByBLSPubKey(blsPubKey)
}

func (csm *candSM) ContainsSelfStakingBucket(index uint64) bool {
return csm.candCenter.ContainsSelfStakingBucket(index)
}
Expand Down
12 changes: 10 additions & 2 deletions action/protocol/staking/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -972,9 +972,17 @@ func (p *Protocol) ActiveCandidates(ctx context.Context, sr protocol.StateReader
if err != nil {
return nil, err
}
if active {
cand = append(cand, list[i])
if !active {
continue
}
// Post-fork (IIP-52): drop candidates without a registered BLS public
// key so the per-block aggregate signature has a well-defined signer
// set. Pre-fork the BLSPubKey field is empty for every candidate and
// this filter is a no-op.
if fCtx.EnableBLSAggregation && len(list[i].BLSPubKey) == 0 {
continue
}
cand = append(cand, list[i])
}
return cand.toStateCandidateList(protocol.MustGetFeatureWithHeightCtx(ctx).CandidateWithoutIdentityStorage(height))
}
Expand Down
36 changes: 34 additions & 2 deletions blockchain/block/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ func (b *Block) RunnableActions() RunnableActions {
return RunnableActions{actions: b.Actions, txHash: b.txRoot}
}

// Finalize creates a footer for the block
// Finalize creates a footer for the block using the per-delegate endorsements
// path. Used pre-fork (and as the legacy entry point); post-fork blocks are
// finalized via FinalizeWithAggregate instead.
func (b *Block) Finalize(endorsements []*endorsement.Endorsement, ts time.Time) error {
if len(b.endorsements) != 0 {
if b.isFinalized() {
return errors.New("the block has been finalized")
}
b.endorsements = endorsements
Expand All @@ -85,6 +87,36 @@ func (b *Block) Finalize(endorsements []*endorsement.Endorsement, ts time.Time)
return nil
}

// FinalizeWithAggregate creates a footer for the block using the BLS12-381
// aggregate signature path (IIP-52). The aggregate signature is a single
// 96-byte BLS sig over the per-block COMMIT vote; signerBitmap identifies
// which epoch delegates contributed (bit i = delegate i in the epoch's
// delegate list, LSB-first within each byte).
//
// Same one-shot contract as Finalize: a block can only be finalized once.
func (b *Block) FinalizeWithAggregate(aggregatedSignature, signerBitmap []byte, ts time.Time) error {
if b.isFinalized() {
return errors.New("the block has been finalized")
}
if len(aggregatedSignature) == 0 {
return errors.New("aggregated signature is empty")
}
if len(signerBitmap) == 0 {
return errors.New("signer bitmap is empty")
}
b.aggregatedSignature = append([]byte(nil), aggregatedSignature...)
b.signerBitmap = append([]byte(nil), signerBitmap...)
b.commitTime = ts
return nil
}

// isFinalized reports whether Finalize/FinalizeWithAggregate has already been
// called for this block. Either path sets commitTime as part of its work, so
// a non-zero commitTime is a sufficient witness.
func (b *Block) isFinalized() bool {
return len(b.endorsements) != 0 || len(b.aggregatedSignature) != 0 || !b.commitTime.IsZero()
}

// TransactionLog returns transaction logs in the block
func (b *Block) TransactionLog() *BlkTransactionLog {
if len(b.Receipts) == 0 {
Expand Down
45 changes: 42 additions & 3 deletions blockchain/block/footer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ import (
"github.com/iotexproject/iotex-proto/golang/iotextypes"
)

// Footer defines a set of proof of this block
// Footer defines a set of proof of this block. Pre-fork the proof is the
// per-delegate COMMIT endorsements (endorsements). Once BLS signature
// aggregation is activated (IIP-52) the proof is the per-block aggregated
// signature plus a bitmap identifying which epoch delegates contributed; the
// endorsements slice stays empty.
type Footer struct {
endorsements []*endorsement.Endorsement
commitTime time.Time
endorsements []*endorsement.Endorsement
commitTime time.Time
aggregatedSignature []byte
signerBitmap []byte
}

// Proto converts BlockFooter
Expand All @@ -30,6 +36,12 @@ func (f *Footer) Proto() *iotextypes.BlockFooter {
for _, en := range f.endorsements {
pb.Endorsements = append(pb.Endorsements, en.Proto())
}
if len(f.aggregatedSignature) > 0 {
pb.AggregatedSignature = append([]byte(nil), f.aggregatedSignature...)
}
if len(f.signerBitmap) > 0 {
pb.SignerBitmap = append([]byte(nil), f.signerBitmap...)
}
return &pb
}

Expand All @@ -43,6 +55,12 @@ func (f *Footer) ConvertFromBlockFooterPb(pb *iotextypes.BlockFooter) error {
}
commitTime := pb.GetTimestamp().AsTime()
f.commitTime = commitTime
if aggSig := pb.GetAggregatedSignature(); len(aggSig) > 0 {
f.aggregatedSignature = append([]byte(nil), aggSig...)
}
if bitmap := pb.GetSignerBitmap(); len(bitmap) > 0 {
f.signerBitmap = append([]byte(nil), bitmap...)
}
pbEndorsements := pb.GetEndorsements()
if pbEndorsements == nil {
return nil
Expand All @@ -69,6 +87,27 @@ func (f *Footer) Endorsements() []*endorsement.Endorsement {
return f.endorsements
}

// AggregatedSignature returns the BLS12-381 aggregate signature over the
// per-block COMMIT vote (96 bytes, G2 compressed) when BLS signature
// aggregation is activated; empty for pre-fork blocks.
func (f *Footer) AggregatedSignature() []byte {
return append([]byte(nil), f.aggregatedSignature...)
}

// SignerBitmap returns the bitmap identifying which epoch delegates
// contributed to AggregatedSignature. Bit i (LSB-first within each byte)
// corresponds to delegate i in the epoch's delegate list. Empty for
// pre-fork blocks.
func (f *Footer) SignerBitmap() []byte {
return append([]byte(nil), f.signerBitmap...)
}

// IsAggregated reports whether this footer carries a BLS aggregate signature
// rather than the per-delegate endorsements list.
func (f *Footer) IsAggregated() bool {
return len(f.aggregatedSignature) > 0
}

// Serialize returns the serialized byte stream of the block footer
func (f *Footer) Serialize() ([]byte, error) {
return proto.Marshal(f.Proto())
Expand Down
31 changes: 28 additions & 3 deletions blockchain/block/footer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

func TestConvertToBlockFooterPb(t *testing.T) {
require := require.New(t)
footer := &Footer{nil, time.Now()}
footer := &Footer{endorsements: nil, commitTime: time.Now()}
blockFooter := footer.Proto()
require.NotNil(blockFooter)
require.Equal(0, len(blockFooter.Endorsements))
Expand All @@ -43,7 +43,7 @@ func TestConvertFromBlockFooterPb(t *testing.T) {

func TestSerDesFooter(t *testing.T) {
require := require.New(t)
footer := &Footer{nil, time.Now()}
footer := &Footer{endorsements: nil, commitTime: time.Now()}
ser, err := footer.Serialize()
require.NoError(err)
require.NoError(footer.Deserialize(ser))
Expand All @@ -57,10 +57,35 @@ func TestSerDesFooter(t *testing.T) {
require.Equal(1, len(footer.endorsements))
}

func TestFooter_AggregateSerDes(t *testing.T) {
require := require.New(t)
aggSig := make([]byte, 96)
for i := range aggSig {
aggSig[i] = byte(i)
}
bitmap := []byte{0xa5, 0x03}
footer := &Footer{
commitTime: time.Unix(1700000000, 0).UTC(),
aggregatedSignature: aggSig,
signerBitmap: bitmap,
}
require.True(footer.IsAggregated())

ser, err := footer.Serialize()
require.NoError(err)

restored := &Footer{}
require.NoError(restored.Deserialize(ser))
require.True(restored.IsAggregated())
require.Equal(aggSig, restored.AggregatedSignature())
require.Equal(bitmap, restored.SignerBitmap())
require.Equal(0, len(restored.Endorsements()))
}

func makeFooter() (f *Footer) {
endors := make([]*endorsement.Endorsement, 0)
endor := endorsement.NewEndorsement(time.Now(), identityset.PrivateKey(27).PublicKey(), nil)
endors = append(endors, endor)
f = &Footer{endors, time.Now()}
f = &Footer{endorsements: endors, commitTime: time.Now()}
return
}
Loading