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

Soft opt out #833

Merged
merged 32 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
584b3f2
WIP soft opt out code with incomplete boilerplate
jtremback Apr 7, 2023
01ce4d4
proto changes
shaspitz Apr 7, 2023
a5880e3
Seems like it should work
jtremback Apr 7, 2023
19a31c8
Unit test for UpdateLargestSoftOptOutValidatorPower
jtremback Apr 7, 2023
a1da5e3
fixes and renames, unit tests work
jtremback Apr 7, 2023
ed09b43
update comment
shaspitz Apr 10, 2023
353c8db
log
shaspitz Apr 10, 2023
282716a
Update proto/interchain_security/ccv/consumer/v1/consumer.proto
shaspitz Apr 11, 2023
fef5415
better validation for soft opt out threshhold
shaspitz Apr 11, 2023
0d5b248
improve test
shaspitz Apr 11, 2023
3506858
slicestable
shaspitz Apr 11, 2023
66ee828
semantics and improved test
shaspitz Apr 11, 2023
421bee2
use correct key util
shaspitz Apr 11, 2023
9b6c50a
Merge branch 'main' into soft-opt-out
shaspitz Apr 11, 2023
edac41a
Update module.go
shaspitz Apr 11, 2023
df7ddaf
comment
shaspitz Apr 11, 2023
059c750
updated semantics
shaspitz Apr 11, 2023
f862642
separate files
shaspitz Apr 11, 2023
b4ede6b
fix TestMakeConsumerGenesis test
shaspitz Apr 11, 2023
5b86af8
fix naming
shaspitz Apr 11, 2023
171a573
change upper bound on soft opt out thresh
shaspitz Apr 12, 2023
b7b35aa
fix test
shaspitz Apr 12, 2023
741da8a
allow empty valset for tests
shaspitz Apr 12, 2023
59910f6
Merge branch 'main' into soft-opt-out
shaspitz Apr 12, 2023
65bf8da
gofumpt and fix from merge
shaspitz Apr 12, 2023
6bfb60e
Merge branch 'main' into soft-opt-out
mpoke Apr 13, 2023
16180be
Update x/ccv/consumer/types/params_test.go
mpoke Apr 13, 2023
d495656
Update x/ccv/consumer/types/params.go
mpoke Apr 13, 2023
2d60b2f
Soft opt out diff tests (#847)
shaspitz Apr 13, 2023
d686044
add comment about beginblocker order requirement for soft opt-out
sainoe Apr 13, 2023
d0bff24
Merge branch 'main' into soft-opt-out
sainoe Apr 13, 2023
8d2a613
Merge branch 'main' into soft-opt-out
sainoe Apr 13, 2023
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
1 change: 1 addition & 0 deletions app/consumer-democracy/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ func New(
// CanWithdrawInvariant invariant.
// NOTE: staking module is required if HistoricalEntries param > 0
// NOTE: capability module's beginblocker must come before any modules using capabilities (e.g. IBC)
// NOTE: the soft opt-out requires that the consumer module's beginblocker comes after the slashing module's beginblocker
app.MM.SetOrderBeginBlockers(
// upgrades should be run first
upgradetypes.ModuleName,
Expand Down
1 change: 1 addition & 0 deletions app/consumer/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ func New(
// NOTE: Capability module must occur first so that it can initialize any capabilities
// so that other modules that want to create or claim capabilities afterwards in InitChain
// can do so safely.
// NOTE: the soft opt-out requires that the consumer module's beginblocker comes after the slashing module's beginblocker
app.MM.SetOrderInitGenesis(
capabilitytypes.ModuleName,
authtypes.ModuleName,
Expand Down
5 changes: 5 additions & 0 deletions proto/interchain_security/ccv/consumer/v1/consumer.proto
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ message Params {
// which should be smaller than that of the provider in general.
google.protobuf.Duration unbonding_period = 9
[(gogoproto.nullable) = false, (gogoproto.stdduration) = true];

// The threshold for the percentage of validators at the bottom of the set who
// can opt out of running the consumer chain without being punished. For example, a
// value of 0.05 means that the validators in the bottom 5% of the set can opt out
string soft_opt_out_threshold = 10;
}

// LastTransmissionBlockHeight is the last time validator holding
Expand Down
1 change: 1 addition & 0 deletions tests/difference/core/driver/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ func (b *Builder) createConsumerGenesis(client *ibctmtypes.ClientState) *consume
consumertypes.DefaultConsumerRedistributeFrac,
consumertypes.DefaultHistoricalEntries,
b.initState.UnbondingC,
"0", // disable soft opt-out
)
return consumertypes.NewInitialGenesisState(client, providerConsState, valUpdates, params)
}
Expand Down
2 changes: 1 addition & 1 deletion tests/difference/core/driver/traces.json

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions tests/difference/core/model/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,50 @@ class CCVProvider {
return;
}

//
// Soft opt out logic
//

// Sort token powers from lowest to highest
const tokens = this.m.staking.tokens;
const sortedTokens = Object.values(tokens).sort((a, b) => a - b);

// Get total power (token is 1:1 to power)
let totalPower = 0;
sortedTokens.forEach((token, _) => {
totalPower += token;
});

let smallestNonOptOutPower = -1;

// Soft opt out threshold is set as 0 as for now soft opt-out is disabled.
// See createConsumerGenesis() in diff test setup.go
const softOptOutThreshold = 0;

if (softOptOutThreshold == 0) {
smallestNonOptOutPower = 0
} else {
// get power of the smallest validator that cannot soft opt out
let powerSum = 0;

for (let i = 0; i < sortedTokens.length; i++) {
powerSum += sortedTokens[i];
if (powerSum / totalPower > softOptOutThreshold) {
smallestNonOptOutPower = sortedTokens[i];
break;
}
}
}

if (smallestNonOptOutPower == -1) {
throw new Error('control flow should not reach here');
}

if (this.m.staking.tokens[data.val] < smallestNonOptOutPower) {
// soft opt out if validator power is smaller than smallest power which needs to be up
return;
}

this.m.events.push(Event.RECEIVE_DOWNTIME_SLASH_REQUEST);


Expand Down
9 changes: 9 additions & 0 deletions testutil/crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ func NewCryptoIdentityFromIntSeed(i int) *CryptoIdentity {
return NewCryptoIdentityFromBytesSeed(seed)
}

// GenMultipleCryptoIds generates and returns multiple CryptoIdentities from a starting int seed.
func GenMultipleCryptoIds(num int, fromIntSeed int) []*CryptoIdentity {
ids := make([]*CryptoIdentity, num)
for i := 0; i < num; i++ {
ids[i] = NewCryptoIdentityFromIntSeed(fromIntSeed + i)
}
return ids
}

func (v *CryptoIdentity) TMValidator(power int64) *tmtypes.Validator {
return tmtypes.NewValidator(v.TMCryptoPubKey(), power)
}
Expand Down
9 changes: 9 additions & 0 deletions x/ccv/consumer/keeper/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func (k Keeper) GetParams(ctx sdk.Context) types.Params {
k.GetConsumerRedistributionFrac(ctx),
k.GetHistoricalEntries(ctx),
k.GetUnbondingPeriod(ctx),
k.GetSoftOptOutThreshold(ctx),
)
}

Expand Down Expand Up @@ -106,3 +107,11 @@ func (k Keeper) GetUnbondingPeriod(ctx sdk.Context) time.Duration {
k.paramStore.Get(ctx, types.KeyConsumerUnbondingPeriod, &period)
return period
}

// GetSoftOptOutThreshold returns the percentage of validators at the bottom of the set
// that can opt out of running the consumer chain
func (k Keeper) GetSoftOptOutThreshold(ctx sdk.Context) string {
var str string
k.paramStore.Get(ctx, types.KeySoftOptOutThreshold, &str)
return str
}
3 changes: 2 additions & 1 deletion x/ccv/consumer/keeper/params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ func TestParams(t *testing.T) {
types.DefaultConsumerRedistributeFrac,
types.DefaultHistoricalEntries,
types.DefaultConsumerUnbondingPeriod,
types.DefaultSoftOptOutThreshold,
) // these are the default params, IBC suite independently sets enabled=true

params := consumerKeeper.GetParams(ctx)
require.Equal(t, expParams, params)

newParams := types.NewParams(false, 1000,
"channel-2", "cosmos19pe9pg5dv9k5fzgzmsrgnw9rl9asf7ddwhu7lm",
7*24*time.Hour, 25*time.Hour, "0.5", 500, 24*21*time.Hour)
7*24*time.Hour, 25*time.Hour, "0.5", 500, 24*21*time.Hour, "0.05")
consumerKeeper.SetParams(ctx, newParams)
params = consumerKeeper.GetParams(ctx)
require.Equal(t, newParams, params)
Expand Down
74 changes: 74 additions & 0 deletions x/ccv/consumer/keeper/soft_opt_out.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package keeper

import (
"encoding/binary"
"sort"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/interchain-security/x/ccv/consumer/types"
)

// SetSmallestNonOptOutPower sets the smallest validator power that cannot soft opt out.
func (k Keeper) SetSmallestNonOptOutPower(ctx sdk.Context, power uint64) {
store := ctx.KVStore(k.storeKey)
store.Set(types.SmallestNonOptOutPowerKey(), sdk.Uint64ToBigEndian(power))
}

// UpdateSmallestNonOptOutPower updates the smallest validator power that cannot soft opt out.
// This is the smallest validator power such that the sum of the power of all validators with a lower power
// is less than [SoftOptOutThreshold] of the total power of all validators.
func (k Keeper) UpdateSmallestNonOptOutPower(ctx sdk.Context) {
// get soft opt-out threshold
optOutThreshold := sdk.MustNewDecFromStr(k.GetSoftOptOutThreshold(ctx))
if optOutThreshold.IsZero() {
// If the SoftOptOutThreshold is zero, then soft opt-out is disable.
// Setting the smallest non-opt-out power to zero, fixes the diff-testing
// when soft opt-out is disable.
k.SetSmallestNonOptOutPower(ctx, uint64(0))
return
}

// get all validators
valset := k.GetAllCCValidator(ctx)

// Valset should only be empty for hacky tests. Log error in case this ever happens in prod.
if len(valset) == 0 {
k.Logger(ctx).Error("UpdateSoftOptOutThresholdPower called with empty validator set")
return
}

// sort validators by power ascending
shaspitz marked this conversation as resolved.
Show resolved Hide resolved
sort.SliceStable(valset, func(i, j int) bool {
return valset[i].Power < valset[j].Power
})

// get total power in set
totalPower := sdk.ZeroDec()
for _, val := range valset {
totalPower = totalPower.Add(sdk.NewDecFromInt(sdk.NewInt(val.Power)))
}

// get power of the smallest validator that cannot soft opt out
powerSum := sdk.ZeroDec()
for _, val := range valset {
powerSum = powerSum.Add(sdk.NewDecFromInt(sdk.NewInt(val.Power)))
// if powerSum / totalPower > SoftOptOutThreshold
if powerSum.Quo(totalPower).GT(optOutThreshold) {
// set smallest non opt out power
k.SetSmallestNonOptOutPower(ctx, uint64(val.Power))
k.Logger(ctx).Info("smallest non opt out power updated", "power", val.Power)
return
}
}
panic("UpdateSoftOptOutThresholdPower should not reach this point. Incorrect logic!")
}

// GetSmallestNonOptOutPower returns the smallest validator power that cannot soft opt out.
func (k Keeper) GetSmallestNonOptOutPower(ctx sdk.Context) int64 {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.SmallestNonOptOutPowerKey())
if bz == nil {
return 0
}
return int64(binary.BigEndian.Uint64(bz))
}
118 changes: 118 additions & 0 deletions x/ccv/consumer/keeper/soft_opt_out_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package keeper_test

