diff --git a/docs/build/abci/03-vote-extensions.md b/docs/build/abci/03-vote-extensions.md index 26cbfd1a296d..1ce431618f21 100644 --- a/docs/build/abci/03-vote-extensions.md +++ b/docs/build/abci/03-vote-extensions.md @@ -120,3 +120,163 @@ func (k Keeper) BeginBlocker(ctx context.Context) error { 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 +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 + }, +)) +``` \ No newline at end of file