ceremonyclient/node/consensus/app/consensus_voting_provider.go
2025-12-15 16:45:31 -06:00

342 lines
9.6 KiB
Go

package app
import (
"context"
"slices"
"sort"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
"source.quilibrium.com/quilibrium/monorepo/consensus"
"source.quilibrium.com/quilibrium/monorepo/consensus/models"
"source.quilibrium.com/quilibrium/monorepo/consensus/verification"
"source.quilibrium.com/quilibrium/monorepo/protobufs"
"source.quilibrium.com/quilibrium/monorepo/types/tries"
up2p "source.quilibrium.com/quilibrium/monorepo/utils/p2p"
)
// AppVotingProvider implements VotingProvider
type AppVotingProvider struct {
engine *AppConsensusEngine
}
// FinalizeQuorumCertificate implements consensus.VotingProvider.
func (p *AppVotingProvider) FinalizeQuorumCertificate(
ctx context.Context,
state *models.State[*protobufs.AppShardFrame],
aggregatedSignature models.AggregatedSignature,
) (models.QuorumCertificate, error) {
cloned := (*state.State).Clone().(*protobufs.AppShardFrame)
cloned.Header.PublicKeySignatureBls48581 =
&protobufs.BLS48581AggregateSignature{
Signature: aggregatedSignature.GetSignature(),
PublicKey: &protobufs.BLS48581G2PublicKey{
KeyValue: aggregatedSignature.GetPubKey(),
},
Bitmask: aggregatedSignature.GetBitmask(),
}
frameBytes, err := cloned.ToCanonicalBytes()
if err != nil {
return nil, errors.Wrap(err, "finalize quorum certificate")
}
p.engine.pubsub.PublishToBitmask(
p.engine.getFrameMessageBitmask(),
frameBytes,
)
return &protobufs.QuorumCertificate{
Filter: (*state.State).Header.Address,
Rank: (*state.State).GetRank(),
FrameNumber: (*state.State).Header.FrameNumber,
Selector: []byte((*state.State).Identity()),
Timestamp: uint64(time.Now().UnixMilli()),
AggregateSignature: &protobufs.BLS48581AggregateSignature{
Signature: aggregatedSignature.GetSignature(),
PublicKey: &protobufs.BLS48581G2PublicKey{
KeyValue: aggregatedSignature.GetPubKey(),
},
Bitmask: aggregatedSignature.GetBitmask(),
},
}, nil
}
// FinalizeTimeout implements consensus.VotingProvider.
func (p *AppVotingProvider) FinalizeTimeout(
ctx context.Context,
rank uint64,
latestQuorumCertificate models.QuorumCertificate,
latestQuorumCertificateRanks []consensus.TimeoutSignerInfo,
aggregatedSignature models.AggregatedSignature,
) (models.TimeoutCertificate, error) {
ranksInProverOrder := slices.Clone(latestQuorumCertificateRanks)
provers, err := p.engine.proverRegistry.GetActiveProvers(p.engine.appAddress)
if err != nil {
return nil, err
}
proverIndexes := map[models.Identity]int{}
for i, p := range provers {
proverIndexes[models.Identity(p.Address)] = i
}
sort.Slice(ranksInProverOrder, func(i, j int) bool {
return proverIndexes[ranksInProverOrder[i].Signer]-
proverIndexes[ranksInProverOrder[j].Signer] < 0
})
ranks := []uint64{}
for _, r := range ranksInProverOrder {
ranks = append(ranks, r.NewestQCRank)
}
return &protobufs.TimeoutCertificate{
Filter: p.engine.appAddress,
Rank: rank,
LatestRanks: ranks,
LatestQuorumCertificate: latestQuorumCertificate.(*protobufs.QuorumCertificate),
Timestamp: uint64(time.Now().UnixMilli()),
AggregateSignature: &protobufs.BLS48581AggregateSignature{
Signature: aggregatedSignature.GetSignature(),
PublicKey: &protobufs.BLS48581G2PublicKey{
KeyValue: aggregatedSignature.GetPubKey(),
},
Bitmask: aggregatedSignature.GetBitmask(),
},
}, nil
}
// SignTimeoutVote implements consensus.VotingProvider.
func (p *AppVotingProvider) SignTimeoutVote(
ctx context.Context,
filter []byte,
currentRank uint64,
newestQuorumCertificateRank uint64,
) (**protobufs.ProposalVote, error) {
// Get signing key
signer, _, publicKey, _ := p.engine.GetProvingKey(p.engine.config.Engine)
if publicKey == nil {
p.engine.logger.Error("no proving key available")
return nil, errors.Wrap(
errors.New("no proving key available for voting"),
"sign vote",
)
}
// Create vote (signature)
signatureData := verification.MakeTimeoutMessage(
filter,
currentRank,
newestQuorumCertificateRank,
)
sig, err := signer.SignWithDomain(
signatureData,
slices.Concat([]byte("appshardtimeout"), p.engine.appAddress),
)
if err != nil {
p.engine.logger.Error("could not sign vote", zap.Error(err))
return nil, errors.Wrap(err, "sign vote")
}
voterAddress := p.engine.getAddressFromPublicKey(publicKey)
// Create vote message
vote := &protobufs.ProposalVote{
Filter: filter, // buildutils:allow-slice-alias slice is static
FrameNumber: 0,
Rank: currentRank,
Selector: nil,
Timestamp: uint64(time.Now().UnixMilli()),
PublicKeySignatureBls48581: &protobufs.BLS48581AddressedSignature{
Address: voterAddress,
Signature: sig,
},
}
return &vote, nil
}
// SignVote implements consensus.VotingProvider.
func (p *AppVotingProvider) SignVote(
ctx context.Context,
state *models.State[*protobufs.AppShardFrame],
) (**protobufs.ProposalVote, error) {
// Get signing key
signer, _, publicKey, _ := p.engine.GetProvingKey(p.engine.config.Engine)
if publicKey == nil {
p.engine.logger.Error("no proving key available")
return nil, errors.Wrap(
errors.New("no proving key available for voting"),
"sign vote",
)
}
nextLeader, err := p.engine.LeaderForRank(state.Rank)
if err != nil {
p.engine.logger.Error("could not determine next prover", zap.Error(err))
return nil, errors.Wrap(
errors.New("could not determine next prover"),
"sign vote",
)
}
var extProof []byte
if nextLeader != p.engine.Self() {
p.engine.proofCacheMu.RLock()
proof, ok := p.engine.proofCache[state.Rank]
p.engine.proofCacheMu.RUnlock()
if !ok {
return nil, errors.Wrap(
errors.New("no proof ready for vote"),
"sign vote",
)
}
extProof = proof[:]
}
// Create vote (signature)
signatureData := verification.MakeVoteMessage(
(*state.State).Header.Address,
state.Rank,
state.Identifier,
)
sig, err := signer.SignWithDomain(
signatureData,
slices.Concat([]byte("appshard"), p.engine.appAddress),
)
if err != nil {
p.engine.logger.Error("could not sign vote", zap.Error(err))
return nil, errors.Wrap(err, "sign vote")
}
voterAddress := p.engine.getAddressFromPublicKey(publicKey)
// Create vote message
vote := &protobufs.ProposalVote{
Filter: (*state.State).Header.Address,
FrameNumber: (*state.State).Header.FrameNumber,
Rank: (*state.State).Header.Rank,
Selector: []byte((*state.State).Identity()),
Timestamp: uint64(time.Now().UnixMilli()),
PublicKeySignatureBls48581: &protobufs.BLS48581AddressedSignature{
Address: voterAddress,
Signature: slices.Concat(sig, extProof),
},
}
return &vote, nil
}
// GetFullPath converts a key to its path representation using 6-bit nibbles
func GetFullPath(key []byte) []int32 {
var nibbles []int32
depth := 0
for {
n1 := getNextNibble(key, depth)
if n1 == -1 {
break
}
nibbles = append(nibbles, n1)
depth += tries.BranchBits
}
return nibbles
}
// getNextNibble returns the next BranchBits bits from the key starting at pos
func getNextNibble(key []byte, pos int) int32 {
startByte := pos / 8
if startByte >= len(key) {
return -1
}
// Calculate how many bits we need from the current byte
startBit := pos % 8
bitsFromCurrentByte := 8 - startBit
result := int(key[startByte] & ((1 << bitsFromCurrentByte) - 1))
if bitsFromCurrentByte >= tries.BranchBits {
// We have enough bits in the current byte
return int32((result >> (bitsFromCurrentByte - tries.BranchBits)) &
tries.BranchMask)
}
// We need bits from the next byte
result = result << (tries.BranchBits - bitsFromCurrentByte)
if startByte+1 < len(key) {
remainingBits := tries.BranchBits - bitsFromCurrentByte
nextByte := int(key[startByte+1])
result |= (nextByte >> (8 - remainingBits))
}
return int32(result & tries.BranchMask)
}
// extractShardAddresses extracts all possible shard addresses from a transaction address
func (p *AppVotingProvider) extractShardAddresses(txAddress []byte) [][]byte {
var shardAddresses [][]byte
// Get the full path from the transaction address
path := GetFullPath(txAddress)
// The first 43 nibbles (258 bits) represent the base shard address
// We need to extract all possible shard addresses by considering path segments after the 43rd nibble
if len(path) <= 43 {
// If the path is too short, just return the original address truncated to 32 bytes
if len(txAddress) >= 32 {
shardAddresses = append(shardAddresses, txAddress[:32])
}
return shardAddresses
}
// Convert the first 43 nibbles to bytes (base shard address)
baseShardAddr := txAddress[:32]
l1 := up2p.GetBloomFilterIndices(baseShardAddr, 256, 3)
candidates := map[string]struct{}{}
// Now generate all possible shard addresses by extending the path
// Each additional nibble after the 43rd creates a new shard address
for i := 43; i < len(path); i++ {
// Create a new shard address by extending the base with this path segment
extendedAddr := make([]byte, 32)
copy(extendedAddr, baseShardAddr)
// Add the path segment as a byte
extendedAddr = append(extendedAddr, byte(path[i]))
candidates[string(extendedAddr)] = struct{}{}
}
shards, err := p.engine.shardsStore.GetAppShards(
slices.Concat(l1, baseShardAddr),
[]uint32{},
)
if err != nil {
return [][]byte{}
}
for _, shard := range shards {
if _, ok := candidates[string(
slices.Concat(shard.L2, uint32ToBytes(shard.Path)),
)]; ok {
shardAddresses = append(shardAddresses, shard.L2)
}
}
return shardAddresses
}
func uint32ToBytes(path []uint32) []byte {
bytes := []byte{}
for _, p := range path {
bytes = append(bytes, byte(p))
}
return bytes
}
var _ consensus.VotingProvider[*protobufs.AppShardFrame, *protobufs.ProposalVote, PeerID] = (*AppVotingProvider)(nil)