import (
"testing"

"github.com/cosmos/interchain-security/testutil/crypto"
testkeeper "github.com/cosmos/interchain-security/testutil/keeper"
"github.com/cosmos/interchain-security/x/ccv/consumer/types"
"github.com/stretchr/testify/require"
tmtypes "github.com/tendermint/tendermint/types"
)

// Tests that UpdateSmallestNonOptOutPower updates the smallest validator power that cannot soft opt out.
// Soft opt out allows the bottom [SoftOptOutThreshold] portion of validators in the set to opt out.
// UpdateSmallestNonOptOutPower should update the smallest validator power that cannot opt out.
func TestUpdateSmallestNonOptOutPower(t *testing.T) {
cIds := crypto.GenMultipleCryptoIds(7, 682934679238)

testCases := []struct {
name string
// soft opt out threshold set as param
optOutThresh string
// validators to set in store
validators []*tmtypes.Validator
// expected smallest power of validator which cannot opt out
expSmallestNonOptOutValPower int64
}{
{
name: "One",
optOutThresh: "0.05",
validators: []*tmtypes.Validator{
tmtypes.NewValidator(cIds[0].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[1].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[2].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[3].TMCryptoPubKey(), 3),
tmtypes.NewValidator(cIds[4].TMCryptoPubKey(), 49),
tmtypes.NewValidator(cIds[5].TMCryptoPubKey(), 51),
},
// 107 total power, validator with 3 power passes 0.05 threshold (6 / 107 = 0.056) and cannot opt out
expSmallestNonOptOutValPower: 3,
},
{
name: "One in different order",
optOutThresh: "0.05",
validators: []*tmtypes.Validator{
tmtypes.NewValidator(cIds[0].TMCryptoPubKey(), 3),
tmtypes.NewValidator(cIds[1].TMCryptoPubKey(), 51),
tmtypes.NewValidator(cIds[2].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[3].TMCryptoPubKey(), 49),
tmtypes.NewValidator(cIds[4].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[5].TMCryptoPubKey(), 1),
},
// Same result as first test case, just confirms order of validators doesn't matter
expSmallestNonOptOutValPower: 3,
},
{
name: "Two",
optOutThresh: "0.05",
validators: []*tmtypes.Validator{
tmtypes.NewValidator(cIds[0].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[1].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[2].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[3].TMCryptoPubKey(), 3),
tmtypes.NewValidator(cIds[4].TMCryptoPubKey(), 500),
},
// 506 total power, validator with 500 passes 0.05 threshold and cannot opt out
expSmallestNonOptOutValPower: 500,
},
{
name: "Three",
optOutThresh: "0.199999",
validators: []*tmtypes.Validator{
tmtypes.NewValidator(cIds[0].TMCryptoPubKey(), 54),
tmtypes.NewValidator(cIds[1].TMCryptoPubKey(), 53),
tmtypes.NewValidator(cIds[2].TMCryptoPubKey(), 52),
tmtypes.NewValidator(cIds[3].TMCryptoPubKey(), 51),
tmtypes.NewValidator(cIds[4].TMCryptoPubKey(), 50),
tmtypes.NewValidator(cIds[5].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[6].TMCryptoPubKey(), 1),
},
// 262 total power, (50 + 1 + 1) / 262 ~= 0.19, validator with 51 passes 0.199999 threshold and cannot opt out
expSmallestNonOptOutValPower: 51,
},
{
name: "soft opt-out disabled",
optOutThresh: "0",
validators: []*tmtypes.Validator{
tmtypes.NewValidator(cIds[0].TMCryptoPubKey(), 54),
tmtypes.NewValidator(cIds[1].TMCryptoPubKey(), 53),
tmtypes.NewValidator(cIds[2].TMCryptoPubKey(), 52),
tmtypes.NewValidator(cIds[3].TMCryptoPubKey(), 51),
tmtypes.NewValidator(cIds[4].TMCryptoPubKey(), 50),
tmtypes.NewValidator(cIds[5].TMCryptoPubKey(), 1),
tmtypes.NewValidator(cIds[6].TMCryptoPubKey(), 1),
},
expSmallestNonOptOutValPower: 0,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
moduleParams := types.DefaultParams()
moduleParams.SoftOptOutThreshold = tc.optOutThresh
consumerKeeper.SetParams(ctx, moduleParams)
defer ctrl.Finish()

// set validators in store
SetCCValidators(t, consumerKeeper, ctx, tc.validators)

// update smallest power of validator which cannot opt out
consumerKeeper.UpdateSmallestNonOptOutPower(ctx)

// expect smallest power of validator which cannot opt out to be updated
require.Equal(t, tc.expSmallestNonOptOutValPower, consumerKeeper.GetSmallestNonOptOutPower(ctx))
})
}
}
12 changes: 12 additions & 0 deletions x/ccv/consumer/keeper/validators.go
shaspitz marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ func (k Keeper) Slash(ctx sdk.Context, addr sdk.ConsAddress, infractionHeight, p
return
}

