Skip to content
Merged
Show file tree
Hide file tree
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
19 changes: 19 additions & 0 deletions proto/oracle/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ service Msg {
// aggregate exchange rate vote
rpc AggregateExchangeRateVote(MsgAggregateExchangeRateVote) returns (MsgAggregateExchangeRateVoteResponse);

// Aggregate vote and prevote combines the functionality of prevote and vote into one RPC
rpc AggregateExchangeRateCombinedVote(MsgAggregateExchangeRateCombinedVote) returns (MsgAggregateExchangeRateCombinedVoteResponse);

// DelegateFeedConsent defines a method for setting the feeder delegation
rpc DelegateFeedConsent(MsgDelegateFeedConsent) returns (MsgDelegateFeedConsentResponse);
}
Expand Down Expand Up @@ -48,6 +51,22 @@ message MsgAggregateExchangeRateVote {
// MsgAggregateExchangeRateVoteResponse defines the Msg/AggregateExchangeRateVote response type.
message MsgAggregateExchangeRateVoteResponse {}

// MsgAggregateExchangeRateVote represents a message to submit
// aggregate exchange rate vote.
message MsgAggregateExchangeRateCombinedVote {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;

string vote_salt = 1 [(gogoproto.moretags) = "yaml:\"vote_salt\""];
string vote_exchange_rates = 2 [(gogoproto.moretags) = "yaml:\"vote_exchange_rates\""];
string prevote_hash = 3 [(gogoproto.moretags) = "yaml:\"prevote_hash\""];
string feeder = 4 [(gogoproto.moretags) = "yaml:\"feeder\""];
string validator = 5 [(gogoproto.moretags) = "yaml:\"validator\""];
}

// MsgAggregateExchangeRateVoteResponse defines the Msg/AggregateExchangeRateVote response type.
message MsgAggregateExchangeRateCombinedVoteResponse {}

// MsgDelegateFeedConsent represents a message to
// delegate oracle voting rights to another address.
message MsgDelegateFeedConsent {
Expand Down
40 changes: 40 additions & 0 deletions x/oracle/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,37 @@ func TestOraclePriceSnapshot(t *testing.T) {
require.Equal(t, expected2, input.OracleKeeper.GetPriceSnapshot(input.Ctx, 200))
}

func TestOracleCombinedVote(t *testing.T) {
input, h := setup(t)

input.Ctx = input.Ctx.WithBlockHeight(1)
input.OracleKeeper.SetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom, randomExchangeRate)

input.Ctx = input.Ctx.WithBlockHeight(2)
makeAggregateCombinedVote(t, input, h, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(1)}}, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(2)}}, 0)
makeAggregateCombinedVote(t, input, h, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(1)}}, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(2)}}, 1)
makeAggregateCombinedVote(t, input, h, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(1)}}, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(2)}}, 2)
oracle.EndBlocker(input.Ctx, input.OracleKeeper)

// we expect random exchange rate because the vote had no prevote
rate, height, err := input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom)
require.NoError(t, err)
require.Equal(t, randomExchangeRate, rate)
require.Equal(t, sdk.NewInt(1), height)

input.Ctx = input.Ctx.WithBlockHeight(3)
makeAggregateCombinedVote(t, input, h, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(2)}}, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(3)}}, 0)
makeAggregateCombinedVote(t, input, h, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(2)}}, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(3)}}, 1)
makeAggregateCombinedVote(t, input, h, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(2)}}, sdk.DecCoins{{Denom: utils.MicroAtomDenom, Amount: sdk.NewDec(3)}}, 2)
oracle.EndBlocker(input.Ctx, input.OracleKeeper)

// we expect exchange rate of 2 because the vote had a previous prevote
rate, height, err = input.OracleKeeper.GetBaseExchangeRate(input.Ctx, utils.MicroAtomDenom)
require.NoError(t, err)
require.Equal(t, sdk.NewDec(2), rate)
require.Equal(t, sdk.NewInt(3), height)
}

