cosmos-sdk/server/v2/cometbft/utils.go
2024-12-19 18:51:59 +00:00

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
}