Skip to content

Commit

Permalink
docs: add docs on server v2 vote extensions (#23010)
Browse files Browse the repository at this point in the history
  • Loading branch information
facundomedica authored Dec 19, 2024
1 parent 2c16372 commit 65ceca6
Showing 1 changed file with 160 additions and 0 deletions.
160 changes: 160 additions & 0 deletions docs/build/abci/03-vote-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
))
```

0 comments on commit 65ceca6

Please sign in to comment.