|
| 1 | +package baseapp |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "fmt" |
| 6 | + |
| 7 | + "cosmossdk.io/math" |
| 8 | + "github.com/cockroachdb/errors" |
| 9 | + abci "github.com/cometbft/cometbft/abci/types" |
| 10 | + cmtcrypto "github.com/cometbft/cometbft/crypto" |
| 11 | + cryptoenc "github.com/cometbft/cometbft/crypto/encoding" |
| 12 | + cmtprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" |
| 13 | + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" |
| 14 | + protoio "github.com/cosmos/gogoproto/io" |
| 15 | + "github.com/cosmos/gogoproto/proto" |
| 16 | + |
| 17 | + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" |
| 18 | + sdk "github.com/cosmos/cosmos-sdk/types" |
| 19 | + "github.com/cosmos/cosmos-sdk/types/mempool" |
| 20 | +) |
| 21 | + |
| 22 | +// VoteExtensionThreshold defines the total voting power % that must be |
| 23 | +// submitted in order for all vote extensions to be considered valid for a |
| 24 | +// given height. |
| 25 | +var VoteExtensionThreshold = math.LegacyNewDecWithPrec(667, 3) |
| 26 | + |
| 27 | +type ( |
| 28 | + // Validator defines the interface contract require for verifying vote extension |
| 29 | + // signatures. Typically, this will be implemented by the x/staking module, |
| 30 | + // which has knowledge of the CometBFT public key. |
| 31 | + Validator interface { |
| 32 | + CmtConsPublicKey() (cmtprotocrypto.PublicKey, error) |
| 33 | + BondedTokens() math.Int |
| 34 | + } |
| 35 | + |
| 36 | + // ValidatorStore defines the interface contract require for verifying vote |
| 37 | + // extension signatures. Typically, this will be implemented by the x/staking |
| 38 | + // module, which has knowledge of the CometBFT public key. |
| 39 | + ValidatorStore interface { |
| 40 | + GetValidatorByConsAddr(sdk.Context, cryptotypes.Address) (Validator, error) |
| 41 | + TotalBondedTokens(ctx sdk.Context) math.Int |
| 42 | + } |
| 43 | +) |
| 44 | + |
| 45 | +// ValidateVoteExtensions defines a helper function for verifying vote extension |
| 46 | +// signatures that may be passed or manually injected into a block proposal from |
| 47 | +// a proposer in ProcessProposal. It returns an error if any signature is invalid |
| 48 | +// or if unexpected vote extensions and/or signatures are found or less than 2/3 |
| 49 | +// power is received. |
| 50 | +func ValidateVoteExtensions( |
| 51 | + ctx sdk.Context, |
| 52 | + valStore ValidatorStore, |
| 53 | + currentHeight int64, |
| 54 | + chainID string, |
| 55 | + extCommit abci.ExtendedCommitInfo, |
| 56 | +) error { |
| 57 | + cp := ctx.ConsensusParams() |
| 58 | + extsEnabled := cp.Abci != nil && cp.Abci.VoteExtensionsEnableHeight > 0 |
| 59 | + |
| 60 | + marshalDelimitedFn := func(msg proto.Message) ([]byte, error) { |
| 61 | + var buf bytes.Buffer |
| 62 | + if err := protoio.NewDelimitedWriter(&buf).WriteMsg(msg); err != nil { |
| 63 | + return nil, err |
| 64 | + } |
| 65 | + |
| 66 | + return buf.Bytes(), nil |
| 67 | + } |
| 68 | + |
| 69 | + var sumVP math.Int |
| 70 | + for _, vote := range extCommit.Votes { |
| 71 | + if !extsEnabled { |
| 72 | + if len(vote.VoteExtension) > 0 { |
| 73 | + return fmt.Errorf("vote extensions disabled; received non-empty vote extension at height %d", currentHeight) |
| 74 | + } |
| 75 | + if len(vote.ExtensionSignature) > 0 { |
| 76 | + return fmt.Errorf("vote extensions disabled; received non-empty vote extension signature at height %d", currentHeight) |
| 77 | + } |
| 78 | + |
| 79 | + continue |
| 80 | + } |
| 81 | + |
| 82 | + if len(vote.ExtensionSignature) == 0 { |
| 83 | + return fmt.Errorf("vote extensions enabled; received empty vote extension signature at height %d", currentHeight) |
| 84 | + } |
| 85 | + |
| 86 | + valConsAddr := cmtcrypto.Address(vote.Validator.Address) |
| 87 | + |
| 88 | + validator, err := valStore.GetValidatorByConsAddr(ctx, valConsAddr) |
| 89 | + if err != nil { |
| 90 | + return fmt.Errorf("failed to get validator %X: %w", valConsAddr, err) |
| 91 | + } |
| 92 | + if validator == nil { |
| 93 | + return fmt.Errorf("validator %X not found", valConsAddr) |
| 94 | + } |
| 95 | + |
| 96 | + cmtPubKeyProto, err := validator.CmtConsPublicKey() |
| 97 | + if err != nil { |
| 98 | + return fmt.Errorf("failed to get validator %X public key: %w", valConsAddr, err) |
| 99 | + } |
| 100 | + |
| 101 | + cmtPubKey, err := cryptoenc.PubKeyFromProto(cmtPubKeyProto) |
| 102 | + if err != nil { |
| 103 | + return fmt.Errorf("failed to convert validator %X public key: %w", valConsAddr, err) |
| 104 | + } |
| 105 | + |
| 106 | + cve := cmtproto.CanonicalVoteExtension{ |
| 107 | + Extension: vote.VoteExtension, |
| 108 | + Height: currentHeight - 1, // the vote extension was signed in the previous height |
| 109 | + Round: int64(extCommit.Round), |
| 110 | + ChainId: chainID, |
| 111 | + } |
| 112 | + |
| 113 | + extSignBytes, err := marshalDelimitedFn(&cve) |
| 114 | + if err != nil { |
| 115 | + return fmt.Errorf("failed to encode CanonicalVoteExtension: %w", err) |
| 116 | + } |
| 117 | + |
| 118 | + if !cmtPubKey.VerifySignature(extSignBytes, vote.ExtensionSignature) { |
| 119 | + return fmt.Errorf("failed to verify validator %X vote extension signature", valConsAddr) |
| 120 | + } |
| 121 | + |
| 122 | + sumVP = sumVP.Add(validator.BondedTokens()) |
| 123 | + } |
| 124 | + |
| 125 | + // Ensure we have at least 2/3 voting power that submitted valid vote |
| 126 | + // extensions. |
| 127 | + totalVP := valStore.TotalBondedTokens(ctx) |
| 128 | + percentSubmitted := math.LegacyNewDecFromInt(sumVP).Quo(math.LegacyNewDecFromInt(totalVP)) |
| 129 | + if percentSubmitted.LT(VoteExtensionThreshold) { |
| 130 | + return fmt.Errorf("insufficient cumulative voting power received to verify vote extensions; got: %s, expected: >=%s", percentSubmitted, VoteExtensionThreshold) |
| 131 | + } |
| 132 | + |
| 133 | + return nil |
| 134 | +} |
| 135 | + |
| 136 | +type ( |
| 137 | + // ProposalTxVerifier defines the interface that is implemented by BaseApp, |
| 138 | + // that any custom ABCI PrepareProposal and ProcessProposal handler can use |
| 139 | + // to verify a transaction. |
| 140 | + ProposalTxVerifier interface { |
| 141 | + PrepareProposalVerifyTx(tx sdk.Tx) ([]byte, error) |
| 142 | + ProcessProposalVerifyTx(txBz []byte) (sdk.Tx, error) |
| 143 | + } |
| 144 | + |
| 145 | + // DefaultProposalHandler defines the default ABCI PrepareProposal and |
| 146 | + // ProcessProposal handlers. |
| 147 | + DefaultProposalHandler struct { |
| 148 | + mempool mempool.Mempool |
| 149 | + txVerifier ProposalTxVerifier |
| 150 | + } |
| 151 | +) |
| 152 | + |
| 153 | +func NewDefaultProposalHandler(mp mempool.Mempool, txVerifier ProposalTxVerifier) DefaultProposalHandler { |
| 154 | + return DefaultProposalHandler{ |
| 155 | + mempool: mp, |
| 156 | + txVerifier: txVerifier, |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +// PrepareProposalHandler returns the default implementation for processing an |
| 161 | +// ABCI proposal. The application's mempool is enumerated and all valid |
| 162 | +// transactions are added to the proposal. Transactions are valid if they: |
| 163 | +// |
| 164 | +// 1) Successfully encode to bytes. |
| 165 | +// 2) Are valid (i.e. pass runTx, AnteHandler only). |
| 166 | +// |
| 167 | +// Enumeration is halted once RequestPrepareProposal.MaxBytes of transactions is |
| 168 | +// reached or the mempool is exhausted. |
| 169 | +// |
| 170 | +// Note: |
| 171 | +// |
| 172 | +// - Step (2) is identical to the validation step performed in |
| 173 | +// DefaultProcessProposal. It is very important that the same validation logic |
| 174 | +// is used in both steps, and applications must ensure that this is the case in |
| 175 | +// non-default handlers. |
| 176 | +// |
| 177 | +// - If no mempool is set or if the mempool is a no-op mempool, the transactions |
| 178 | +// requested from CometBFT will simply be returned, which, by default, are in |
| 179 | +// FIFO order. |
| 180 | +func (h DefaultProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler { |
| 181 | + return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { |
| 182 | + // If the mempool is nil or NoOp we simply return the transactions |
| 183 | + // requested from CometBFT, which, by default, should be in FIFO order. |
| 184 | + _, isNoOp := h.mempool.(mempool.NoOpMempool) |
| 185 | + if h.mempool == nil || isNoOp { |
| 186 | + return &abci.ResponsePrepareProposal{Txs: req.Txs}, nil |
| 187 | + } |
| 188 | + |
| 189 | + var ( |
| 190 | + selectedTxs [][]byte |
| 191 | + totalTxBytes int64 |
| 192 | + ) |
| 193 | + |
| 194 | + iterator := h.mempool.Select(ctx, req.Txs) |
| 195 | + |
| 196 | + for iterator != nil { |
| 197 | + memTx := iterator.Tx() |
| 198 | + |
| 199 | + // NOTE: Since transaction verification was already executed in CheckTx, |
| 200 | + // which calls mempool.Insert, in theory everything in the pool should be |
| 201 | + // valid. But some mempool implementations may insert invalid txs, so we |
| 202 | + // check again. |
| 203 | + bz, err := h.txVerifier.PrepareProposalVerifyTx(memTx) |
| 204 | + if err != nil { |
| 205 | + err := h.mempool.Remove(memTx) |
| 206 | + if err != nil && !errors.Is(err, mempool.ErrTxNotFound) { |
| 207 | + panic(err) |
| 208 | + } |
| 209 | + } else { |
| 210 | + txSize := int64(len(bz)) |
| 211 | + if totalTxBytes += txSize; totalTxBytes <= req.MaxTxBytes { |
| 212 | + selectedTxs = append(selectedTxs, bz) |
| 213 | + } else { |
| 214 | + // We've reached capacity per req.MaxTxBytes so we cannot select any |
| 215 | + // more transactions. |
| 216 | + break |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + iterator = iterator.Next() |
| 221 | + } |
| 222 | + |
| 223 | + return &abci.ResponsePrepareProposal{Txs: selectedTxs}, nil |
| 224 | + } |
| 225 | +} |
| 226 | + |
| 227 | +// ProcessProposalHandler returns the default implementation for processing an |
| 228 | +// ABCI proposal. Every transaction in the proposal must pass 2 conditions: |
| 229 | +// |
| 230 | +// 1. The transaction bytes must decode to a valid transaction. |
| 231 | +// 2. The transaction must be valid (i.e. pass runTx, AnteHandler only) |
| 232 | +// |
| 233 | +// If any transaction fails to pass either condition, the proposal is rejected. |
| 234 | +// Note that step (2) is identical to the validation step performed in |
| 235 | +// DefaultPrepareProposal. It is very important that the same validation logic |
| 236 | +// is used in both steps, and applications must ensure that this is the case in |
| 237 | +// non-default handlers. |
| 238 | +func (h DefaultProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler { |
| 239 | + // If the mempool is nil or NoOp we simply return ACCEPT, |
| 240 | + // because PrepareProposal may have included txs that could fail verification. |
| 241 | + _, isNoOp := h.mempool.(mempool.NoOpMempool) |
| 242 | + if h.mempool == nil || isNoOp { |
| 243 | + return NoOpProcessProposal() |
| 244 | + } |
| 245 | + |
| 246 | + return func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { |
| 247 | + for _, txBytes := range req.Txs { |
| 248 | + _, err := h.txVerifier.ProcessProposalVerifyTx(txBytes) |
| 249 | + if err != nil { |
| 250 | + return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 254 | + return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil |
| 255 | + } |
| 256 | +} |
| 257 | + |
| 258 | +// NoOpPrepareProposal defines a no-op PrepareProposal handler. It will always |
| 259 | +// return the transactions sent by the client's request. |
| 260 | +func NoOpPrepareProposal() sdk.PrepareProposalHandler { |
| 261 | + return func(_ sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { |
| 262 | + return &abci.ResponsePrepareProposal{Txs: req.Txs}, nil |
| 263 | + } |
| 264 | +} |
| 265 | + |
| 266 | +// NoOpProcessProposal defines a no-op ProcessProposal Handler. It will always |
| 267 | +// return ACCEPT. |
| 268 | +func NoOpProcessProposal() sdk.ProcessProposalHandler { |
| 269 | + return func(_ sdk.Context, _ *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { |
| 270 | + return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil |
| 271 | + } |
| 272 | +} |
| 273 | + |
| 274 | +// NoOpExtendVote defines a no-op ExtendVote handler. It will always return an |
| 275 | +// empty byte slice as the vote extension. |
| 276 | +func NoOpExtendVote() sdk.ExtendVoteHandler { |
| 277 | + return func(_ sdk.Context, _ *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) { |
| 278 | + return &abci.ResponseExtendVote{VoteExtension: []byte{}}, nil |
| 279 | + } |
| 280 | +} |
| 281 | + |
| 282 | +// NoOpVerifyVoteExtensionHandler defines a no-op VerifyVoteExtension handler. It |
| 283 | +// will always return an ACCEPT status with no error. |
| 284 | +func NoOpVerifyVoteExtensionHandler() sdk.VerifyVoteExtensionHandler { |
| 285 | + return func(_ sdk.Context, _ *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) { |
| 286 | + return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_ACCEPT}, nil |
| 287 | + } |
| 288 | +} |
0 commit comments