func makeAggregatePrevoteAndVote(t *testing.T, input keeper.TestInput, h sdk.Handler, height int64, rates sdk.DecCoins, idx int) {
// Account 1, SDR
salt := "1"
Expand All @@ -643,3 +674,12 @@ func makeAggregatePrevoteAndVote(t *testing.T, input keeper.TestInput, h sdk.Han
_, err = h(input.Ctx.WithBlockHeight(height+1), voteMsg)
require.NoError(t, err)
}

func makeAggregateCombinedVote(t *testing.T, input keeper.TestInput, h sdk.Handler, vote_rates sdk.DecCoins, prevote_rates sdk.DecCoins, idx int) {
salt := "1"

hash := types.GetAggregateVoteHash(salt, prevote_rates.String(), keeper.ValAddrs[idx])
voteMsg := types.NewMsgAggregateExchangeRateCombinedVote(salt, vote_rates.String(), hash, keeper.Addrs[idx], keeper.ValAddrs[idx])
_, err := h(input.Ctx, voteMsg)
require.NoError(t, err)
}
87 changes: 80 additions & 7 deletions x/oracle/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func GetTxCmd() *cobra.Command {
GetCmdDelegateFeederPermission(),
GetCmdAggregateExchangeRatePrevote(),
GetCmdAggregateExchangeRateVote(),
GetCmdAggregateExchangeRateCombinedVote(),
)

return oracleTxCmd
Expand Down Expand Up @@ -92,11 +93,11 @@ func GetCmdAggregateExchangeRatePrevote() *cobra.Command {
Short: "Submit an oracle aggregate prevote for the exchange rates of Luna",
Long: strings.TrimSpace(`
Submit an oracle aggregate prevote for the exchange rates of Luna denominated in multiple denoms.
The purpose of aggregate prevote is to hide aggregate exchange rate vote with hash which is formatted
The purpose of aggregate prevote is to hide aggregate exchange rate vote with hash which is formatted
as hex string in SHA256("{salt}:{exchange_rate}{denom},...,{exchange_rate}{denom}:{voter}")

# Aggregate Prevote
$ terrad tx oracle aggregate-prevote 1234 8888.0ukrw,1.243uusd,0.99usdr
$ terrad tx oracle aggregate-prevote 1234 8888.0ukrw,1.243uusd,0.99usdr

where "ukrw,uusd,usdr" is the denominating currencies, and "8888.0,1.243,0.99" is the exchange rates of micro Luna in micro denoms from the voter's point of view.

Expand Down Expand Up @@ -153,18 +154,18 @@ func GetCmdAggregateExchangeRateVote() *cobra.Command {
cmd := &cobra.Command{
Use: "aggregate-vote [salt] [exchange-rates] [validator]",
Args: cobra.RangeArgs(2, 3),
Short: "Submit an oracle aggregate vote for the exchange_rates of Luna",
Short: "Submit an oracle aggregate vote for the exchange_rates of the base denom",
Long: strings.TrimSpace(`
Submit a aggregate vote for the exchange_rates of Luna w.r.t the input denom. Companion to a prevote submitted in the previous vote period.
Submit a aggregate vote for the exchange_rates of the base denom w.r.t the input denom. Companion to a prevote submitted in the previous vote period.

$ terrad tx oracle aggregate-vote 1234 8888.0ukrw,1.243uusd,0.99usdr
$ seid tx oracle aggregate-vote 1234 8888.0ukrw,1.243uusd,0.99usdr

where "ukrw,uusd,usdr" is the denominating currencies, and "8888.0,1.243,0.99" is the exchange rates of micro Luna in micro denoms from the voter's point of view.

"salt" should match the salt used to generate the SHA256 hex in the aggregated pre-vote.
"salt" should match the salt used to generate the SHA256 hex in the aggregated pre-vote.

If voting from a voting delegate, set "validator" to the address of the validator to vote on behalf of:
$ terrad tx oracle aggregate-vote 1234 8888.0ukrw,1.243uusd,0.99usdr terravaloper1....
$ seid tx oracle aggregate-vote 1234 8888.0ukrw,1.243uusd,0.99usdr seivaloper1....
`),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
Expand Down Expand Up @@ -209,3 +210,75 @@ $ terrad tx oracle aggregate-vote 1234 8888.0ukrw,1.243uusd,0.99usdr terravalope

return cmd
}

// GetCmdAggregateExchangeRatePrevote will create a aggregateExchangeRatePrevote tx and sign it with the given key.
func GetCmdAggregateExchangeRateCombinedVote() *cobra.Command {
cmd := &cobra.Command{
Use: "aggregate-combined-vote [vote-salt] [vote-exchange-rates] [prevote-salt] [prevote-exchange-rates] [validator]",
Args: cobra.RangeArgs(4, 5),
Short: "Submit an oracle aggregate vote AND prevote for the exchange rates",
Long: strings.TrimSpace(`
Submit an oracle aggregate vote and prevote for the exchange rates of the base denom denominated in multiple denoms. The vote is a companian to a prevote from the previous vote window.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit - "while the prevote is for the current window"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the prevote is performed in the current window, but is for the vote in the next window


The purpose of aggregate prevote is to hide aggregate exchange rate vote with hash which is formatted
as hex string in SHA256("{salt}:{exchange_rate}{denom},...,{exchange_rate}{denom}:{voter}")

# Aggregate Combined Vote
$ seid tx oracle aggregate-combined-vote 1234 8888.0ukrw,1.243uusd,0.99usdr 3456 9999.0ukrw,1.111uusd,0.95usdr

where "ukrw,uusd,usdr" is the denominating currencies, and "8888.0,1.243,0.99" is the exchange rates of micro base denom in micro denoms from the voter's point of view.

If voting from a voting delegate, set "validator" to the address of the validator to vote on behalf of:
$ terrad tx oracle aggregate-combined vote 1234 8888.0ukrw,1.243uusd,0.99usdr 3456 9999.0ukrw,1.111uusd,0.95usdr seivaloper1...
`),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

voteSalt := args[0]
voteExchangeRatesStr := args[1]
prevoteSalt := args[2]
prevoteExchangeRatesStr := args[3]
_, err = types.ParseExchangeRateTuples(voteExchangeRatesStr)
if err != nil {
return fmt.Errorf("given vote exchange_rates {%s} is not a valid format; exchange_rate should be formatted as DecCoins; %s", voteExchangeRatesStr, err.Error())
}

_, err = types.ParseExchangeRateTuples(prevoteExchangeRatesStr)
if err != nil {
return fmt.Errorf("given prevote exchange_rates {%s} is not a valid format; exchange_rate should be formatted as DecCoins; %s", prevoteExchangeRatesStr, err.Error())
}

// Get from address
voter := clientCtx.GetFromAddress()

// By default the voter is voting on behalf of itself
validator := sdk.ValAddress(voter)

// Override validator if validator is given
if len(args) == 5 {
parsedVal, err := sdk.ValAddressFromBech32(args[4])
if err != nil {
return errors.Wrap(err, "validator address is invalid")
}
validator = parsedVal
}

hash := types.GetAggregateVoteHash(prevoteSalt, prevoteExchangeRatesStr, validator)
msgs := []sdk.Msg{types.NewMsgAggregateExchangeRateCombinedVote(voteSalt, voteExchangeRatesStr, hash, voter, validator)}
for _, msg := range msgs {
if err := msg.ValidateBasic(); err != nil {
return err
}
}

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msgs...)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}
71 changes: 71 additions & 0 deletions x/oracle/client/rest/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func registerTxHandlers(cliCtx client.Context, rtr *mux.Router) {
rtr.HandleFunc(fmt.Sprintf("/oracle/voters/{%s}/feeder", RestVoter), newDelegateHandlerFunction(cliCtx)).Methods("POST")
rtr.HandleFunc(fmt.Sprintf("/oracle/voters/{%s}/aggregate_prevote", RestVoter), newAggregatePrevoteHandlerFunction(cliCtx)).Methods("POST")
rtr.HandleFunc(fmt.Sprintf("/oracle/voters/{%s}/aggregate_vote", RestVoter), newAggregateVoteHandlerFunction(cliCtx)).Methods("POST")
rtr.HandleFunc(fmt.Sprintf("/oracle/voters/{%s}/aggregate_combined_vote", RestVoter), newAggregateCombinedVoteHandlerFunction(cliCtx)).Methods("POST")
}

type (
Expand All @@ -40,6 +41,16 @@ type (
ExchangeRates string `json:"exchange_rates" yaml:"exchange_rates"`
Salt string `json:"salt" yaml:"salt"`
}

aggregateCombinedVoteReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`

VoteExchangeRates string `json:"vote_exchange_rates" yaml:"vote_exchange_rates"`
VoteSalt string `json:"vote_salt" yaml:"vote_salt"`
PrevoteHash string `json:"prevote_hash" yaml:"prevote_hash"`
PrevoteExchangeRates string `json:"prevote_exchange_rates" yaml:"prevote_exchange_rates"`
PrevoteSalt string `json:"prevote_salt" yaml:"prevote_salt"`
}
)

func newDelegateHandlerFunction(clientCtx client.Context) http.HandlerFunc {
Expand Down Expand Up @@ -161,3 +172,63 @@ func newAggregateVoteHandlerFunction(clientCtx client.Context) http.HandlerFunc
tx.WriteGeneratedTxResponse(clientCtx, w, req.BaseReq, msg)
}
}

