653 lines
19 KiB
Go
653 lines
19 KiB
Go
package cometbft
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
abci "github.com/cometbft/cometbft/api/cometbft/abci/v1"
|
|
cmtproto "github.com/cometbft/cometbft/api/cometbft/types/v1"
|
|
cryptoenc "github.com/cometbft/cometbft/crypto/encoding"
|
|
protoio "github.com/cosmos/gogoproto/io"
|
|
gogoproto "github.com/cosmos/gogoproto/proto"
|
|
gogoany "github.com/cosmos/gogoproto/types/any"
|
|
"google.golang.org/grpc/codes"
|
|
grpcstatus "google.golang.org/grpc/status"
|
|
|
|
appmodulev2 "cosmossdk.io/core/appmodule/v2"
|
|
"cosmossdk.io/core/comet"
|
|
"cosmossdk.io/core/event"
|
|
"cosmossdk.io/core/server"
|
|
"cosmossdk.io/core/transaction"
|
|
errorsmod "cosmossdk.io/errors" // we aren't using errors/v2 as it doesn't support grpc status codes
|
|
"cosmossdk.io/server/v2/cometbft/handlers"
|
|
"cosmossdk.io/x/consensus/types"
|
|
|
|
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
|
|
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
)
|
|
|
|
func queryResponse(res transaction.Msg, height int64) (*abci.QueryResponse, error) {
|
|
// this is a tied to protobuf due to client responses always being handled in protobuf
|
|
bz, err := gogoproto.Marshal(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &abci.QueryResponse{
|
|
Value: bz,
|
|
Height: height,
|
|
}, nil
|
|
}
|
|
|
|
// responseExecTxResultWithEvents returns an ABCI ExecTxResult object with fields
|
|
// filled in from the given error, gas values and events.
|
|
func responseExecTxResultWithEvents(err error, gw, gu uint64, events []abci.Event, debug bool) *abci.ExecTxResult {
|
|
space, code, log := errorsmod.ABCIInfo(err, debug)
|
|
return &abci.ExecTxResult{
|
|
Codespace: space,
|
|
Code: code,
|
|
Log: log,
|
|
GasWanted: int64(gw),
|
|
GasUsed: int64(gu),
|
|
Events: events,
|
|
}
|
|
}
|
|
|
|
// splitABCIQueryPath splits a string path using the delimiter '/'.
|
|
//
|
|
// e.g. "this/is/funny" becomes []string{"this", "is", "funny"}
|
|
func splitABCIQueryPath(requestPath string) (path []string) {
|
|
path = strings.Split(requestPath, "/")
|
|
|
|
// first element is empty string
|
|
if len(path) > 0 && path[0] == "" {
|
|
path = path[1:]
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
func finalizeBlockResponse(
|
|
in *server.BlockResponse,
|
|
cp *cmtproto.ConsensusParams,
|
|
appHash []byte,
|
|
indexSet map[string]struct{},
|
|
cfg *AppTomlConfig,
|
|
) (*abci.FinalizeBlockResponse, error) {
|
|
events := make([]abci.Event, 0)
|
|
|
|
if !cfg.DisableABCIEvents {
|
|
var err error
|
|
events, err = intoABCIEvents(
|
|
append(in.BeginBlockEvents, in.EndBlockEvents...),
|
|
indexSet,
|
|
cfg.DisableIndexABCIEvents,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
txResults, err := intoABCITxResults(in.TxResults, indexSet, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp := &abci.FinalizeBlockResponse{
|
|
Events: events,
|
|
TxResults: txResults,
|
|
ValidatorUpdates: intoABCIValidatorUpdates(in.ValidatorUpdates),
|
|
AppHash: appHash,
|
|
ConsensusParamUpdates: cp,
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func intoABCIValidatorUpdates(updates []appmodulev2.ValidatorUpdate) []abci.ValidatorUpdate {
|
|
valsetUpdates := make([]abci.ValidatorUpdate, len(updates))
|
|
|
|
for i, v := range updates {
|
|
valsetUpdates[i] = abci.ValidatorUpdate{
|
|
PubKeyBytes: v.PubKey,
|
|
PubKeyType: v.PubKeyType,
|
|
Power: v.Power,
|
|
}
|
|
}
|
|
|
|
return valsetUpdates
|
|
}
|
|
|
|
func intoABCITxResults(
|
|
results []server.TxResult,
|
|
indexSet map[string]struct{},
|
|
cfg *AppTomlConfig,
|
|
) ([]*abci.ExecTxResult, error) {
|
|
res := make([]*abci.ExecTxResult, len(results))
|
|
for i := range results {
|
|
var err error
|
|
events := make([]abci.Event, 0)
|
|
|
|
if !cfg.DisableABCIEvents {
|
|
events, err = intoABCIEvents(results[i].Events, indexSet, cfg.DisableIndexABCIEvents)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
res[i] = responseExecTxResultWithEvents(
|
|
results[i].Error,
|
|
results[i].GasWanted,
|
|
results[i].GasUsed,
|
|
events,
|
|
cfg.Trace,
|
|
)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func intoABCIEvents(events []event.Event, indexSet map[string]struct{}, indexNone bool) ([]abci.Event, error) {
|
|
indexAll := len(indexSet) == 0
|
|
abciEvents := make([]abci.Event, len(events))
|
|
for i, e := range events {
|
|
attrs, err := e.Attributes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
abciEvents[i] = abci.Event{
|
|
Type: e.Type,
|
|
Attributes: make([]abci.EventAttribute, len(attrs)),
|
|
}
|
|
|
|
for j, attr := range attrs {
|
|
_, index := indexSet[fmt.Sprintf("%s.%s", e.Type, attr.Key)]
|
|
abciEvents[i].Attributes[j] = abci.EventAttribute{
|
|
Key: attr.Key,
|
|
Value: attr.Value,
|
|
Index: !indexNone && (index || indexAll),
|
|
}
|
|
}
|
|
}
|
|
return abciEvents, nil
|
|
}
|
|
|
|
func intoABCISimulationResponse(txRes server.TxResult, indexSet map[string]struct{}, indexNone bool) ([]byte, error) {
|
|
abciEvents, err := intoABCIEvents(txRes.Events, indexSet, indexNone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msgResponses := make([]*gogoany.Any, len(txRes.Resp))
|
|
for i, resp := range txRes.Resp {
|
|
// use this hack to maintain the protov2 API here for now
|
|
anyMsg, err := gogoany.NewAnyWithCacheWithValue(resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
msgResponses[i] = anyMsg
|
|
}
|
|
|
|
errMsg := ""
|
|
if txRes.Error != nil {
|
|
errMsg = txRes.Error.Error()
|
|
}
|
|
|
|
res := &sdk.SimulationResponse{
|
|
GasInfo: sdk.GasInfo{
|
|
GasWanted: txRes.GasWanted,
|
|
GasUsed: txRes.GasUsed,
|
|
},
|
|
Result: &sdk.Result{
|
|
Data: []byte{},
|
|
Log: errMsg,
|
|
Events: abciEvents,
|
|
MsgResponses: msgResponses,
|
|
},
|
|
}
|
|
|
|
return gogoproto.Marshal(res)
|
|
}
|
|
|
|
// ToSDKEvidence takes comet evidence and returns sdk evidence
|
|
func ToSDKEvidence(ev []abci.Misbehavior) []*comet.Evidence {
|
|
evidence := make([]*comet.Evidence, len(ev))
|
|
for i, e := range ev {
|
|
evidence[i] = &comet.Evidence{
|
|
Type: comet.MisbehaviorType(e.Type),
|
|
Height: e.Height,
|
|
Time: e.Time,
|
|
TotalVotingPower: e.TotalVotingPower,
|
|
Validator: comet.Validator{
|
|
Address: e.Validator.Address,
|
|
Power: e.Validator.Power,
|
|
},
|
|
}
|
|
}
|
|
return evidence
|
|
}
|
|
|
|
// ToSDKCommitInfo takes comet commit info and returns sdk commit info
|
|
func ToSDKCommitInfo(commit abci.CommitInfo) *comet.CommitInfo {
|
|
ci := comet.CommitInfo{
|
|
Round: commit.Round,
|
|
}
|
|
|
|
for _, v := range commit.Votes {
|
|
ci.Votes = append(ci.Votes, comet.VoteInfo{
|
|
Validator: comet.Validator{
|
|
Address: v.Validator.Address,
|
|
Power: v.Validator.Power,
|
|
},
|
|
BlockIDFlag: comet.BlockIDFlag(v.BlockIdFlag),
|
|
})
|
|
}
|
|
return &ci
|
|
}
|
|
|
|
// ToSDKExtendedCommitInfo takes comet extended commit info and returns sdk commit info
|
|
func ToSDKExtendedCommitInfo(commit abci.ExtendedCommitInfo) comet.CommitInfo {
|
|
ci := comet.CommitInfo{
|
|
Round: commit.Round,
|
|
}
|
|
|
|
for _, v := range commit.Votes {
|
|
ci.Votes = append(ci.Votes, comet.VoteInfo{
|
|
Validator: comet.Validator{
|
|
Address: v.Validator.Address,
|
|
Power: v.Validator.Power,
|
|
},
|
|
BlockIDFlag: comet.BlockIDFlag(v.BlockIdFlag),
|
|
})
|
|
}
|
|
|
|
return ci
|
|
}
|
|
|
|
// queryResult returns a ResponseQuery from an error. It will try to parse ABCI info from the error.
|
|
func queryResult(err error, debug bool) *abci.QueryResponse {
|
|
space, code, log := errorsmod.ABCIInfo(err, debug)
|
|
return &abci.QueryResponse{
|
|
Codespace: space,
|
|
Code: code,
|
|
Log: log,
|
|
}
|
|
}
|
|
|
|
func gRPCErrorToSDKError(err error) *abci.QueryResponse {
|
|
toQueryResp := func(sdkErr *errorsmod.Error, err error) *abci.QueryResponse {
|
|
res := &abci.QueryResponse{
|
|
Code: sdkErr.ABCICode(),
|
|
Codespace: sdkErr.Codespace(),
|
|
}
|
|
type grpcStatus interface{ GRPCStatus() *grpcstatus.Status }
|
|
if grpcErr, ok := err.(grpcStatus); ok {
|
|
res.Log = grpcErr.GRPCStatus().Message()
|
|
} else {
|
|
res.Log = err.Error()
|
|
}
|
|
return res
|
|
}
|
|
|
|
status, ok := grpcstatus.FromError(err)
|
|
if !ok {
|
|
return toQueryResp(sdkerrors.ErrInvalidRequest, err)
|
|
}
|
|
|
|
switch status.Code() {
|
|
case codes.NotFound:
|
|
return toQueryResp(sdkerrors.ErrKeyNotFound, err)
|
|
case codes.InvalidArgument:
|
|
return toQueryResp(sdkerrors.ErrInvalidRequest, err)
|
|
case codes.FailedPrecondition:
|
|
return toQueryResp(sdkerrors.ErrInvalidRequest, err)
|
|
case codes.Unauthenticated:
|
|
return toQueryResp(sdkerrors.ErrUnauthorized, err)
|
|
default:
|
|
return toQueryResp(sdkerrors.ErrUnknownRequest, err)
|
|
}
|
|
}
|
|
|
|
func (c *consensus[T]) validateFinalizeBlockHeight(req *abci.FinalizeBlockRequest) error {
|
|
if req.Height < 1 {
|
|
return fmt.Errorf("invalid height: %d", req.Height)
|
|
}
|
|
|
|
lastBlockHeight, _, err := c.store.StateLatest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// expectedHeight holds the expected height to validate
|
|
var expectedHeight uint64
|
|
if lastBlockHeight == 0 && c.initialHeight > 1 {
|
|
// In this case, we're validating the first block of the chain, i.e no
|
|
// previous commit. The height we're expecting is the initial height.
|
|
expectedHeight = c.initialHeight
|
|
} else {
|
|
// This case can mean two things:
|
|
//
|
|
// - Either there was already a previous commit in the store, in which
|
|
// case we increment the version from there.
|
|
// - Or there was no previous commit, in which case we start at version 1.
|
|
expectedHeight = lastBlockHeight + 1
|
|
}
|
|
|
|
if req.Height != int64(expectedHeight) {
|
|
return fmt.Errorf("invalid height: %d; expected: %d", req.Height, expectedHeight)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetConsensusParams makes a query to the consensus module in order to get the latest consensus
|
|
// parameters from committed state
|
|
func GetConsensusParams[T transaction.Tx](ctx context.Context, app handlers.AppManager[T]) (*cmtproto.ConsensusParams, error) {
|
|
res, err := app.Query(ctx, 0, &types.QueryParamsRequest{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if r, ok := res.(*types.QueryParamsResponse); !ok {
|
|
return nil, errors.New("failed to query consensus params")
|
|
} else {
|
|
// convert our params to cometbft params
|
|
return r.Params, nil
|
|
}
|
|
}
|
|
|
|
func (c *consensus[T]) GetBlockRetentionHeight(cp *cmtproto.ConsensusParams, commitHeight int64) int64 {
|
|
// pruning is disabled if minRetainBlocks is zero
|
|
if c.cfg.AppTomlConfig.MinRetainBlocks == 0 {
|
|
return 0
|
|
}
|
|
|
|
minNonZero := func(x, y int64) int64 {
|
|
switch {
|
|
case x == 0:
|
|
return y
|
|
|
|
case y == 0:
|
|
return x
|
|
|
|
case x < y:
|
|
return x
|
|
|
|
default:
|
|
return y
|
|
}
|
|
}
|
|
|
|
// Define retentionHeight as the minimum value that satisfies all non-zero
|
|
// constraints. All blocks below (commitHeight-retentionHeight) are pruned
|
|
// from CometBFT.
|
|
var retentionHeight int64
|
|
|
|
// Define the number of blocks needed to protect against misbehaving validators
|
|
// which allows light clients to operate safely. Note, we piggy back of the
|
|
// evidence parameters instead of computing an estimated number of blocks based
|
|
// on the unbonding period and block commitment time as the two should be
|
|
// equivalent.
|
|
if cp.Evidence != nil && cp.Evidence.MaxAgeNumBlocks > 0 {
|
|
retentionHeight = commitHeight - cp.Evidence.MaxAgeNumBlocks
|
|
}
|
|
|
|
if c.snapshotManager != nil {
|
|
snapshotRetentionHeights := c.snapshotManager.GetSnapshotBlockRetentionHeights()
|
|
if snapshotRetentionHeights > 0 {
|
|
retentionHeight = minNonZero(retentionHeight, commitHeight-snapshotRetentionHeights)
|
|
}
|
|
}
|
|
|
|
v := commitHeight - int64(c.cfg.AppTomlConfig.MinRetainBlocks)
|
|
retentionHeight = minNonZero(retentionHeight, v)
|
|
|
|
if retentionHeight <= 0 {
|
|
// prune nothing in the case of a non-positive height
|
|
return 0
|
|
}
|
|
|
|
return retentionHeight
|
|
}
|
|
|
|
// checkHalt checks if height or time exceeds halt-height or halt-time respectively.
|
|
func (c *consensus[T]) checkHalt(height int64, time time.Time) error {
|
|
var halt bool
|
|
switch {
|
|
case c.cfg.AppTomlConfig.HaltHeight > 0 && uint64(height) >= c.cfg.AppTomlConfig.HaltHeight:
|
|
halt = true
|
|
|
|
case c.cfg.AppTomlConfig.HaltTime > 0 && time.Unix() >= int64(c.cfg.AppTomlConfig.HaltTime):
|
|
halt = true
|
|
}
|
|
|
|
if halt {
|
|
return fmt.Errorf("halt per configuration height %d time %d", c.cfg.AppTomlConfig.HaltHeight, c.cfg.AppTomlConfig.HaltTime)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// uint64ToInt64 converts a uint64 to an int64, returning math.MaxInt64 if the uint64 is too large.
|
|
func uint64ToInt64(u uint64) int64 {
|
|
if u > uint64(math.MaxInt64) {
|
|
return math.MaxInt64
|
|
}
|
|
return int64(u)
|
|
}
|
|
|
|
// RawTx allows access to the raw bytes of a transaction even if it failed
|
|
// to decode.
|
|
func RawTx(tx []byte) transaction.Tx {
|
|
return InjectedTx(tx)
|
|
}
|
|
|
|
type InjectedTx []byte
|
|
|
|
var _ transaction.Tx = InjectedTx{}
|
|
|
|
func (tx InjectedTx) Bytes() []byte {
|
|
return tx
|
|
}
|
|
|
|
func (tx InjectedTx) Hash() [32]byte {
|
|
return sha256.Sum256(tx)
|
|
}
|
|
|
|
func (tx InjectedTx) GetGasLimit() (uint64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (tx InjectedTx) GetMessages() ([]transaction.Msg, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (tx InjectedTx) GetSenders() ([]transaction.Identity, error) {
|
|
return [][]byte{[]byte("cometbft")}, nil
|
|
}
|
|
|
|
// ValidateVoteExtensions defines a helper function for verifying vote extension
|
|
// signatures that may be passed or manually injected into a block proposal from
|
|
// a proposer in PrepareProposal. It returns an error if any signature is invalid
|
|
// or if unexpected vote extensions and/or signatures are found or less than 2/3
|
|
// power is received.
|
|
// If commitInfo is nil, this function can be used to check a set of vote extensions
|
|
// without comparing them to a commit.
|
|
func ValidateVoteExtensions[T transaction.Tx](
|
|
ctx context.Context,
|
|
app handlers.AppManager[T],
|
|
chainID string,
|
|
validatorStore func(context.Context, []byte) (cryptotypes.PubKey, error),
|
|
extCommit abci.ExtendedCommitInfo,
|
|
currentHeight int64,
|
|
commitInfo *abci.CommitInfo,
|
|
) error {
|
|
cp, err := GetConsensusParams(ctx, app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if commitInfo != nil {
|
|
// Check that both extCommit + commit are ordered in accordance with vp/address.
|
|
if err := validateExtendedCommitAgainstLastCommit(extCommit, *commitInfo); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Start checking vote extensions only **after** the vote extensions enable
|
|
// height, because when `currentHeight == VoteExtensionsEnableHeight`
|
|
// PrepareProposal doesn't get any vote extensions in its request.
|
|
extsEnabled := cp.Feature != nil && cp.Feature.VoteExtensionsEnableHeight != nil && currentHeight > cp.Feature.VoteExtensionsEnableHeight.Value && cp.Feature.VoteExtensionsEnableHeight.Value != 0
|
|
if !extsEnabled {
|
|
extsEnabled = cp.Abci != nil && currentHeight > cp.Abci.VoteExtensionsEnableHeight && cp.Abci.VoteExtensionsEnableHeight != 0
|
|
}
|
|
if !extsEnabled {
|
|
return nil
|
|
}
|
|
|
|
marshalDelimitedFn := func(msg gogoproto.Message) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
if err := protoio.NewDelimitedWriter(&buf).WriteMsg(msg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
var (
|
|
// Total voting power of all vote extensions.
|
|
totalVP int64
|
|
// Total voting power of all validators that submitted valid vote extensions.
|
|
sumVP int64
|
|
)
|
|
|
|
for _, vote := range extCommit.Votes {
|
|
totalVP += vote.Validator.Power
|
|
|
|
// Only check + include power if the vote is a commit vote. There must be super-majority, otherwise the
|
|
// previous block (the block the vote is for) could not have been committed.
|
|
if vote.BlockIdFlag != cmtproto.BlockIDFlagCommit {
|
|
continue
|
|
}
|
|
|
|
if !extsEnabled {
|
|
if len(vote.VoteExtension) > 0 {
|
|
return fmt.Errorf("vote extensions disabled; received non-empty vote extension at height %d", currentHeight)
|
|
}
|
|
if len(vote.ExtensionSignature) > 0 {
|
|
return fmt.Errorf("vote extensions disabled; received non-empty vote extension signature at height %d", currentHeight)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
if len(vote.ExtensionSignature) == 0 {
|
|
return fmt.Errorf("vote extensions enabled; received empty vote extension signature at height %d", currentHeight)
|
|
}
|
|
|
|
valConsAddr := sdk.ConsAddress(vote.Validator.Address)
|
|
|
|
pubKeyProto, err := validatorStore(ctx, valConsAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get validator %X public key: %w", valConsAddr, err)
|
|
}
|
|
|
|
cmtpk, err := cryptocodec.ToCmtProtoPublicKey(pubKeyProto)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert validator %X public key: %w", valConsAddr, err)
|
|
}
|
|
|
|
cmtPubKey, err := cryptoenc.PubKeyFromProto(cmtpk)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to convert validator %X public key: %w", valConsAddr, err)
|
|
}
|
|
|
|
cve := cmtproto.CanonicalVoteExtension{
|
|
Extension: vote.VoteExtension,
|
|
Height: currentHeight - 1, // the vote extension was signed in the previous height
|
|
Round: int64(extCommit.Round),
|
|
ChainId: chainID,
|
|
}
|
|
|
|
extSignBytes, err := marshalDelimitedFn(&cve)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to encode CanonicalVoteExtension: %w", err)
|
|
}
|
|
|
|
if !cmtPubKey.VerifySignature(extSignBytes, vote.ExtensionSignature) {
|
|
return fmt.Errorf("failed to verify validator %X vote extension signature", valConsAddr)
|
|
}
|
|
|
|
sumVP += vote.Validator.Power
|
|
}
|
|
|
|
// This check is probably unnecessary, but better safe than sorry.
|
|
if totalVP <= 0 {
|
|
return fmt.Errorf("total voting power must be positive, got: %d", totalVP)
|
|
}
|
|
|
|
// If the sum of the voting power has not reached (2/3 + 1) we need to error.
|
|
if requiredVP := ((totalVP * 2) / 3) + 1; sumVP < requiredVP {
|
|
return fmt.Errorf(
|
|
"insufficient cumulative voting power received to verify vote extensions; got: %d, expected: >=%d",
|
|
sumVP, requiredVP,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateExtendedCommitAgainstLastCommit validates an ExtendedCommitInfo against a LastCommit. Specifically,
|
|
// it checks that the ExtendedCommit + LastCommit (for the same height), are consistent with each other + that
|
|
// they are ordered correctly (by voting power) in accordance with
|
|
// [comet](https://github.com/cometbft/cometbft/blob/4ce0277b35f31985bbf2c25d3806a184a4510010/types/validator_set.go#L784).
|
|
func validateExtendedCommitAgainstLastCommit(ec abci.ExtendedCommitInfo, lc abci.CommitInfo) error {
|
|
// check that the rounds are the same
|
|
if ec.Round != lc.Round {
|
|
return fmt.Errorf("extended commit round %d does not match last commit round %d", ec.Round, lc.Round)
|
|
}
|
|
|
|
// check that the # of votes are the same
|
|
if len(ec.Votes) != len(lc.Votes) {
|
|
return fmt.Errorf("extended commit votes length %d does not match last commit votes length %d", len(ec.Votes), len(lc.Votes))
|
|
}
|
|
|
|
// check sort order of extended commit votes
|
|
if !slices.IsSortedFunc(ec.Votes, func(vote1, vote2 abci.ExtendedVoteInfo) int {
|
|
if vote1.Validator.Power == vote2.Validator.Power {
|
|
return bytes.Compare(vote1.Validator.Address, vote2.Validator.Address) // addresses sorted in ascending order (used to break vp conflicts)
|
|
}
|
|
return -int(vote1.Validator.Power - vote2.Validator.Power) // vp sorted in descending order
|
|
}) {
|
|
return errors.New("extended commit votes are not sorted by voting power")
|
|
}
|
|
|
|
addressCache := make(map[string]struct{}, len(ec.Votes))
|
|
// check consistency between LastCommit and ExtendedCommit
|
|
for i, vote := range ec.Votes {
|
|
// cache addresses to check for duplicates
|
|
if _, ok := addressCache[string(vote.Validator.Address)]; ok {
|
|
return fmt.Errorf("extended commit vote address %X is duplicated", vote.Validator.Address)
|
|
}
|
|
addressCache[string(vote.Validator.Address)] = struct{}{}
|
|
|
|
if !bytes.Equal(vote.Validator.Address, lc.Votes[i].Validator.Address) {
|
|
return fmt.Errorf("extended commit vote address %X does not match last commit vote address %X", vote.Validator.Address, lc.Votes[i].Validator.Address)
|
|
}
|
|
if vote.Validator.Power != lc.Votes[i].Validator.Power {
|
|
return fmt.Errorf("extended commit vote power %d does not match last commit vote power %d", vote.Validator.Power, lc.Votes[i].Validator.Power)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|