282 lines
10 KiB
Markdown
282 lines
10 KiB
Markdown
# Vote Extensions
|
|
|
|
:::note Synopsis
|
|
This section describes how the application can define and use vote extensions
|
|
defined in ABCI++.
|
|
:::
|
|
|
|
## Extend Vote
|
|
|
|
ABCI2.0 (colloquially called ABCI++) allows an application to extend a pre-commit vote with arbitrary data. This process does NOT have to be deterministic, and the data returned can be unique to the
|
|
validator process. The Cosmos SDK defines [`baseapp.ExtendVoteHandler`](https://github.com/cosmos/cosmos-sdk/blob/v0.52.0-beta.1/types/abci.go#L26-L27):
|
|
|
|
```go
|
|
type ExtendVoteHandler func(Context, *abci.ExtendVoteRequest) (*abci.ExtendVoteResponse, error)
|
|
```
|
|
|
|
An application can set this handler in `app.go` via the `baseapp.SetExtendVoteHandler`
|
|
`BaseApp` option function. The `sdk.ExtendVoteHandler`, if defined, is called during
|
|
the `ExtendVote` ABCI method. Note, if an application decides to implement
|
|
`baseapp.ExtendVoteHandler`, it MUST return a non-nil `VoteExtension`. However, the vote
|
|
extension can be empty. See [here](https://docs.cometbft.com/v1.0/spec/abci/abci++_methods#extendvote)
|
|
for more details.
|
|
|
|
There are many decentralized censorship-resistant use cases for vote extensions.
|
|
For example, a validator may want to submit prices for a price oracle or encryption
|
|
shares for an encrypted transaction mempool. Note, an application should be careful
|
|
to consider the size of the vote extensions as they could increase latency in block
|
|
production. See [here](https://docs.cometbft.com/v1.0/references/qa/cometbft-qa-38#vote-extensions-testbed)
|
|
for more details.
|
|
|
|
Click [here](https://docs.cosmos.network/main/build/abci/vote-extensions) if you would like a walkthrough of how to implement vote extensions.
|
|
|
|
|
|
## Verify Vote Extension
|
|
|
|
Similar to extending a vote, an application can also verify vote extensions from
|
|
other validators when validating their pre-commits. For a given vote extension,
|
|
this process MUST be deterministic. The Cosmos SDK defines [`sdk.VerifyVoteExtensionHandler`](https://github.com/cosmos/cosmos-sdk/blob/v0.52.0-beta.1/types/abci.go#L29-L31):
|
|
|
|
```go
|
|
type VerifyVoteExtensionHandler func(Context, *abci.VerifyVoteExtensionRequest) (*abci.VerifyVoteExtensionResponse, error)
|
|
```
|
|
|
|
An application can set this handler in `app.go` via the `baseapp.SetVerifyVoteExtensionHandler`
|
|
`BaseApp` option function. The `sdk.VerifyVoteExtensionHandler`, if defined, is called
|
|
during the `VerifyVoteExtension` ABCI method. If an application defines a vote
|
|
extension handler, it should also define a verification handler. Note, not all
|
|
validators will share the same view of what vote extensions they verify depending
|
|
on how votes are propagated. See [here](https://docs.cometbft.com/v1.0/spec/abci/abci++_methods#verifyvoteextension)
|
|
for more details.
|
|
|
|
Additionally, please keep in mind that performance can be degraded if vote extensions are too big ([see vote extension testbed](https://docs.cometbft.com/v1.0/references/qa/cometbft-qa-38#vote-extensions-testbed)), so we highly recommend a size validation in `VerifyVoteExtensions`.
|
|
|
|
|
|
## Vote Extension Propagation
|
|
|
|
The agreed upon vote extensions at height `H` are provided to the proposing validator
|
|
at height `H+1` during `PrepareProposal`. As a result, the vote extensions are
|
|
not natively provided or exposed to the remaining validators during `ProcessProposal`.
|
|
As a result, if an application requires that the agreed upon vote extensions from
|
|
height `H` are available to all validators at `H+1`, the application must propagate
|
|
these vote extensions manually in the block proposal itself. This can be done by
|
|
"injecting" them into the block proposal, since the `Txs` field in `PrepareProposal`
|
|
is just a slice of byte slices.
|
|
|
|
`FinalizeBlock` will ignore any byte slice that doesn't implement an `sdk.Tx`, so
|
|
any injected vote extensions will safely be ignored in `FinalizeBlock`. For more
|
|
details on propagation, see the [ABCI++ 2.0 ADR](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-064-abci-2.0.md#vote-extension-propagation--verification).
|
|
|
|
### Recovery of injected Vote Extensions
|
|
|
|
As stated before, vote extensions can be injected into a block proposal (along with
|
|
other transactions in the `Txs` field). The Cosmos SDK provides a pre-FinalizeBlock
|
|
hook to allow applications to recover vote extensions, perform any necessary
|
|
computation on them, and then store the results in the cached store. These results
|
|
will be available to the application during the subsequent `FinalizeBlock` call.
|
|
|
|
An example of how a pre-FinalizeBlock hook could look like is shown below:
|
|
|
|
```go
|
|
app.SetPreBlocker(func(ctx sdk.Context, req *abci.RequestFinalizeBlock) error {
|
|
allVEs := []VE{} // store all parsed vote extensions here
|
|
for _, tx := range req.Txs {
|
|
// define a custom function that tries to parse the tx as a vote extension
|
|
ve, ok := parseVoteExtension(tx)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
allVEs = append(allVEs, ve)
|
|
}
|
|
|
|
// perform any necessary computation on the vote extensions and store the result
|
|
// in the cached store
|
|
result := compute(allVEs)
|
|
err := storeVEResult(ctx, result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
```
|
|
|
|
Then, in an app's module, the application can retrieve the result of the computation
|
|
of vote extensions from the cached store:
|
|
|
|
```go
|
|
func (k Keeper) BeginBlocker(ctx context.Context) error {
|
|
// retrieve the result of the computation of vote extensions from the cached store
|
|
result, err := k.GetVEResult(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// use the result of the computation of vote extensions
|
|
k.setSomething(result)
|
|
|
|
return nil
|
|
}
|
|
```
|
|
|
|
## Vote Extensions on v2
|
|
|
|
### Extend Vote
|
|
|
|
In v2, the `ExtendVoteHandler` function works in the same way as it does in v1,
|
|
but the implementation is passed as a server option when calling `cometbft.New`.
|
|
|
|
```go
|
|
serverOptions.ExtendVoteHandler = CustomExtendVoteHandler()
|
|
|
|
func CustomExtendVoteHandler() handlers.ExtendVoteHandler {
|
|
return func(ctx context.Context, rm store.ReaderMap, evr *v1.ExtendVoteRequest) (*v1.ExtendVoteResponse, error) {
|
|
return &v1.ExtendVoteResponse{
|
|
VoteExtension: []byte("BTC=1234567.89;height=" + fmt.Sprint(evr.Height)),
|
|
}, nil
|
|
}
|
|
}
|
|
```
|
|
|
|
### Verify Vote Extension
|
|
|
|
Same as above:
|
|
|
|
```go
|
|
serverOptions.VerifyVoteExtensionHandler = CustomVerifyVoteExtensionHandler()
|
|
|
|
func CustomVerifyVoteExtensionHandler() handlers.VerifyVoteExtensionHandler {
|
|
return func(context.Context, store.ReaderMap, *abci.VerifyVoteExtensionRequest) (*abci.VerifyVoteExtensionResponse, error) {
|
|
return &abci.VerifyVoteExtensionResponse{}, nil
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
### Prepare and Process Proposal
|
|
|
|
These are also passed in as server options when calling `cometbft.New`.
|
|
|
|
```go
|
|
serverOptions.PrepareProposalHandler = CustomPrepareProposal[T]()
|
|
serverOptions.ProcessProposalHandler = CustomProcessProposalHandler[T]()
|
|
```
|
|
|
|
The PrepareProposal handler can be used to inject vote extensions into the block proposal
|
|
by using the `cometbft.RawTx` util function, which allows passing in arbitrary bytes.
|
|
|
|
```go
|
|
func CustomPrepareProposal[T transaction.Tx]() handlers.PrepareHandler[T] {
|
|
return func(ctx context.Context, app handlers.AppManager[T], codec transaction.Codec[T], req *v1.PrepareProposalRequest, chainID string) ([]T, error) {
|
|
var txs []T
|
|
for _, tx := range req.Txs {
|
|
decTx, err := codec.Decode(tx)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
txs = append(txs, decTx)
|
|
}
|
|
|
|
// "Process" vote extensions (we'll just inject all votes)
|
|
injectedTx, err := json.Marshal(req.LocalLastCommit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// put the injected tx into the first position
|
|
txs = append([]T{cometbft.RawTx(injectedTx).(T)}, txs...)
|
|
|
|
return txs, nil
|
|
}
|
|
}
|
|
```
|
|
|
|
The ProcessProposal handler can be used to recover the vote extensions from the first transaction
|
|
and perform any necessary verification on them. In the example below we also use the
|
|
`cometbft.ValidateVoteExtensions` util to verify the signature of the vote extensions;
|
|
this function takes a "validatorStore" function that returns the public key of a validator
|
|
given its consensus address. In the example we use the default staking module to get the
|
|
validators.
|
|
|
|
```go
|
|
func CustomProcessProposalHandler[T transaction.Tx]() handlers.ProcessHandler[T] {
|
|
return func(ctx context.Context, am handlers.AppManager[T], c transaction.Codec[T], req *v1.ProcessProposalRequest, chainID string) error {
|
|
// Get all vote extensions from the first tx
|
|
|
|
injectedTx := req.Txs[0]
|
|
var voteExts v1.ExtendedCommitInfo
|
|
if err := json.Unmarshal(injectedTx, &voteExts); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get validators from the staking module
|
|
res, err := am.Query(
|
|
ctx,
|
|
0,
|
|
&staking.QueryValidatorsRequest{},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
validatorsResponse := res.(*staking.QueryValidatorsResponse)
|
|
consAddrToPubkey := map[string]cryptotypes.PubKey{}
|
|
|
|
for _, val := range validatorsResponse.GetValidators() {
|
|
cv := val.ConsensusPubkey.GetCachedValue()
|
|
if cv == nil {
|
|
return fmt.Errorf("public key cached value is nil")
|
|
}
|
|
|
|
cpk, ok := cv.(cryptotypes.PubKey)
|
|
if ok {
|
|
consAddrToPubkey[string(cpk.Address().Bytes())] = cpk
|
|
} else {
|
|
return fmt.Errorf("invalid public key type")
|
|
}
|
|
}
|
|
|
|
// First verify that the vote extensions injected by the proposer are correct
|
|
if err := cometbft.ValidateVoteExtensions(
|
|
ctx,
|
|
am,
|
|
chainID,
|
|
func(ctx context.Context, b []byte) (cryptotypes.PubKey, error) {
|
|
if _, ok := consAddrToPubkey[string(b)]; !ok {
|
|
return nil, fmt.Errorf("validator not found")
|
|
}
|
|
return consAddrToPubkey[string(b)], nil
|
|
},
|
|
voteExts,
|
|
req.Height,
|
|
&req.ProposedLastCommit,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: do something with the vote extensions
|
|
|
|
return nil
|
|
}
|
|
}
|
|
```
|
|
|
|
|
|
### Preblocker
|
|
|
|
In v2, the `PreBlocker` function works in the same way as it does in v1. However, it is
|
|
now passed in as an option to `appbuilder.Build`.
|
|
|
|
```go
|
|
app.App, err = appBuilder.Build(runtime.AppBuilderWithPreblocker(
|
|
func(ctx context.Context, txs []T) error {
|
|
// to recover the vote extension use
|
|
voteExtBz := txs[0].Bytes()
|
|
err := doSomethingWithVoteExt(voteExtBz)
|
|
return err
|
|
},
|
|
))
|
|
``` |