Skip to content

Commit

Permalink
vote extensions w/o app wiring
Browse files Browse the repository at this point in the history
  • Loading branch information
adamewozniak committed Sep 29, 2024
1 parent 14c03f3 commit 16f6d71
Show file tree
Hide file tree
Showing 9 changed files with 1,047 additions and 2 deletions.
12 changes: 11 additions & 1 deletion app/preblocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"cosmossdk.io/core/appmodule"
cometabci "github.com/cometbft/cometbft/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
gmptypes "github.com/ojo-network/ojo/x/gmp/types"
"github.com/ojo-network/ojo/x/oracle/abci"
"github.com/ojo-network/ojo/x/oracle/types"
)
Expand Down Expand Up @@ -61,10 +62,19 @@ func (app *App) PreBlocker(ctx sdk.Context, req *cometabci.RequestFinalizeBlock)
}
app.OracleKeeper.SetAggregateExchangeRateVote(ctx, valAddr, exchangeRateVote)
}

// now process gmp proposal
var gmpInjectedVoteExtension gmptypes.InjectedVoteExtensionTx
if err := gmpInjectedVoteExtension.Unmarshal(req.Txs[0]); err != nil {
app.Logger().Error("failed to decode injected vote extension tx", "err", err)
return nil, err
}
fmt.Println("gmpInjectedVoteExtension", gmpInjectedVoteExtension)
// set gas estimate
}

app.Logger().Info(
"oracle preblocker executed",
"preblocker executed",
"vote_extensions_enabled", voteExtensionsEnabled,
)

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
github.com/mgechev/revive v1.3.9
github.com/mitchellh/mapstructure v1.5.0
github.com/ojo-network/ojo-evm/relayer v0.0.0-20240904192312-acda927a5d24
github.com/ojo-network/price-feeder v0.2.1-rc1
github.com/ory/dockertest/v3 v3.10.0
github.com/rs/zerolog v1.32.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,8 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q=
github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s=
github.com/ojo-network/ojo-evm/relayer v0.0.0-20240904192312-acda927a5d24 h1:t0E41HkLJJmyh8rCwNjGQPoHfQSH4QHDTg51UawFKyA=
github.com/ojo-network/ojo-evm/relayer v0.0.0-20240904192312-acda927a5d24/go.mod h1:dW6/pQ+hNtC6955jQUR1cUJrGWcQ6fNEjeSLJCJPz3A=
github.com/ojo-network/price-feeder v0.2.1-rc1 h1:m9vgR9DYuXhM5f2DQX/wewihzkPVAVdVjZgtr/gB6bs=
github.com/ojo-network/price-feeder v0.2.1-rc1/go.mod h1:CSdgh7XiOMLGQwRW8OqbL5FlkE5Mc3+fDZEMRN8JkYY=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
Expand Down
29 changes: 29 additions & 0 deletions proto/ojo/gmp/v1/abci.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
syntax = "proto3";
package ojo.gmp.v1;

import "gogoproto/gogo.proto";
import "cosmos_proto/cosmos.proto";

option go_package = "github.com/ojo-network/ojo/x/gmp/types";

option (gogoproto.goproto_getters_all) = false;

// GmpVoteExtension defines the vote extension structure used by the gmp
// module.
message GmpVoteExtension {
int64 height = 1;
string gas_estimation = 2 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec"
];
}

// InjectedVoteExtensionTx defines the vote extension tx injected by the prepare
// proposal handler.
message InjectedVoteExtensionTx {
string median_gas_estimation = 1 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec"
];
bytes extended_commit_info = 2;
}
258 changes: 258 additions & 0 deletions x/gmp/abci/proposal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package abci

import (
"fmt"
"sort"

"cosmossdk.io/log"
"cosmossdk.io/math"
cometabci "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"

stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
gmpkeeper "github.com/ojo-network/ojo/x/gmp/keeper"
gmptypes "github.com/ojo-network/ojo/x/gmp/types"
)

type ProposalHandler struct {
logger log.Logger
gmpKeeper gmpkeeper.Keeper
stakingKeeper stakingkeeper.Keeper
}

