Skip to content

Commit

Permalink
introduce slashing for oracle service (#224)
Browse files Browse the repository at this point in the history
* introduce slashing for oracle service

* add slashing info for genesis, export/init

* Update x/oracle/types/events.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update x/oracle/types/genesis_test.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix typos, duplicate imports

* typos

* lint

* lint, optimize

* lint

* Update x/oracle/keeper/slashing.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix nil worker

* perf: return immediately before loop ends

* refactor(oracle)

* fix(oracle): dont save same value multiple times

* typos, int64->uint64

* typo

* fix(oracle):keep nonce, filter for slashing calculation

* lint

---------

Co-authored-by: X <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 21, 2024
1 parent 8ada91b commit 7b25aa3
Show file tree
Hide file tree
Showing 29 changed files with 2,310 additions and 110 deletions.
1 change: 1 addition & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ func NewExocoreApp(
appCodec, keys[oracleTypes.StoreKey], memKeys[oracleTypes.MemStoreKey],
app.GetSubspace(oracleTypes.ModuleName), app.StakingKeeper,
&app.DelegationKeeper, &app.AssetsKeeper, authAddrString,
&app.SlashingKeeper,
)

// the SDK slashing module is used to slash validators in the case of downtime. it tracks
Expand Down
29 changes: 28 additions & 1 deletion proto/exocore/oracle/v1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "exocore/oracle/v1/params.proto";
import "exocore/oracle/v1/prices.proto";
import "exocore/oracle/v1/recent_msg.proto";
import "exocore/oracle/v1/recent_params.proto";
import "exocore/oracle/v1/slashing.proto";
import "exocore/oracle/v1/validator_update_block.proto";
import "gogoproto/gogo.proto";

Expand All @@ -18,10 +19,12 @@ option go_package = "github.com/ExocoreNetwork/exocore/x/oracle/types";
message GenesisState {
// module params
Params params = 1 [(gogoproto.nullable) = false];
// prices of all tokens

// prices of all tokens including NST
repeated Prices prices_list = 2 [(gogoproto.nullable) = false];

//TODO: userDefinedTokenFeeder
// information for memory-cache recovery
// latest block on which the validator set be updated
ValidatorUpdateBlock validator_update_block = 3;
// index for the cached recent params
Expand All @@ -32,10 +35,18 @@ message GenesisState {
repeated RecentMsg recent_msg_list = 6[(gogoproto.nullable) = false];
// cached recent params
repeated RecentParams recent_params_list = 7[(gogoproto.nullable) = false];

// information for NST related
// stakerInfos for each nst token
repeated StakerInfosAssets staker_infos_assets = 8[(gogoproto.nullable) = false];
// stakerList for each nst token
repeated StakerListAssets staker_list_assets = 9[(gogoproto.nullable) = false];

// information for slashing history
// ValidatorReportInfo records all the validatorReportInfos
repeated ValidatorReportInfo validator_report_infos = 10[(gogoproto.nullable)=false];
// ValidatorMissedRounds records missedRounds for all validators seen
repeated ValidatorMissedRounds validator_missed_rounds = 11[(gogoproto.nullable)=false];
}

// stakerInfosAssets bond stakerinfos to their related assets id
Expand All @@ -53,3 +64,19 @@ message StakerListAssets {
// stakerList
StakerList staker_list = 2;
}

// ValidatorMissedRounds record missed rounds indexes for a validator which consAddr corresponding to the address
message ValidatorMissedRounds {
// address of validator
string address = 1;
// missed_rounds tells how many rounds this validtor had missed for current windo
repeated MissedRound missed_rounds = 2;
}

// MissedRound records if round with index is missed
message MissedRound {
// index of the round in current window
uint64 index = 1;
// if this round is missed
bool missed = 2;
}
41 changes: 38 additions & 3 deletions proto/exocore/oracle/v1/params.proto
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
syntax = "proto3";
package exocore.oracle.v1;

import "amino/amino.proto";
import "exocore/oracle/v1/info.proto";
import "exocore/oracle/v1/token_feeder.proto";
import "gogoproto/gogo.proto";

import "google/protobuf/duration.proto";
option go_package = "github.com/ExocoreNetwork/exocore/x/oracle/types";

// Params defines the parameters for the module.
Expand All @@ -28,10 +29,13 @@ message Params {
int32 threshold_b = 8;
// for v1, mode=1, get final price as soon as voting power reach threshold_a/threshold_b
ConsensusMode mode = 9;
// for each round, a validator only allowed to provide at most max_det_id continuos rounds of prices for DS
// for each round, a validator only allowed to provide at most max_det_id continuous rounds of prices for DS
int32 max_det_id = 10;
// for each token, only keep max_size_prices round of prices
int32 max_size_prices = 11;

// slashing defines the slashing related params
SlashingParams slashing = 12;
}

// ConsensusMode defines the consensus mode for the prices.
Expand All @@ -42,4 +46,35 @@ enum ConsensusMode {
// CONSENSUS_MODE_ASAP defines the mode to get final price immediately when the voting power
// exceeds the threshold.
CONSENSUS_MODE_ASAP = 1 [(gogoproto.enumvalue_customname) = "ConsensusModeASAP"];
}
}

// slashing related params
message SlashingParams {
// reported_rounds_window defines how many rounds included in one window for performance review of missing report
int64 reported_rounds_window = 1;
// min_reported_perwindow defines at least how many rounds should be reported, this is a percentage of window
bytes min_reported_per_window = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];
// oracle_miss_jail_duration defines the duration one validator should be jailed for missing reporting price
google.protobuf.Duration oracle_miss_jail_duration = 3
[(gogoproto.nullable) = false, (amino.dont_omitempty) = true, (gogoproto.stdduration) = true];
// oracle_malicious_jail_duration defines the duratin one validator should be jailed for malicious behavior
google.protobuf.Duration oracle_malicious_jail_duration =4
[(gogoproto.nullable) = false, (amino.dont_omitempty) = true, (gogoproto.stdduration) = true];
// slash_fraction_miss defines the fraction one validator should be punished for msissing reporting price
bytes slash_fraction_miss = 5 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];
// slash_fraction_miss defines the fraction one validator should be punished for malicious behavior
bytes slash_fraction_malicious = 6 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];

}
17 changes: 17 additions & 0 deletions proto/exocore/oracle/v1/slashing.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
syntax = "proto3";

