Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add docs on server v2 vote extensions #23010

Merged
merged 1 commit into from
Dec 19, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
Comment on lines +150 to +154
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add proper verification logic

The current implementation returns an empty response without any verification. This could be misleading as production code should properly validate vote extensions.

Consider this implementation:

-func CustomVerifyVoteExtensionHandler]() handlers.VerifyVoteExtensionHandler {
-    return  func(context.Context, store.ReaderMap, *abci.VerifyVoteExtensionRequest) (*abci.VerifyVoteExtensionResponse, error) {
-        return &abci.VerifyVoteExtensionResponse{}, nil
-    }
+func CustomVerifyVoteExtensionHandler() handlers.VerifyVoteExtensionHandler {
+    return func(ctx context.Context, rm store.ReaderMap, req *abci.VerifyVoteExtensionRequest) (*abci.VerifyVoteExtensionResponse, error) {
+        if req == nil || len(req.VoteExtension) == 0 {
+            return nil, fmt.Errorf("invalid vote extension")
+        }
+        
+        // Verify the format and data
+        if err := validateVoteExtensionFormat(req.VoteExtension); err != nil {
+            return nil, fmt.Errorf("invalid vote extension format: %w", err)
+        }
+        
+        return &abci.VerifyVoteExtensionResponse{}, nil
+    }
 }

Committable suggestion skipped: line range outside the PR's diff.


```

### 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
},
))
```
Loading