func newAggregateCombinedVoteHandlerFunction(clientCtx client.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req aggregateCombinedVoteReq
if !rest.ReadRESTReq(w, r, clientCtx.LegacyAmino, &req) {
return
}

req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}

feederAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From)
if rest.CheckBadRequestError(w, err) {
return
}

voterAddr, ok := checkVoterAddressVar(w, r)
if !ok {
return
}

// Check validation of tuples
_, err = types.ParseExchangeRateTuples(req.VoteExchangeRates)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}

var prevoteHash types.AggregateVoteHash

// If hash is not given, then retrieve hash from exchange_rate and salt
if len(req.PrevoteHash) == 0 && (len(req.PrevoteExchangeRates) > 0 && len(req.PrevoteSalt) > 0) {
_, err := types.ParseExchangeRateTuples(req.PrevoteExchangeRates)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}

prevoteHash = types.GetAggregateVoteHash(req.PrevoteSalt, req.PrevoteExchangeRates, voterAddr)
} else if len(req.PrevoteHash) > 0 {
prevoteHash, err = types.AggregateVoteHashFromHexString(req.PrevoteHash)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
} else {
rest.WriteErrorResponse(w, http.StatusBadRequest, "must provide Hash or (ExchangeRates & Salt)")
return
}

msg := types.NewMsgAggregateExchangeRateCombinedVote(req.VoteSalt, req.VoteExchangeRates, prevoteHash, feederAddr, voterAddr)
if rest.CheckBadRequestError(w, msg.ValidateBasic()) {
return
}