package exocore.oracle.v1;

option go_package = "github.com/ExocoreNetwork/exocore/x/oracle/types";

// ValidatorReportInfo represents the information to describe the miss status of a validator reporting prices
message ValidatorReportInfo {
// address of the validtor
string address = 1;
// start_height for the performance round of the configured window of rounds
int64 start_height = 2;
// index_offset track the offset of current window
int64 index_offset = 3;
// missed_rounds_counter counts the number of missed rounds for this window
int64 missed_rounds_counter = 4;
}
2 changes: 2 additions & 0 deletions testutil/keeper/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
assetskeeper "github.com/ExocoreNetwork/exocore/x/assets/keeper"
delegationkeeper "github.com/ExocoreNetwork/exocore/x/delegation/keeper"
dogfoodkeeper "github.com/ExocoreNetwork/exocore/x/dogfood/keeper"
slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -52,6 +53,7 @@ func OracleKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
delegationkeeper.Keeper{},
assetskeeper.Keeper{},
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
slashingkeeper.Keeper{},
)

ctx := sdk.NewContext(stateStore, tmproto.Header{
Expand Down
1 change: 1 addition & 0 deletions x/delegation/keeper/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func (k *Keeper) EndBlock(
continue
}

// TODO: the field IsPending in types.UndelegationRecord is useless since when a record is completed it will be removed, so the record is either existing&pending or unexist&completed, and the IsPending is not used nowhere(like slashFromUndelegation doesn't check this field either), good to remove this field. And types.UndelegationRecord is actually PendingUndelegationRecord
// delete the Undelegation records that have been complemented
err = k.DeleteUndelegationRecord(cc, record)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions x/delegation/keeper/delegation_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ func (k *Keeper) SetStakerShareToZero(ctx sdk.Context, operator, assetID string,
singleStateKey := assetstype.GetJoinedStoreKey(stakerID, assetID, operator)
value := store.Get(singleStateKey)
if value != nil {
// TODO: check if pendingUndelegation==0 => just delete this item instead of update share to zero, otherwise this item will be left in the storage forever with zero value
delegationState := delegationtype.DelegationAmounts{}
k.cdc.MustUnmarshal(value, &delegationState)
delegationState.UndelegatableShare = sdkmath.LegacyZeroDec()
Expand Down
3 changes: 2 additions & 1 deletion x/operator/keeper/slash.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func GetSlashIDForDogfood(infraction stakingtypes.Infraction, infractionHeight i
return strings.Join([]string{hexutil.EncodeUint64(uint64(infraction)), hexutil.EncodeUint64(uint64(infractionHeight))}, utils.DelimiterForID)
}

// SlashFromUndelegation executes the slash from an undelegation
// SlashFromUndelegation executes the slash from an undelegation, reduce the .ActualCompletedAmount from undelegationRecords
func SlashFromUndelegation(undelegation *delegationtype.UndelegationRecord, slashProportion sdkmath.LegacyDec) *types.SlashFromUndelegation {
if undelegation.ActualCompletedAmount.IsZero() {
return nil
Expand Down Expand Up @@ -135,6 +135,7 @@ func (k *Keeper) SlashAssets(ctx sdk.Context, snapshotHeight int64, parameter *t
state.OperatorShare = sdkmath.LegacyZeroDec()
}
state.TotalAmount = remainingAmount
// TODO: check if pendingUndelegation also zero => delete this item, and this operator should be opted out if all aasets falls to 0 since the miniself is not satisfied then.
executionInfo.SlashAssetsPool = append(executionInfo.SlashAssetsPool, types.SlashFromAssetsPool{
AssetID: assetID,
Amount: slashAmount,
Expand Down
1 change: 0 additions & 1 deletion x/operator/keeper/usd_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,6 @@ func (k *Keeper) CalculateUSDValueForOperator(
}
// iterate all assets owned by the operator to calculate its voting power
opFuncToIterateAssets := func(assetID string, state *assetstype.OperatorAssetInfo) error {
// var price operatortypes.Price
var price oracletype.Price
var decimal uint32
if isForSlash {
Expand Down
32 changes: 31 additions & 1 deletion x/oracle/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,30 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState)
for _, elem := range genState.StakerInfosAssets {
k.SetStakerInfos(ctx, elem.AssetId, elem.StakerInfos)
}
// set validatorReportInfos
for _, elem := range genState.ValidatorReportInfos {
k.SetValidatorReportInfo(ctx, elem.Address, elem)
}
// set validatorMissedRounds
for _, elem := range genState.ValidatorMissedRounds {
for _, missedRound := range elem.MissedRounds {
k.SetValidatorMissedRoundBitArray(ctx, elem.Address, missedRound.Index, missedRound.Missed)
}
}
// this line is used by starport scaffolding # genesis/module/init
k.SetParams(ctx, genState.Params)
}

// ExportGenesis returns the module's exported genesis
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState {
genesis := types.DefaultGenesis()
// params
genesis.Params = k.GetParams(ctx)

// priceList
genesis.PricesList = k.GetAllPrices(ctx)

// cache recovery related, used by agc
// Get all validatorUpdateBlock
validatorUpdateBlock, found := k.GetValidatorUpdateBlock(ctx)
if found {
Expand All @@ -67,9 +81,25 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState {
}
genesis.RecentMsgList = k.GetAllRecentMsg(ctx)
genesis.RecentParamsList = k.GetAllRecentParams(ctx)
// TODO: export stakerListAssets, and stakerInfosAssets

// NST related
genesis.StakerInfosAssets = k.GetAllStakerInfosAssets(ctx)
genesis.StakerListAssets = k.GetAllStakerListAssets(ctx)

// slashing related
reportInfos := make([]types.ValidatorReportInfo, 0)
validatorMissedRounds := make([]types.ValidatorMissedRounds, 0)
k.IterateValidatorReportInfos(ctx, func(validator string, reportInfo types.ValidatorReportInfo) bool {
reportInfos = append(reportInfos, reportInfo)
missedRounds := k.GetValidatorMissedRounds(ctx, validator)
validatorMissedRounds = append(validatorMissedRounds, types.ValidatorMissedRounds{
Address: validator,
MissedRounds: missedRounds,
})
return false
})
genesis.ValidatorReportInfos = reportInfos
genesis.ValidatorMissedRounds = validatorMissedRounds
// this line is used by starport scaffolding # genesis/module/export

return genesis
Expand Down
35 changes: 34 additions & 1 deletion x/oracle/keeper/aggregator/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package aggregator

import (
"math/big"
"sort"

"github.com/ExocoreNetwork/exocore/x/oracle/keeper/common"
"github.com/ExocoreNetwork/exocore/x/oracle/types"
Expand Down Expand Up @@ -124,6 +125,7 @@ func (agg *aggregator) fillPrice(pSources []*types.PriceSource, validator string
pTR.price = new(big.Int).Set(priceTmp.price)
pTR.detRoundID = priceTmp.detRoundID
pTR.timestamp = priceTmp.timestamp
break
}
}
}
Expand Down Expand Up @@ -151,7 +153,7 @@ func (agg *aggregator) confirmDSPrice(confirmedRounds []*confirmedPrice) {
price.detRoundID = priceSourceRound.detID
price.timestamp = priceSourceRound.timestamp
price.price = priceSourceRound.price
} // else TODO: panice in V1
} // else TODO: panic in V1
}
}
}
Expand Down Expand Up @@ -191,6 +193,37 @@ func (agg *aggregator) aggregate() *big.Int {
return agg.finalPrice
}

// TODO: this only suites for DS. check source type for extension
// GetFinaPriceListForFeederIDs retrieve final price info as an array ordered by sourceID asc
func (agg *aggregator) getFinalPriceList(feederID uint64) []*types.AggFinalPrice {
sourceIDs := make([]uint64, 0, len(agg.dsPrices))
for sID := range agg.dsPrices {
sourceIDs = append(sourceIDs, sID)
}
sort.Slice(sourceIDs, func(i, j int) bool {
return sourceIDs[i] < sourceIDs[j]
})
ret := make([]*types.AggFinalPrice, 0, len(sourceIDs))
for _, sID := range sourceIDs {
for _, report := range agg.reports {
price := report.prices[sID]
if price == nil || price.detRoundID != agg.dsPrices[sID] {
// the DetID mismatch should not happen
continue
}
ret = append(ret, &types.AggFinalPrice{
FeederID: feederID,
SourceID: sID,
DetID: price.detRoundID,
Price: price.price.String(),
})
// {feederID, sourceID} has been found, skip rest reports
break
}
}
return ret
}

func newAggregator(validatorSetLength int, totalPower *big.Int) *aggregator {
return &aggregator{
reports: make([]*reportPrice, 0, validatorSetLength),
Expand Down
1 change: 0 additions & 1 deletion x/oracle/keeper/aggregator/calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ func (r *roundPrices) updatePriceAndPower(pw *priceAndPower, totalPower *big.Int
updated = true
if common.ExceedsThreshold(pw.power, totalPower) {
r.price = pw.price
// r.confirmed = true
confirmed = true
}
}
Expand Down
Loading

0 comments on commit 7b25aa3

Please sign in to comment.