func NewProposalHandler(
logger log.Logger,
gmpKeeper gmpkeeper.Keeper,
stakingKeeper stakingkeeper.Keeper,
) *ProposalHandler {
return &ProposalHandler{
logger: logger,
gmpKeeper: gmpKeeper,
stakingKeeper: stakingKeeper,
}
}

// PrepareProposalHandler is called only on the selected validator as "block proposer" (selected by CometBFT, read
// more about this process here: https://docs.cometbft.com/v0.38/spec/consensus/proposer-selection). The block
// proposer is in charge of creating the next block by selecting the transactions from the mempool, and in this
// method it will create an extra transaction using the vote extension from the previous block which are only
// available on the next height at which vote extensions were enabled.
func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
return func(ctx sdk.Context, req *cometabci.RequestPrepareProposal) (*cometabci.ResponsePrepareProposal, error) {
if req == nil {
err := fmt.Errorf("prepare proposal received a nil request")
h.logger.Error(err.Error())
return nil, err
}

err := baseapp.ValidateVoteExtensions(ctx, h.stakingKeeper, req.Height, ctx.ChainID(), req.LocalLastCommit)
if err != nil {
return &cometabci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, err
}

if req.Txs == nil {
err := fmt.Errorf("prepare proposal received a request with nil Txs")
h.logger.Error(
"height", req.Height,
err.Error(),
)
return &cometabci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, err
}

proposalTxs := req.Txs

voteExtensionsEnabled := VoteExtensionsEnabled(ctx)
if voteExtensionsEnabled {
medianGasEstimation, err := h.generateMedianGasEstimate(ctx, req.LocalLastCommit)
if err != nil {
return &cometabci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, err
}
extendedCommitInfoBz, err := req.LocalLastCommit.Marshal()
if err != nil {
return &cometabci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, err
}

injectedVoteExtTx := gmptypes.InjectedVoteExtensionTx{
MedianGasEstimation: &medianGasEstimation,
ExtendedCommitInfo: extendedCommitInfoBz,
}

bz, err := injectedVoteExtTx.Marshal()
if err != nil {
h.logger.Error("failed to encode injected vote extension tx", "err", err)
return &cometabci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, gmptypes.ErrEncodeInjVoteExt
}

// Inject a placeholder tx into the proposal s.t. validators can decode, verify,
// and store the oracle exchange rate votes.
proposalTxs = append([][]byte{bz}, proposalTxs...)
}

h.logger.Info(
"prepared proposal",
"txs", len(proposalTxs),
"vote_extensions_enabled", voteExtensionsEnabled,
)

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

// ProcessProposalHandler is called on all validators, and they can verify if the proposed block is valid. In case an
// invalid block is being proposed validators can reject it, causing a new round of PrepareProposal to happen. This
// step MUST be deterministic.
func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
return func(ctx sdk.Context, req *cometabci.RequestProcessProposal) (*cometabci.ResponseProcessProposal, error) {
if req == nil {
err := fmt.Errorf("process proposal received a nil request")
h.logger.Error(err.Error())
return nil, err
}

if req.Txs == nil {
err := fmt.Errorf("process proposal received a request with nil Txs")
h.logger.Error(
"height", req.Height,
err.Error(),
)
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, err
}

voteExtensionsEnabled := VoteExtensionsEnabled(ctx)
if voteExtensionsEnabled {
if len(req.Txs) < 1 {
h.logger.Error("got process proposal request with no commit info")
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT},
gmptypes.ErrNoCommitInfo
}

var injectedVoteExtTx gmptypes.InjectedVoteExtensionTx
if err := injectedVoteExtTx.Unmarshal(req.Txs[0]); err != nil {
h.logger.Error("failed to decode injected vote extension tx", "err", err)
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, err
}
var extendedCommitInfo cometabci.ExtendedCommitInfo
if err := extendedCommitInfo.Unmarshal(injectedVoteExtTx.ExtendedCommitInfo); err != nil {
h.logger.Error("failed to decode injected extended commit info", "err", err)
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, err
}

err := baseapp.ValidateVoteExtensions(
ctx,
h.stakingKeeper,
req.Height,
ctx.ChainID(),
extendedCommitInfo,
)
if err != nil {
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, err
}

// Verify the proposer's gas estimation by computing the same median.
gasEstimateMedian, err := h.generateMedianGasEstimate(ctx, extendedCommitInfo)
if err != nil {
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, err
}
if err := h.verifyMedianGasEstimation(*injectedVoteExtTx.MedianGasEstimation, gasEstimateMedian); err != nil {
return &cometabci.ResponseProcessProposal{Status: cometabci.ResponseProcessProposal_REJECT}, err
}
}

h.logger.Info(
"processed proposal",
"txs", len(req.Txs),
"vote_extensions_enabled", voteExtensionsEnabled,
)

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

func (h *ProposalHandler) generateMedianGasEstimate(
ctx sdk.Context,
ci cometabci.ExtendedCommitInfo,
) (median math.LegacyDec, err error) {
gasEstimates := make([]math.LegacyDec, 0)
for _, vote := range ci.Votes {
if vote.BlockIdFlag != cmtproto.BlockIDFlagCommit {
continue
}

var voteExt gmptypes.GmpVoteExtension
if err := voteExt.Unmarshal(vote.VoteExtension); err != nil {
h.logger.Error(
"failed to decode vote extension",
"err", err,
)
return math.LegacyZeroDec(), err
}

var valConsAddr sdk.ConsAddress
if err := valConsAddr.Unmarshal(vote.Validator.Address); err != nil {
h.logger.Error(
"failed to unmarshal validator consensus address",
"err", err,
)
return math.LegacyZeroDec(), err
}
val, err := h.stakingKeeper.GetValidatorByConsAddr(ctx, valConsAddr)
if err != nil {
h.logger.Error(
"failed to get consensus validator from staking keeper",
"err", err,
)
return math.LegacyZeroDec(), err
}
_, err = sdk.ValAddressFromBech32(val.OperatorAddress)
if err != nil {
return math.LegacyZeroDec(), err
}

// append median gas estimate to gas estimates
gasEstimates = append(gasEstimates, *voteExt.GasEstimation)
}

// calculate median of gas estimates
return calculateMedian(gasEstimates), nil
}

func (h *ProposalHandler) verifyMedianGasEstimation(
injectedEstimation math.LegacyDec,
generatedEstimation math.LegacyDec,
) error {
// if they're not the same, error
if injectedEstimation != generatedEstimation {
return fmt.Errorf("injected median gas estimation does not match generated median gas estimation")
}
return nil
}

func calculateMedian(values []math.LegacyDec) math.LegacyDec {
if len(values) == 0 {
return math.LegacyZeroDec()
}

// Create a copy of the slice to avoid modifying the original
sortedValues := make([]math.LegacyDec, len(values))
copy(sortedValues, values)

// Sort the copy in ascending order
sort.Slice(sortedValues, func(i, j int) bool {
return sortedValues[i].LT(sortedValues[j])
})

length := len(sortedValues)
mid := length / 2

if length%2 == 0 {
// If even, return the average of the two middle values
return sortedValues[mid-1].Add(sortedValues[mid]).QuoInt64(2)
} else {
// If odd, return the middle value
return sortedValues[mid]
}
}
23 changes: 23 additions & 0 deletions x/gmp/abci/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package abci

import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// VoteExtensionsEnabled determines if vote extensions are enabled for the current block.
func VoteExtensionsEnabled(ctx sdk.Context) bool {
cp := ctx.ConsensusParams()
if cp.Abci == nil || cp.Abci.VoteExtensionsEnableHeight == 0 {
return false
}

// Per the cosmos sdk, the first block should not utilize the latest finalize block state. This means
// vote extensions should NOT be making state changes.
//
// Ref: https://github.com/cosmos/cosmos-sdk/blob/2100a73dcea634ce914977dbddb4991a020ee345/baseapp/baseapp.go#L488-L495
if ctx.BlockHeight() <= 1 {
return false
}

return cp.Abci.VoteExtensionsEnableHeight < ctx.BlockHeight()
}
Loading

0 comments on commit 16f6d71

Please sign in to comment.