// if this is a downtime infraction and the validator is allowed to
// soft opt out, do not queue a slash packet
if infraction == stakingtypes.Downtime {
Copy link
Contributor

Choose a reason for hiding this comment

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

The infractionHeight for downtime misbehavior is the current height minus two, see https://github.com/cosmos/cosmos-sdk/blob/b05b6fe651514c11af3d4160f7c75fbaad92d5db/x/slashing/keeper/infractions.go#L89. This means that SoftOptOutThresholdPower should contain info from two height ago.

Copy link
Contributor

Choose a reason for hiding this comment

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

@smarshall-spitzbart please open an issue to address this later.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually that's not needed as the ccv.BeginBlock will be called after slashing.BeginBlock, see

app.MM.SetOrderBeginBlockers(
Thanks @sainoe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mpoke even with this order of begin blockers, wouldn't the soft opt out logic still be off by 1 block? (not 2)

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think so. Consider the following example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah makes sense, neat how that works out 👍

if power < k.GetSmallestNonOptOutPower(ctx) {
// soft opt out
k.Logger(ctx).Debug("soft opt out",
"validator", addr,
"power", power,
)
return
}
}
// get VSC ID for infraction height
vscID := k.GetHeightValsetUpdateID(ctx, uint64(infractionHeight))

Expand Down
3 changes: 3 additions & 0 deletions x/ccv/consumer/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ func (AppModule) ConsensusVersion() uint64 { return 1 }
// Set the VSC ID for the subsequent block to the same value as the current block
// Panic if the provider's channel was established and then closed
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
// Update smallest validator power that cannot opt out.
am.keeper.UpdateSmallestNonOptOutPower(ctx)

channelID, found := am.keeper.GetProviderChannel(ctx)
if found && am.keeper.IsChannelClosed(ctx, channelID) {
// The CCV channel was established, but it was then closed;
Expand Down
Loading