Skip to content

Commit 2fca9ea

Browse files
tac0turtlemergify[bot]
authored andcommitted
chore: change prepare and process proposal to be NoOps by default (#16407)
Co-authored-by: Sergio Mena <[email protected]> Co-authored-by: Aleksandr Bezobchuk <[email protected]> (cherry picked from commit efdc955) # Conflicts: # baseapp/abci_utils.go
1 parent 616841b commit 2fca9ea

File tree

2 files changed

+293
-1
lines changed

2 files changed

+293
-1
lines changed

baseapp/abci_utils.go

+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
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+
}

docs/docs/building-apps/02-app-mempool.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ all transactions, it can provide greater control over transaction ordering.
4343
Allowing the application to handle ordering enables the application to define how
4444
it would like the block constructed.
4545

46-
Currently, there is a default `PrepareProposal` implementation provided by the application.
46+
The Cosmos SDK defines the `DefaultProposalHandler` type, which provides applications with
47+
`PrepareProposal` and `ProcessProposal` handlers.
4748

4849
```go reference
4950
https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/baseapp/baseapp.go#L868-L916
@@ -116,6 +117,9 @@ A no-op mempool is a mempool where transactions are completely discarded and ign
116117
When this mempool is used, it assumed that an application will rely on CometBFT's transaction ordering defined in `RequestPrepareProposal`,
117118
which is FIFO-ordered by default.
118119

120+
> Note: If a NoOp mempool is used, PrepareProposal and ProcessProposal both should be aware of this as
121+
> PrepareProposal could include transactions that could fail verification in ProcessProposal.
122+
119123
### Sender Nonce Mempool
120124

121125
The nonce mempool is a mempool that keeps transactions from an sorted by nonce in order to avoid the issues with nonces.

0 commit comments

Comments
 (0)