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

Oracle vote extensions #47

Merged
merged 47 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e19b1c0
bootstrap code for oracle vote extensions
antstalepresh Dec 18, 2023
f80aa19
update voteExtHandler bApp config
antstalepresh Dec 21, 2023
75fec8d
Resolve nil memory pointer issue
antstalepresh Dec 22, 2023
75783d2
cli commands addition
antstalepresh Jan 8, 2024
e7de75a
SetOraclePrices function update to set the price on correct key
antstalepresh Jan 9, 2024
367ab82
oracle extension price change working
antstalepresh Jan 11, 2024
4af9e04
remove unused store service
antstalepresh Jan 11, 2024
d1ca02c
simplification for not required fields
antstalepresh Jan 11, 2024
3fe0966
Merge branch 'sdk-50' of github.com:Team-Kujira/core into oracle-vote…
antstalepresh Jan 11, 2024
9e5bdd3
Update PrepareProposal, ProcessProposal, PreBlocker to handle both de…
antstalepresh Jan 17, 2024
dcff75b
Add oracle endpoint on app.toml for custom configuration
antstalepresh Jan 17, 2024
23d3032
Remove supportedPairs logic and let all the pairs to be submitted
antstalepresh Jan 18, 2024
7dbc33c
Remove unused app/abci.go - legacy implementation of vote extension
antstalepresh Jan 18, 2024
e519d9f
add miss counter using original median and miss counter logic
antstalepresh Jan 22, 2024
ece3b8a
add consensus addr and validator mapping for GetBallotByDenom
antstalepresh Jan 23, 2024
4e5969f
Implement body for compareOraclePrices and compareMissMap
antstalepresh Jan 23, 2024
0bf5027
Add DeleteExchangeRate on PreBlocker & Remove unused code
antstalepresh Jan 24, 2024
44119dd
Add SlashAndResetMissCounters on PreBlocker
antstalepresh Jan 24, 2024
1dcab55
update comments and logging
antstalepresh Jan 25, 2024
1f5a482
simplify PrepareProposal and ProcessProposal to reuse sdk default han…
antstalepresh Jan 25, 2024
fc67377
lint fixes
antstalepresh Jan 26, 2024
d274b90
add unit test for ComputeStakeWeightedPricesAndMissMap and GetBallotB…
antstalepresh Jan 29, 2024
6e2d4fa
set required denoms on unit test
antstalepresh Jan 29, 2024
6af59b5
Write unit test for CompareOraclePrices & CompareMissMap
antstalepresh Jan 30, 2024
f299ca4
add unit test for PrepareProposal
antstalepresh Jan 31, 2024
d5ef346
add unit test for ProcessProposal
antstalepresh Feb 1, 2024
f6c76af
add unit test for PreBlocker
antstalepresh Feb 1, 2024
718bc47
add utility function SetupTest and use it on oracle abci
antstalepresh Feb 1, 2024
8dc920b
fix panic when empty txs pass
antstalepresh Feb 1, 2024
a95cf87
Add unit test for ExtendVoteHandler and VerifyVoteExtensionHandler
antstalepresh Feb 2, 2024
560148b
Threshold check for oracle vote extension
antstalepresh Feb 5, 2024
32f3d9f
Recover unit test for OracleTally
antstalepresh Feb 6, 2024
d160af4
remove abci test
antstalepresh Feb 6, 2024
bf19ff5
resolve sync issue on a new validator when oracle server's off
antstalepresh Feb 7, 2024
2b807f9
simplification
antstalepresh Feb 14, 2024
12b515c
vote extension enable check on PrepareProposal
antstalepresh Feb 14, 2024
a56653f
update prepareProposal for better readability
antstalepresh Feb 14, 2024
c29d16a
return default empty extension when http request fails
antstalepresh Feb 14, 2024
feb2535
Merge branch 'sdk-50' into oracle-vote-extensions
codehans Feb 14, 2024
0fa65a9
Merge branch 'oracle-vote-extensions' of github.com:Team-Kujira/core …
codehans Feb 14, 2024
c63affd
optimistic exec
codehans Feb 14, 2024
b5777ec
ibc-tm
codehans Feb 16, 2024
9f8c7a4
Merge branch 'sdk-50' of github.com:Team-Kujira/core into oracle-vote…
antstalepresh Feb 21, 2024
03985a2
Merge branch 'sdk-50' into oracle-vote-extensions
codehans Feb 22, 2024
7e9f70b
lint
antstalepresh Feb 26, 2024
ffcd41c
lint
antstalepresh Feb 26, 2024
819b141
lint
antstalepresh Feb 26, 2024
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
50 changes: 17 additions & 33 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ import (
schedulertypes "github.com/Team-Kujira/core/x/scheduler/types"

"github.com/Team-Kujira/core/x/oracle"
oracleabci "github.com/Team-Kujira/core/x/oracle/abci"
oraclekeeper "github.com/Team-Kujira/core/x/oracle/keeper"
oracletypes "github.com/Team-Kujira/core/x/oracle/types"

Expand Down Expand Up @@ -302,39 +303,6 @@ func New(
std.RegisterLegacyAminoCodec(legacyAmino)
std.RegisterInterfaces(interfaceRegistry)

// Below we could construct and set an application specific mempool and
// ABCI 1.0 PrepareProposal and ProcessProposal handlers. These defaults are
// already set in the SDK's BaseApp, this shows an example of how to override
// them.
//
// Example:
//
// bApp := baseapp.NewBaseApp(...)
// nonceMempool := mempool.NewSenderNonceMempool()
// abciPropHandler := NewDefaultProposalHandler(nonceMempool, bApp)
//
// bApp.SetMempool(nonceMempool)
// bApp.SetPrepareProposal(abciPropHandler.PrepareProposalHandler())
// bApp.SetProcessProposal(abciPropHandler.ProcessProposalHandler())
//
// Alternatively, you can construct BaseApp options, append those to
// baseAppOptions and pass them to NewBaseApp.
//
// Example:
//
// prepareOpt = func(app *baseapp.BaseApp) {
// abciPropHandler := baseapp.NewDefaultProposalHandler(nonceMempool, app)
// app.SetPrepareProposal(abciPropHandler.PrepareProposalHandler())
// }
// baseAppOptions = append(baseAppOptions, prepareOpt)

// create and set dummy vote extension handler
voteExtOp := func(bApp *baseapp.BaseApp) {
voteExtHandler := NewVoteExtensionHandler()
voteExtHandler.SetHandlers(bApp)
}
baseAppOptions = append(baseAppOptions, voteExtOp)

bApp := baseapp.NewBaseApp(Name, logger, db, txConfig.TxDecoder(), baseAppOptions...)
bApp.SetCommitMultiStoreTracer(traceStore)
bApp.SetVersion(version.Version)
Expand Down Expand Up @@ -632,6 +600,22 @@ func New(
authority,
)

voteExtHandler := oracleabci.NewVoteExtHandler(
logger,
app.OracleKeeper,
)

propHandler := oracleabci.NewProposalHandler(
logger,
app.OracleKeeper,
app.StakingKeeper,
)
bApp.SetExtendVoteHandler(voteExtHandler.ExtendVoteHandler())
bApp.SetVerifyVoteExtensionHandler(voteExtHandler.VerifyVoteExtensionHandler())
bApp.SetPrepareProposal(propHandler.PrepareProposal())
bApp.SetProcessProposal(propHandler.ProcessProposal())
bApp.SetPreBlocker(propHandler.PreBlocker)

denomKeeper := denomkeeper.NewKeeper(
appCodec,
keys[denomtypes.StoreKey],
Expand Down
184 changes: 184 additions & 0 deletions x/oracle/abci/proposal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package abci

import (
"encoding/json"
"errors"
"fmt"

"cosmossdk.io/log"
"cosmossdk.io/math"
"github.com/Team-Kujira/core/x/oracle/keeper"
abci "github.com/cometbft/cometbft/abci/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
)

// StakeWeightedPrices defines the structure a proposer should use to calculate
// and submit the stake-weighted prices for a given set of supported currency
// pairs, in addition to the vote extensions used to calculate them. This is so
// validators can verify the proposer's calculations.
type StakeWeightedPrices struct {
StakeWeightedPrices map[string]math.LegacyDec
ExtendedCommitInfo abci.ExtendedCommitInfo
}

type ProposalHandler struct {
logger log.Logger
keeper keeper.Keeper
valStore baseapp.ValidatorStore
}

func NewProposalHandler(logger log.Logger, keeper keeper.Keeper, valStore baseapp.ValidatorStore) *ProposalHandler {
return &ProposalHandler{
logger: logger,
keeper: keeper,
valStore: valStore,
}
}

func (h *ProposalHandler) PrepareProposal() sdk.PrepareProposalHandler {
return func(ctx sdk.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {

Check failure on line 41 in x/oracle/abci/proposal.go

View workflow job for this annotation

GitHub Actions / lint

unnecessary leading newline (whitespace)

Check failure on line 42 in x/oracle/abci/proposal.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gofumpt`-ed (gofumpt)
err := baseapp.ValidateVoteExtensions(ctx, h.valStore, req.Height, ctx.ChainID(), req.LocalLastCommit)
if err != nil {
return nil, err
}

proposalTxs := req.Txs

if req.Height >= ctx.ConsensusParams().Abci.VoteExtensionsEnableHeight {
stakeWeightedPrices, err := h.computeStakeWeightedOraclePrices(ctx, req.LocalLastCommit)
if err != nil {
return nil, errors.New("failed to compute stake-weighted oracle prices")
}

injectedVoteExtTx := StakeWeightedPrices{
StakeWeightedPrices: stakeWeightedPrices,
ExtendedCommitInfo: req.LocalLastCommit,
}

// NOTE: We use stdlib JSON encoding, but an application may choose to use
// a performant mechanism. This is for demo purposes only.
bz, err := json.Marshal(injectedVoteExtTx)
if err != nil {
h.logger.Error("failed to encode injected vote extension tx", "err", err)
return nil, errors.New("failed to encode injected vote extension tx")
}

// Inject a "fake" tx into the proposal s.t. validators can decode, verify,
// and store the canonical stake-weighted average prices.
proposalTxs = append(proposalTxs, bz)
}

// proceed with normal block proposal construction, e.g. POB, normal txs, etc...

return &abci.ResponsePrepareProposal{
Txs: proposalTxs,
}, nil
}
}

func (h *ProposalHandler) ProcessProposal() sdk.ProcessProposalHandler {
return func(ctx sdk.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
if len(req.Txs) == 0 {
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil
}

var injectedVoteExtTx StakeWeightedPrices
if err := json.Unmarshal(req.Txs[0], &injectedVoteExtTx); err != nil {
h.logger.Error("failed to decode injected vote extension tx", "err", err)
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}

err := baseapp.ValidateVoteExtensions(ctx, h.valStore, req.Height, ctx.ChainID(), injectedVoteExtTx.ExtendedCommitInfo)
if err != nil {
return nil, err
}

// Verify the proposer's stake-weighted oracle prices by computing the same
// calculation and comparing the results. We omit verification for brevity
// and demo purposes.
stakeWeightedPrices, err := h.computeStakeWeightedOraclePrices(ctx, injectedVoteExtTx.ExtendedCommitInfo)
if err != nil {
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}
if err := compareOraclePrices(injectedVoteExtTx.StakeWeightedPrices, stakeWeightedPrices); err != nil {
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}

return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil
}
}

func (h *ProposalHandler) PreBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) {
res := &sdk.ResponsePreBlock{}
if len(req.Txs) == 0 {
return res, nil
}

var injectedVoteExtTx StakeWeightedPrices
if err := json.Unmarshal(req.Txs[0], &injectedVoteExtTx); err != nil {
h.logger.Error("failed to decode injected vote extension tx", "err", err)
return nil, err
}

// set oracle prices using the passed in context, which will make these prices available in the current block
if err := h.keeper.SetOraclePrices(ctx, injectedVoteExtTx.StakeWeightedPrices); err != nil {
return nil, err
}

return res, nil
}

func (h *ProposalHandler) computeStakeWeightedOraclePrices(ctx sdk.Context, ci abci.ExtendedCommitInfo) (map[string]math.LegacyDec, error) {
requiredPairs := h.keeper.GetSupportedPairs(ctx)
stakeWeightedPrices := make(map[string]math.LegacyDec, len(requiredPairs)) // base -> average stake-weighted price
for _, pair := range requiredPairs {
stakeWeightedPrices[pair.Base] = math.LegacyZeroDec()
}

var totalStake int64
for _, v := range ci.Votes {
if v.BlockIdFlag != cmtproto.BlockIDFlagCommit {
continue
}

var voteExt OracleVoteExtension
if err := json.Unmarshal(v.VoteExtension, &voteExt); err != nil {
h.logger.Error("failed to decode vote extension", "err", err, "validator", fmt.Sprintf("%x", v.Validator.Address))
return nil, err
}

totalStake += v.Validator.Power

// Compute stake-weighted average of prices for each supported pair, i.e.
// (P1)(W1) + (P2)(W2) + ... + (Pn)(Wn) / (W1 + W2 + ... + Wn)
//
// NOTE: These are the prices computed at the PREVIOUS height, i.e. H-1
for base, price := range voteExt.Prices {
// Only compute stake-weighted average for supported pairs.
//
// NOTE: VerifyVoteExtension should be sufficient to ensure that only
// supported pairs are supplied, but we add this here for demo purposes.
if _, ok := stakeWeightedPrices[base]; ok {
stakeWeightedPrices[base] = stakeWeightedPrices[base].Add(price.MulInt64(v.Validator.Power))
}
}
}

if totalStake == 0 {
return nil, nil
}

// finalize average by dividing by total stake, i.e. total weights
for base, price := range stakeWeightedPrices {
stakeWeightedPrices[base] = price.QuoInt64(totalStake)
}

return stakeWeightedPrices, nil
}

func compareOraclePrices(p1, p2 map[string]math.LegacyDec) error {

Check warning on line 182 in x/oracle/abci/proposal.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'p1' seems to be unused, consider removing or renaming it as _ (revive)
return nil
}
118 changes: 118 additions & 0 deletions x/oracle/abci/vote_extensions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package abci

import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"

"cosmossdk.io/log"
"cosmossdk.io/math"
"github.com/Team-Kujira/core/x/oracle/keeper"
abci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

type VoteExtHandler struct {
logger log.Logger
currentBlock int64 // current block height
lastPriceSyncTS time.Time // last time we synced prices

Keeper keeper.Keeper
}

func NewVoteExtHandler(
logger log.Logger,
keeper keeper.Keeper,
) *VoteExtHandler {
return &VoteExtHandler{
logger: logger,
Keeper: keeper,
}
}

// OracleVoteExtension defines the canonical vote extension structure.
type OracleVoteExtension struct {
Height int64
Prices map[string]math.LegacyDec
}

type PricesResponse struct {
Prices map[string]math.LegacyDec `json:"prices"`
}

func (h *VoteExtHandler) ExtendVoteHandler() sdk.ExtendVoteHandler {
return func(ctx sdk.Context, req *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) {
h.currentBlock = req.Height
h.lastPriceSyncTS = time.Now()

h.logger.Info("computing oracle prices for vote extension", "height", req.Height, "time", h.lastPriceSyncTS)

requestURL := fmt.Sprintf("http://localhost:10171/api/v1/prices")

Check failure on line 52 in x/oracle/abci/vote_extensions.go

View workflow job for this annotation

GitHub Actions / lint

S1039: unnecessary use of fmt.Sprintf (gosimple)
res, err := http.Get(requestURL)

Check failure on line 53 in x/oracle/abci/vote_extensions.go

View workflow job for this annotation

GitHub Actions / lint

G107: Potential HTTP request made with variable url (gosec)
if err != nil {
return nil, err
}

resBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

prices := PricesResponse{}
err = json.Unmarshal(resBody, &prices)
if err != nil {
return nil, err
}

computedPrices := prices.Prices

// produce a canonical vote extension
voteExt := OracleVoteExtension{
Height: req.Height,
Prices: computedPrices,
}

h.logger.Info("computed prices", "prices", computedPrices)

// NOTE: We use stdlib JSON encoding, but an application may choose to use
// a performant mechanism. This is for demo purposes only.
bz, err := json.Marshal(voteExt)
if err != nil {
return nil, fmt.Errorf("failed to marshal vote extension: %w", err)
}

return &abci.ResponseExtendVote{VoteExtension: bz}, nil
}
}

func (h *VoteExtHandler) VerifyVoteExtensionHandler() sdk.VerifyVoteExtensionHandler {
return func(ctx sdk.Context, req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) {
var voteExt OracleVoteExtension

err := json.Unmarshal(req.VoteExtension, &voteExt)
if err != nil {
// NOTE: It is safe to return an error as the Cosmos SDK will capture all
// errors, log them, and reject the proposal.
return nil, fmt.Errorf("failed to unmarshal vote extension: %w", err)
}

if voteExt.Height != req.Height {
return nil, fmt.Errorf("vote extension height does not match request height; expected: %d, got: %d", req.Height, voteExt.Height)
}

// Verify incoming prices from a validator are valid. Note, verification during
// VerifyVoteExtensionHandler MUST be deterministic. For brevity and demo
// purposes, we omit implementation.
if err := h.verifyOraclePrices(ctx, voteExt.Prices); err != nil {
return nil, fmt.Errorf("failed to verify oracle prices from validator %X: %w", req.ValidatorAddress, err)
}

return &abci.ResponseVerifyVoteExtension{Status: abci.ResponseVerifyVoteExtension_ACCEPT}, nil
}
}

func (h *VoteExtHandler) verifyOraclePrices(ctx sdk.Context, prices map[string]math.LegacyDec) error {

Check warning on line 116 in x/oracle/abci/vote_extensions.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
return nil
}
16 changes: 16 additions & 0 deletions x/oracle/abci/vote_extensions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package abci_test

import (
"encoding/json"
"testing"

"github.com/Team-Kujira/core/x/oracle/abci"
"github.com/stretchr/testify/require"
)

func TestDecoding(t *testing.T) {
resBody := []byte(`{"prices":{"BTC":"47375.706652541026694000","ETH":"2649.328939436595054949","USDT":"1.000661260343873178"}}`)
prices := abci.PricesResponse{}
err := json.Unmarshal(resBody, &prices)
require.NoError(t, err)
}
Loading
Loading