Skip to main content

Implement ExtendVote

First, we’ll create the OracleVoteExtension struct. This is the object that will be marshaled as bytes and signed by the validator. In our example, we’ll use JSON to marshal the vote extension for simplicity, but we recommend finding an encoding that produces a smaller output, given that large vote extensions could impact CometBFT’s performance. Custom encodings and compressed bytes can be used out of the box.
Then we’ll create a VoteExtensionsHandler struct that contains everything we need to query for prices.
Finally, a function that returns sdk.ExtendVoteHandler is needed too, and this is where our vote extension logic will live.
As you can see above, the creation of a vote extension is pretty simple and we just have to return bytes. CometBFT will handle the signing of these bytes for us. We ignored the process of getting the prices but you can see a more complete example here: Here we’ll do some simple checks like:
  • Is the vote extension unmarshaled correctly?
  • Is the vote extension for the right height?
  • Some other validation, for example, are the prices from this extension too deviated from my own prices? Or maybe checks that can detect malicious behavior.

Implement PrepareProposal

And we create the struct for our “special tx” that will contain the prices and the votes so validators can later re-check in ProcessProposal that they get the same result as the block’s proposer. With this, we could also check if all the votes have been used by comparing the votes received in ProcessProposal.
Now we create the PrepareProposalHandler. In this step, we’ll first check if the vote extensions’ signatures are correct using a helper function called ValidateVoteExtensions from the baseapp package.
Then we proceed to make the calculations only if the current height is higher than the height at which vote extensions have been enabled. Remember that vote extensions are made available to the block proposer on the next block at which they are produced/enabled.
Finally we inject the result as a transaction at a specific location, usually at the beginning of the block:

Implement ProcessProposal

Now we can implement the method that all validators will execute to ensure the proposer is doing his work correctly. Here, if vote extensions are enabled, we’ll check if the tx at index 0 is an injected vote extension.
Then we re-validate the vote extensions signatures using baseapp.ValidateVoteExtensions, recalculate the results (just like in PrepareProposal), and compare them with the results we got from the injected tx.
Important: In this example we avoided using the mempool and other basics, please refer to the DefaultProposalHandler for a complete implementation: Link

Implement PreBlocker

Now validators are extending their vote, verifying other votes and including the result in the block. But how do we actually make use of this result? This is done in the PreBlocker which is code that is run before any other code during FinalizeBlock so we make sure we make this information available to the chain and its modules during the entire block execution (from BeginBlock). At this step we know that the injected tx is well-formatted and has been verified by the validators participating in consensus, so making use of it is straightforward. Just check if vote extensions are enabled, pick up the first transaction and use a method in your module’s keeper to set the result.

Conclusion

In this tutorial, we’ve created a simple price oracle module that incorporates vote extensions. We’ve seen how to implement ExtendVote, VerifyVoteExtension, PrepareProposal, ProcessProposal, and PreBlocker to handle the voting and verification process of vote extensions, as well as how to make use of the results during the block execution.