Skip to content

Commit 65ceca6

Browse files
docs: add docs on server v2 vote extensions (#23010)
1 parent 2c16372 commit 65ceca6

File tree

1 file changed

+160
-0
lines changed

1 file changed

+160
-0
lines changed

docs/build/abci/03-vote-extensions.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,163 @@ func (k Keeper) BeginBlocker(ctx context.Context) error {
120120
return nil
121121
}
122122
```
123+
124+
## Vote Extensions on v2
125+
126+
### Extend Vote
127+
128+
In v2, the `ExtendVoteHandler` function works in the same way as it does in v1,
129+
but the implementation is passed as a server option when calling `cometbft.New`.
130+
131+
```go
132+
serverOptions.ExtendVoteHandler = CustomExtendVoteHandler()
133+
134+
func CustomExtendVoteHandler() handlers.ExtendVoteHandler {
135+
return func(ctx context.Context, rm store.ReaderMap, evr *v1.ExtendVoteRequest) (*v1.ExtendVoteResponse, error) {
136+
return &v1.ExtendVoteResponse{
137+
VoteExtension: []byte("BTC=1234567.89;height=" + fmt.Sprint(evr.Height)),
138+
}, nil
139+
}
140+
}
141+
```
142+
143+
### Verify Vote Extension
144+
145+
Same as above:
146+
147+
```go
148+
serverOptions.VerifyVoteExtensionHandler = CustomVerifyVoteExtensionHandler()
149+
150+
func CustomVerifyVoteExtensionHandler]() handlers.VerifyVoteExtensionHandler {
151+
return func(context.Context, store.ReaderMap, *abci.VerifyVoteExtensionRequest) (*abci.VerifyVoteExtensionResponse, error) {
152+
return &abci.VerifyVoteExtensionResponse{}, nil
153+
}
154+
}
155+
156+
```
157+
158+
### Prepare and Process Proposal
159+
160+
These are also passed in as server options when calling `cometbft.New`.
161+
162+
```go
163+
serverOptions.PrepareProposalHandler = CustomPrepareProposal[T]()
164+
serverOptions.ProcessProposalHandler = CustomProcessProposalHandler[T]()
165+
```
166+
167+
The PrepareProposal handler can be used to inject vote extensions into the block proposal
168+
by using the `cometbft.RawTx` util function, which allows passing in arbitrary bytes.
169+
170+
```go
171+
func CustomPrepareProposal[T transaction.Tx]() handlers.PrepareHandler[T] {
172+
return func(ctx context.Context, app handlers.AppManager[T], codec transaction.Codec[T], req *v1.PrepareProposalRequest, chainID string) ([]T, error) {
173+
var txs []T
174+
for _, tx := range req.Txs {
175+
decTx, err := codec.Decode(tx)
176+
if err != nil {
177+
continue
178+
}
179+
180+
txs = append(txs, decTx)
181+
}
182+
183+
// "Process" vote extensions (we'll just inject all votes)
184+
injectedTx, err := json.Marshal(req.LocalLastCommit)
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
// put the injected tx into the first position
190+
txs = append([]T{cometbft.RawTx(injectedTx).(T)}, txs...)
191+
192+
return txs, nil
193+
}
194+
}
195+
```
196+
197+
The ProcessProposal handler can be used to recover the vote extensions from the first transaction
198+
and perform any necessary verification on them. In the example below we also use the
199+
`cometbft.ValidateVoteExtensions` util to verify the signature of the vote extensions;
200+
this function takes a "validatorStore" function that returns the public key of a validator
201+
given its consensus address. In the example we use the default staking module to get the
202+
validators.
203+
204+
```go
205+
func CustomProcessProposalHandler[T transaction.Tx]() handlers.ProcessHandler[T] {
206+
return func(ctx context.Context, am handlers.AppManager[T], c transaction.Codec[T], req *v1.ProcessProposalRequest, chainID string) error {
207+
// Get all vote extensions from the first tx
208+
209+
injectedTx := req.Txs[0]
210+
var voteExts v1.ExtendedCommitInfo
211+
if err := json.Unmarshal(injectedTx, &voteExts); err != nil {
212+
return err
213+
}
214+
215+
// Get validators from the staking module
216+
res, err := am.Query(
217+
ctx,
218+
0,
219+
&staking.QueryValidatorsRequest{},
220+
)
221+
if err != nil {
222+
return err
223+
}
224+
225+
validatorsResponse := res.(*staking.QueryValidatorsResponse)
226+
consAddrToPubkey := map[string]cryptotypes.PubKey{}
227+
228+
for _, val := range validatorsResponse.GetValidators() {
229+
cv := val.ConsensusPubkey.GetCachedValue()
230+
if cv == nil {
231+
return fmt.Errorf("public key cached value is nil")
232+
}
233+
234+
cpk, ok := cv.(cryptotypes.PubKey)
235+
if ok {
236+
consAddrToPubkey[string(cpk.Address().Bytes())] = cpk
237+
} else {
238+
return fmt.Errorf("invalid public key type")
239+
}
240+
}
241+
242+
// First verify that the vote extensions injected by the proposer are correct
243+
if err := cometbft.ValidateVoteExtensions(
244+
ctx,
245+
am,
246+
chainID,
247+
func(ctx context.Context, b []byte) (cryptotypes.PubKey, error) {
248+
if _, ok := consAddrToPubkey[string(b)]; !ok {
249+
return nil, fmt.Errorf("validator not found")
250+
}
251+
return consAddrToPubkey[string(b)], nil
252+
},
253+
voteExts,
254+
req.Height,
255+
&req.ProposedLastCommit,
256+
); err != nil {
257+
return err
258+
}
259+
260+
// TODO: do something with the vote extensions
261+
262+
return nil
263+
}
264+
}
265+
```
266+
267+
268+
### Preblocker
269+
270+
In v2, the `PreBlocker` function works in the same way as it does in v1. However, it is
271+
is now passed in as an option to `appbuilder.Build`.
272+
273+
```go
274+
app.App, err = appBuilder.Build(runtime.AppBuilderWithPreblocker(
275+
func(ctx context.Context, txs []T) error {
276+
// to recover the vote extension use
277+
voteExtBz := txs[0].Bytes()
278+
err := doSomethingWithVoteExt(voteExtBz)
279+
return err
280+
},
281+
))
282+
```

0 commit comments

Comments
 (0)