tx.WriteGeneratedTxResponse(clientCtx, w, req.BaseReq, msg)
}
}
3 changes: 3 additions & 0 deletions x/oracle/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ func NewHandler(k keeper.Keeper) sdk.Handler {
case *types.MsgAggregateExchangeRateVote:
res, err := msgServer.AggregateExchangeRateVote(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
case *types.MsgAggregateExchangeRateCombinedVote:
res, err := msgServer.AggregateExchangeRateCombinedVote(sdk.WrapSDKContext(ctx), msg)
return sdk.WrapServiceResult(ctx, res, err)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized oracle message type: %T", msg)
}
Expand Down
9 changes: 9 additions & 0 deletions x/oracle/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,3 +543,12 @@ func (k Keeper) ValidateLookbackSeconds(ctx sdk.Context, lookbackSeconds int64)

return nil
}

func (k Keeper) IsPrevoteFromPreviousWindow(ctx sdk.Context, valAddr sdk.ValAddress) bool {
votePeriod := k.VotePeriod(ctx)
prevote, err := k.GetAggregateExchangeRatePrevote(ctx, valAddr)
if err != nil {
return false
}
return (uint64(ctx.BlockHeight())/votePeriod)-(prevote.SubmitBlock/votePeriod) == 1
}
47 changes: 44 additions & 3 deletions x/oracle/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,13 @@ func (ms msgServer) AggregateExchangeRateVote(goCtx context.Context, msg *types.
return nil, err
}

params := ms.GetParams(ctx)

aggregatePrevote, err := ms.GetAggregateExchangeRatePrevote(ctx, valAddr)
if err != nil {
return nil, sdkerrors.Wrap(types.ErrNoAggregatePrevote, msg.Validator)
}

// Check a msg is submitted proper period
if (uint64(ctx.BlockHeight())/params.VotePeriod)-(aggregatePrevote.SubmitBlock/params.VotePeriod) != 1 {
if !ms.IsPrevoteFromPreviousWindow(ctx, valAddr) {
return nil, types.ErrRevealPeriodMissMatch
}

Expand Down Expand Up @@ -128,6 +126,49 @@ func (ms msgServer) AggregateExchangeRateVote(goCtx context.Context, msg *types.
return &types.MsgAggregateExchangeRateVoteResponse{}, nil
}

func (ms msgServer) AggregateExchangeRateCombinedVote(goCtx context.Context, msg *types.MsgAggregateExchangeRateCombinedVote) (*types.MsgAggregateExchangeRateCombinedVoteResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
valAddr, err := sdk.ValAddressFromBech32(msg.Validator)
if err != nil {
return nil, err
}

var voteErr error
// if there isn't a prevote, we want to no-op the vote so we don't get an error
// this way, it is safe to use combined vote regardless of a missed vote window
if err == nil && ms.IsPrevoteFromPreviousWindow(ctx, valAddr) {
_, voteErr = ms.AggregateExchangeRateVote(goCtx, msg.GetVoteFromCombinedVote())
}

_, prevoteErr := ms.AggregateExchangeRatePrevote(goCtx, msg.GetPrevoteFromCombinedVote())

if voteErr != nil {
return nil, voteErr
}
if prevoteErr != nil {
return nil, prevoteErr
}

ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeAggregateVote,
sdk.NewAttribute(types.AttributeKeyVoter, msg.Validator),
sdk.NewAttribute(types.AttributeKeyExchangeRates, msg.VoteExchangeRates),
),
sdk.NewEvent(
types.EventTypeAggregatePrevote,
sdk.NewAttribute(types.AttributeKeyVoter, msg.Validator),
),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Feeder),
),
})

return &types.MsgAggregateExchangeRateCombinedVoteResponse{}, nil
}

func (ms msgServer) DelegateFeedConsent(goCtx context.Context, msg *types.MsgDelegateFeedConsent) (*types.MsgDelegateFeedConsentResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

Expand Down
Loading