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
2 changes: 2 additions & 0 deletions action/protocol/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,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 +347,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
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
2 changes: 1 addition & 1 deletion api/grpcserver_integrity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2133,7 +2133,7 @@ func TestGrpcServer_GetEpochMetaIntegrity(t *testing.T) {
).
SetHeight(height).
SetTimestamp(time.Time{}).
SignAndBuild(pk)
SignAndBuild(block.NewECDSAHeaderSigner(pk))
if err != nil {
return &block.Header{}, err
}
Expand Down
2 changes: 1 addition & 1 deletion api/web3server_marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestBlockObjectMarshal(t *testing.T) {
SetPrevBlockHash(hash.BytesToHash256(previousBlockHash)).
SetBaseFee(big.NewInt(10)).
SetBlobGasUsed(20).SetExcessBlobGas(100).
SignAndBuild(identityset.PrivateKey(28))
SignAndBuild(block.NewECDSAHeaderSigner(identityset.PrivateKey(28)))
require.NoError(err)
txHash, err := sevlp.Hash()
require.NoError(err)
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
2 changes: 1 addition & 1 deletion blockchain/block/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func makeBlock(tb testing.TB, n int) *Block {
SetReceiptRoot(hash.Hash256b([]byte("hello, world!"))).
SetDeltaStateDigest(hash.Hash256b([]byte("world, hello!"))).
SetPrevBlockHash(hash.Hash256b([]byte("hello, block!"))).
SignAndBuild(identityset.PrivateKey(0))
SignAndBuild(NewECDSAHeaderSigner(identityset.PrivateKey(0)))
require.NoError(tb, err)
return &blk
}
Expand Down
17 changes: 11 additions & 6 deletions blockchain/block/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"time"

"github.com/iotexproject/go-pkgs/bloom"
"github.com/iotexproject/go-pkgs/crypto"
"github.com/iotexproject/go-pkgs/hash"
"github.com/pkg/errors"

Expand Down Expand Up @@ -108,13 +107,19 @@ func (b *Builder) SetExcessBlobGas(g uint64) *Builder {
return b
}

// SignAndBuild signs and then builds a block.
func (b *Builder) SignAndBuild(signerPrvKey crypto.PrivateKey) (Block, error) {
b.blk.Header.pubkey = signerPrvKey.PublicKey()
// SignAndBuild signs and then builds a block. The signer carries both
// the public key (stored on the header for later VerifySignature) and
// the signing primitive itself; callers pick ECDSA or BLS by passing
// the matching adapter (NewECDSAHeaderSigner / NewBLSHeaderSigner).
func (b *Builder) SignAndBuild(signer HeaderSigner) (Block, error) {
if signer == nil {
return Block{}, errors.New("nil header signer")
}
b.blk.Header.pubkey = signer.PubKey()
h := b.blk.Header.HashHeaderCore()
sig, err := signerPrvKey.Sign(h[:])
sig, err := signer.Sign(h[:])
if err != nil {
return Block{}, errors.New("failed to sign block")
return Block{}, errors.Wrap(err, "failed to sign block")
}
b.blk.Header.blockSig = sig
return b.blk, nil
Expand Down
2 changes: 1 addition & 1 deletion blockchain/block/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestBuilder(t *testing.T) {
SetHeight(1).
SetTimestamp(testutil.TimestampNow()).
SetPrevBlockHash(hash.ZeroHash256).
SignAndBuild(identityset.PrivateKey(29))
SignAndBuild(NewECDSAHeaderSigner(identityset.PrivateKey(29)))
require.NoError(t, err)

require.True(t, nblk.VerifySignature())
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