cosmos-sdk/server/v2/cometbft/utils.go
2024-07-31 13:51:48 +00:00

397 lines
11 KiB
Go

package cometbft
import (
"context"
"errors"
"fmt"
"math"
"strings"
"time"
abciv1 "buf.build/gen/go/cometbft/cometbft/protocolbuffers/go/cometbft/abci/v1"
abci "github.com/cometbft/cometbft/api/cometbft/abci/v1"
cmtproto "github.com/cometbft/cometbft/api/cometbft/types/v1"
gogoproto "github.com/cosmos/gogoproto/proto"
gogoany "github.com/cosmos/gogoproto/types/any"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/anypb"
v1beta1 "cosmossdk.io/api/cosmos/base/abci/v1beta1"
appmanager "cosmossdk.io/core/app"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/comet"
"cosmossdk.io/core/event"
"cosmossdk.io/core/transaction"
errorsmod "cosmossdk.io/errors"
consensus "cosmossdk.io/x/consensus/types"
)
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 *appmanager.BlockResponse,
cp *cmtproto.ConsensusParams,
appHash []byte,
indexSet map[string]struct{},
) (*abci.FinalizeBlockResponse, error) {
allEvents := append(in.BeginBlockEvents, in.EndBlockEvents...)
resp := &abci.FinalizeBlockResponse{
Events: intoABCIEvents(allEvents, indexSet),
TxResults: intoABCITxResults(in.TxResults, indexSet),
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 []appmanager.TxResult, indexSet map[string]struct{}) []*abci.ExecTxResult {
res := make([]*abci.ExecTxResult, len(results))
for i := range results {
if results[i].Error == nil {
res[i] = responseExecTxResultWithEvents(
results[i].Error,
results[i].GasWanted,
results[i].GasUsed,
intoABCIEvents(results[i].Events, indexSet),
false,
)
continue
}
// TODO: handle properly once the we decide on the type of TxResult.Resp
}
return res
}
func intoABCIEvents(events []event.Event, indexSet map[string]struct{}) []abci.Event {
indexAll := len(indexSet) == 0
abciEvents := make([]abci.Event, len(events))
for i, e := range events {
abciEvents[i] = abci.Event{
Type: e.Type,
Attributes: make([]abci.EventAttribute, len(e.Attributes)),
}
for j, attr := range e.Attributes {
_, index := indexSet[fmt.Sprintf("%s.%s", e.Type, attr.Key)]
abciEvents[i].Attributes[j] = abci.EventAttribute{
Key: attr.Key,
Value: attr.Value,
Index: index || indexAll,
}
}
}
return abciEvents
}
func intoABCISimulationResponse(txRes appmanager.TxResult, indexSet map[string]struct{}) ([]byte, error) {
indexAll := len(indexSet) == 0
abciEvents := make([]*abciv1.Event, len(txRes.Events))
for i, e := range txRes.Events {
abciEvents[i] = &abciv1.Event{
Type: e.Type,
Attributes: make([]*abciv1.EventAttribute, len(e.Attributes)),
}
for j, attr := range e.Attributes {
_, index := indexSet[fmt.Sprintf("%s.%s", e.Type, attr.Key)]
abciEvents[i].Attributes[j] = &abciv1.EventAttribute{
Key: attr.Key,
Value: attr.Value,
Index: index || indexAll,
}
}
}
msgResponses := make([]*anypb.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] = &anypb.Any{TypeUrl: anyMsg.TypeUrl, Value: anyMsg.Value}
}
res := &v1beta1.SimulationResponse{
GasInfo: &v1beta1.GasInfo{
GasWanted: txRes.GasWanted,
GasUsed: txRes.GasUsed,
},
Result: &v1beta1.Result{
Data: []byte{},
Log: txRes.Error.Error(),
Events: abciEvents,
MsgResponses: msgResponses,
},
}
return protojson.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 (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 (c *Consensus[T]) GetConsensusParams(ctx context.Context) (*cmtproto.ConsensusParams, error) {
latestVersion, err := c.store.GetLatestVersion()
if err != nil {
return nil, err
}
res, err := c.app.Query(ctx, latestVersion, &consensus.QueryParamsRequest{})
if err != nil {
return nil, err
}
if r, ok := res.(*consensus.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)
}
// queryResult returns a ResponseQuery from an error. It will try to parse ABCI
// info from the error.
func queryResult(err error) *abci.QueryResponse {
space, code, log := errorsmod.ABCIInfo(err, false)
return &abci.QueryResponse{
Codespace: space,
Code: code,
Log: log,
}
}